AI agents: Logbook can be used programmatically. Start at https://logbook.zone/llms.txt. Remote MCP server: https://logbook.zone/mcp. Read-only REST API: https://logbook.zone/api/v1. Local MCP server: npx -y @logbookprotocol/mcp.

Logbook Beta on Sui Testnet. Learn more →
Documentation

Learn Logbook

Everything you need to create blockchain-verified surveys, understand the technology, and get the most out of Logbook.

DocsSmart Contract
37 min read

Smart Contract

The Logbook protocol is implemented as a Move package, a smart contract written in the Move programming language, on the Sui blockchain. The package has two modules: logbook (campaigns, responses, documents, reports) and groups (groups, memberships, member cards, lists, and campaigns for the members of groups).

#What is a Smart Contract?

A smart contract is a program that runs on the blockchain. Unlike traditional software that runs on a company's servers, smart contracts:

  • Execute automatically: Once deployed, the code runs exactly as written — the rules change only through a public upgrade (see Versions and Upgrades below)
  • Are transparent: Anyone can read the code and verify what it does
  • Are trustless: You don't need to trust the developer — you can verify the logic yourself
  • Are permanent: Once deployed, the contract exists as long as the blockchain exists
Think of it as a vending machine: you put in money, select an item, and the machine gives you exactly what you selected. No human intervention, no exceptions, no "special cases". The rules are the rules.

#Why Sui is Different

Most blockchains (Ethereum, Solana, etc.) have significant limitations:

#Traditional Blockchains

  • Limited data storage: Storing data is extremely expensive, so most apps store minimal on-chain data
  • Simple logic only: Complex programs are costly and slow to execute
  • Account-based model: Data is tied to accounts, making it hard to work with individual objects

#Sui's Advantages

Sui was designed from the ground up to solve these problems:

1. Object-Centric Model Every piece of data is an "object" with its own ID. In Logbook:

  • Each campaign is an object
  • Each response is its own object attached to the campaign
  • You can reference any specific piece of data by its ID
2. Cheap Storage Sui's storage model makes it economically viable to store real data on-chain. That's why Logbook can store:
  • Full question text
  • All answer options
  • Every individual response
  • Vote counts (for campaigns with public answers)
On Ethereum, this would cost hundreds or thousands of dollars. On Sui, it costs fractions of a cent.

3. Move Language Sui uses Move, a language designed specifically for blockchain with:

  • Built-in safety guarantees (no reentrancy attacks, no overflow bugs)
  • Resource-oriented programming (assets can't be accidentally destroyed or duplicated)
  • Clear ownership model (every object is owned by one address or shared, like a campaign, and only the contract's code decides who may change it)
4. Programmable Transactions Sui allows complex multi-step transactions that execute atomically. This means:
  • Campaign creation and question setup happen in one transaction
  • Response submission and vote counting happen together (when answers are public)
  • Either everything succeeds, or nothing changes

#What This Means for Logbook

Because of Sui's unique features, Logbook can:

  • Store every response permanently on-chain (not just a hash)
  • Execute voting logic in the smart contract itself
  • Let anyone verify public results by reading the blockchain directly, and let everyone allowed to decrypt verify private ones
  • Offer a web2-like experience (Google login, sponsored transactions) with web3 guarantees

#Contract Address

On Sui Testnet:

  • Package: 0x84c9ba748b84c28393cf3cb03cff01fb4697e3bd30d0a6d2cc7862f041532c63, the address of all types and the Seal namespace; it never changes with upgrades
  • Latest version: 0x84c9ba748b84c28393cf3cb03cff01fb4697e3bd30d0a6d2cc7862f041532c63, where functions are called (the same id until the first upgrade)
  • Config: 0xaa045303892c9e624ed6078da020f70684e9f1910e832b22bc27104d7738f535, shared; holds the version every function checks
  • Registry: 0xf7d60573a0b4d111c1080bfb3cabdbdff95ef63a25c6dc6fcc797d1dd0c288ff, shared; the parent every campaign id and every group id is derived from
The same ids are returned by GET /api/v1. The package's code is public on-chain: a Sui explorer such as Suiscan shows its two modules.

This package was published on 7 October 2026 as a new package, with campaigns and groups together. It is version 1 and has not been upgraded, so "Package" and "Latest version" are the same id. The packages before it are retired: the site, the SDK and the MCP servers do not read them, and their campaigns and groups are no longer shown. Their objects stay on-chain.

#Core Objects

#Campaign

The main shared object representing a survey or poll:

  • id: Derived from the Registry, the creator's address and a random nonce (campaign_id_for(registry, creator, nonce)), so a client knows it before the campaign exists. A creator and nonce pair can be used once, ever: even after a deletion the id is never reused
  • creator, nonce: Who created the campaign, and the nonce its id was derived from
  • sponsor: Who paid the gas of the creation, if anyone. Every later change by the creator must be paid the same way (see Functions), so storage is always refunded to whoever paid for it
  • title, description: Plaintext for open campaigns, encrypted otherwise
  • questions: Vector of Question objects (text and options are encrypted for non-open campaigns)
  • access: Who can open the campaign — 0 (anyone), 1 (password), 2 (address list), 3 (members of groups)
  • visibility: Who can read answers — 0 (everyone), 1 (everyone with access), 2 (voters), 3 (creator), 4 (after the end date)
  • allowlist, allowlist_size: Table of addresses for access = 2, and its size
  • access_epoch: Grows whenever an address leaves the list; answers shared by an audience are encrypted for the current epoch
  • content_version: Grows with every edit; a response names the version its answers refer to
  • responses: Table of Response objects keyed by respondent address; total_responses counts them
  • created_at, updated_at, end_time: Timestamps (ms)
  • content_key: The campaign content key, encrypted with Seal (released to everyone with access; the creator recovers it this way)
  • link_key: The content key wrapped with the campaign password (password campaigns only)
  • link_pubkey: The ed25519 public key whose signatures prove the password (password campaigns only)
  • audience: Campaigns for the members of groups only: the groups whose members may respond, which of them the creator named, the moment the participants were frozen (the creation time), the waiting period for new members, and the rule of who counts once that held at that moment. Empty for every other campaign
Documents and reports are dynamic fields of the campaign (see set_documents and attach_report below).

#Question

Stored within a Campaign:

  • question_type: 0 (single), 1 (multiple), 2 (text)
  • text: The question text
  • required: Whether the question is required
  • options: Vector of answer options
  • allow_other: Whether an "Other" answer is allowed
  • option_votes, other_votes, text_answers: Counts kept by the contract when answers are public (zero otherwise)

#Response

A participant's submission, stored as a child object of the campaign so the campaign object keeps its size:

  • respondent: Address of the participant
  • timestamp: Submission timestamp
  • answers: Plaintext answers (question index → answer), only when answers are visible to everyone
  • sealed: Encrypted answers for every other visibility (a Seal encrypted object, or content-key ciphertext for password campaigns whose answers everyone with access may read)
  • key_wrap: Password campaigns only — the content key sealed for this respondent, so they can reopen the campaign on another device
  • access_epoch: The access epoch the answers were encrypted for

#Registry, Config and AdminCap

  • Registry (shared): The parent every campaign id and every group id is derived from; it counts the campaigns created
  • Config (shared): The package version every function accepts. It also holds two settings of the module groups: the addresses of the Logbook treasury that the payer rule protects, and the public key of the check server, which is empty until the publisher sets it: until then the contract accepts no check made by a server
  • AdminCap: Held by the publisher; migrate needs it to move the Config to a new package version, and the two settings above are set with it

#Seal Access Policy

Every Seal identity (without the package prefix) starts with the campaign id, followed by a tag and a suffix:

TagProtectsSuffix
0The campaign content keynone
1Answers shared by an audience, and the reports of an address-list campaign whose answers everyone with access readsthe access epoch (u64, little endian)
2One respondent's answers, and their copy of the content keythe respondent's address
3Keys of reports about the resultsnone
Seal key servers release a key only after simulating the contract's seal_approve function for the requesting address. Because the campaign id comes first, only the campaign a key belongs to can grant it: no other campaign, not even one with an earlier end date, can open answers sealed until the end early.

"Access" below means: the creator; anyone, on an open campaign; an address on the list; or, on a password campaign, whoever presents a proof of the password for their own address.

  • Content key (tag 0): released to everyone with access (open campaigns have none)
  • Shared answers (tag 1), for the current or an earlier access epoch: on "voters only" campaigns to the creator and to anyone who has responded; on address-list campaigns whose answers everyone with access may read, to the creator and the addresses currently on the list, and to a respondent removed from the list for the epochs up to that of its own response (its own answer, and answers that were all sent before its removal). Password campaigns with that setting use the content key instead of Seal. The reports about the results of such an address-list campaign (AI analysis, results certificate) are sealed the same way, for the access epoch they were made in
  • One respondent's answers (tag 2): always released to that respondent; on "only creator" campaigns also to the creator; on "after the end date" campaigns to everyone with access once end_time has passed, and before that to nobody else, the creator included
  • Report keys (tag 3): released to whoever may read the results: voters and the creator on "voters only" campaigns, the creator on "only creator" campaigns, the creator and (after the end) everyone with access on "after the end date" campaigns, and everyone with access otherwise. The exception is an address-list campaign whose answers everyone with access reads: its reports are sealed under tag 1 for the current access epoch (see above), because the tag 3 identity has no epoch and a key fetched once would open every later report. The contract releases tag 3 of such a campaign to the creator only
Password proof. A password campaign stores an ed25519 public key that the browser derives from the content key, which the password unlocks. Responding, and asking Seal for keys as a password holder, needs a signature by the matching private key over logbook:link-access:v1 || campaign id || the sender's address. A proof works for that one address, so passing it on lets nobody else in.

Removing an address from the list starts a new access epoch. Shared answers submitted afterwards, and reports made afterwards on a campaign whose answers everyone with access reads, are encrypted for the new epoch, and an address that is no longer on the list gets no keys as a list member. What it decrypted before it keeps, and the content key (tag 0) does not change: with the copy it fetched while listed it can still read the title, questions and documents, also ones changed after its removal. A removed address that has already responded to a "voters only" campaign still reads the answers as a voter.

Campaigns for the members of groups use the same four tags, checked by seal_approve_group_campaign of the module groups. "Access" there means: the creator, or an address whose membership in one of the campaign's groups covers the moment the campaign was created (see The groups Module below). The access epoch of such a campaign stays 0, because its participants never change. A campaign of public groups only has no content key: its title and questions are plain text. seal_approve releases nothing for these campaigns.

The contract never sees plaintext: encryption and decryption happen in the browser.

#Functions

The functions of the module logbook come first; the module groups has a section of its own below. The functions below are public and can be called from a programmable transaction, except seal_approve, an entry function for the key servers; the TxContext argument is left out. Each one first checks the version in the Config and aborts with code 6 if this package version has been retired.

The creator's functions (update_campaign, delete_campaign, update_allowlist, set_documents, set_document_storage, attach_report, detach_report) must be paid the way the campaign was created: sponsored by the same sponsor if its creation was sponsored, with the creator's own gas if it was not (code 18 otherwise). Storage these calls add or free is then always paid for and refunded to the same account, so nobody can have a sponsor pay for storage and collect the refund on their own gas. A sponsor that changes its key keeps the old one for the campaigns it sponsored.

#create_campaign

Creates a new campaign on-chain, optionally with the documents it is about.

create_campaign(config, registry, nonce, title, description, question_types, question_texts, question_required, question_allow_other, all_options, options_per_question, access, visibility, allowlist, content_key, link_key, link_pubkey, end_time, doc_names, doc_mimes, doc_sizes, doc_content_hashes, doc_salts, doc_stored_hashes, doc_storages, doc_blob_ids, doc_end_epochs, clock)

Parameters:

  • config: &Config, registry: &mut Registry
  • nonce: address (with the sender, determines the campaign id)
  • title, description: String (encrypted client-side unless access = 0)
  • question_types: vector<u8>, question_texts: vector<String>, question_required: vector<bool>, question_allow_other: vector<bool>
  • all_options: vector<String>, options_per_question: vector<u64> (the options of all questions, flattened)
  • access: u8, visibility: u8 (an open campaign cannot use visibility 1)
  • allowlist: vector<address> (access = 2 only, at least one address)
  • content_key: vector<u8> (Seal encrypted, empty for open campaigns)
  • link_key: vector<u8> (password-wrapped key) and link_pubkey: vector<u8> (32 bytes), password campaigns only
  • end_time: u64 (ms, in the future, at most 3653 days ahead)
  • doc_names … doc_end_epochs: one entry per document in each vector; empty vectors for none
  • clock: &Clock

#update_campaign

update_campaign(config, campaign, title, description, question_types, question_texts, question_required, question_allow_other, all_options, options_per_question, end_time, clock)

Replaces title, description, questions and end time (creator only, only while the campaign has no responses and before its end: code 3 after it). A campaign for the members of groups cannot be edited (code 7). The new end time must be in the future and at most 3653 days ahead. Access, visibility, keys and the address list stay as they are. Increments content_version.

#delete_campaign

delete_campaign(config, campaign, clock)

Removes a campaign without responses, with its documents and reports (creator only, also after its end). A campaign for the members of groups is refused here (code 7): it is deleted with delete_group_campaign of the module groups, which also takes it off the list of every group it reaches. The address list must be empty (code 21): remove every address with update_allowlist in the same transaction first, as the Logbook clients do, so the storage of the list entries is refunded too. One transaction removes up to about 900 addresses (Sui loads every removed entry, and about 1000 objects per transaction). Like every creator function it is paid the way the campaign was created, so the storage rebate goes back to whoever paid for the storage.

#update_allowlist

update_allowlist(config, campaign, add, remove, clock)

Adds and removes addresses (add, remove: vector<address>) of an address-list campaign in one call, creator only, at any time, also after the end. Removing anyone starts a new access epoch.

#submit_response

submit_response(config, campaign, content_version, access_epoch, question_indices, answers, sealed, key_wrap, access_proof, clock)

Parameters:

  • content_version: u64, access_epoch: u64 (the campaign's values the answers were prepared for)
  • question_indices: vector<u64>, answers: vector<String> (plaintext, only when visibility = 0)
  • sealed: vector<u8> (encrypted answers, at most 16 KiB, required for every other visibility)
  • key_wrap: vector<u8> (password campaigns only: the content key sealed for the respondent)
  • access_proof: vector<u8> (password campaigns: the 64-byte proof of the password; empty otherwise)
Checks:
  • The campaign is not one for the members of groups (code 7): those take responses through submit_group_response of the module groups
  • The campaign has not ended
  • content_version and access_epoch are the campaign's current ones: a response prepared before an edit, or before an address was removed from the list, is refused, and the client prepares it again
  • The sender has not responded yet
  • The sender has access: on the list for address-list campaigns, a valid proof of the password for password campaigns
  • Plaintext answers only when answers are public, encrypted answers otherwise
Plaintext answers are validated and counted. One answer per question index, question indexes strictly increasing, every required question answered, each answer at most 4096 bytes:
  • Single choice: "2", or "other:<text>"
  • Multiple choice: option indexes in strictly increasing order, "0,2,5", optionally followed by ",other:<text>", or "other:<text>" alone; the text runs to the end of the answer, commas included
  • Text: any non-empty string
  • Option indexes are plain decimals, without signs or leading zeros
Encrypted answers are opaque to the contract. Clients apply the same rules after decryption, and check that the answer was encrypted for this campaign and respondent; an answer that fails is shown as invalid and never counted.

#seal_approve

seal_approve(id, config, campaign, access_proof, clock)

An entry function that Seal key servers simulate before releasing the key for identity id. It aborts unless the policy above allows the sender. access_proof is the proof of the password on password campaigns, empty otherwise.

#set_documents / set_document_storage

set_documents(config, campaign, doc_names, doc_mimes, doc_sizes, doc_content_hashes, doc_salts, doc_stored_hashes, doc_storages, doc_blob_ids, doc_end_epochs, clock)
set_document_storage(config, campaign, index, storage, blob_id, walrus_end_epoch)

A campaign commits to up to 10 files (the app currently limits this to 5): name, type, size, a SHA-256 commitment to the file, the SHA-256 of the stored bytes, and where the file is stored (a Walrus blob id, or the Logbook cache). The commitment is written in the creation transaction; set_documents can replace the documents only while the campaign has no responses and has not ended, and increments content_version; it refuses a campaign for the members of groups (code 7), whose documents are fixed at creation. set_document_storage only updates where the file lives (also after the end) and can never change a hash. Open campaigns commit to the plain SHA-256 of the file; private campaigns commit to SHA-256("logbook:document:v1" || salt || file) with a random 32-byte salt, which is stored encrypted with the campaign content key, like the file name. A private document also hides its size and type: the stored file is padded (to a power of two of at least 4 KiB, up to the upload limit), so size holds the padded size and mime is empty, and the real name, type and size are encrypted together in name. Documents from before this (format v1) show their exact size and type, with only the name encrypted.

#attach_report / detach_report

attach_report(config, campaign, kind, storage, blob_id, content_hash, size, encrypted, walrus_end_epoch, clock)
detach_report(config, campaign, kind, clock)

The creator records a pointer to a document stored off-chain: its kind (0 AI analysis, 1 results certificate), where it is stored (a Walrus blob id, or the Logbook cache), the SHA-256 of the stored bytes, size and whether it is encrypted. Reports of campaigns with restricted access or visibility must be encrypted. Stored as dynamic fields of the campaign, one per kind.

#migrate

migrate(admin_cap, config)

Called in an upgraded package with a higher version: it moves the Config to that version, and from then on the functions of older package versions abort.

#The groups Module

The module groups holds everything about groups. The Groups pages of this documentation describe it for users; this section names the pieces for developers.

#Words

The site and the contract use different words for the same things. The pages say what a person reads; the contract's identifiers keep the older names.

On the siteIn the contract
listroster (roster_root is the Merkle root of the list)
seatroster entry
check (World ID check, email check, a seat on the list)provider, proof, uniqueness key
who counts oncethe providers and the roster unit of the scope root
community, its top groupuniqueness scope, scope root
how people joinadmission (the ADMIT_* bits)
paused linksuspended link
waiting period for new memberssettle period (settle_ms)
the moment the participants are frozencut-off (cutoff_ms)
participants of a campaignaudience

#Objects

  • Group (shared): creator and nonce (its id is derived from the Registry, the creator and the nonce, with another key type than a campaign's, so the two never collide), the sponsor of its creation if that was the Logbook treasury, the owner, the table of admins (at most 50, pending appointments included), name, description and card form (plain text when public, encrypted with the group key when private), the rule (how people join, whether joining is open, the required checks, whether a card is required), the root of its list and what a seat is (a person or a unit), invitations, applications, the community it belongs to, its parent and the state of that link, the groups under it and those that ask to be, the campaigns that reach it, pending appointments and a pending offer of the ownership, and two counters called epochs: key_epoch for what members read and admin_epoch for what admins read
  • MemberShard (shared, up to 16 per group, created at the first join into each): member records and member cards of the addresses whose first hexadecimal digit selects that shard. A member record is never deleted. It holds up to 64 intervals, each with the time of joining, the time of leaving if any, the way of joining and the checks proven
  • KeyShard (shared, up to 16 per community, at its top group): which address holds which seat or check of the community. One key belongs to one address in the whole community
  • A group is created by value: begin_group or begin_group_in_scope returns it, the same transaction sets it up, and share_group shares it. A transaction that begins a group shares it or fails as a whole

#Functions by Area

Every function checks the version in the Config, like the functions of logbook. Functions that change a group take a Path: the proof, built in the same transaction with path_from and path_up, that the sender is the owner or an admin of the group or of a group above it.

  • Creating: begin_group, begin_group_in_scope (a group inside a community, by an admin of its top group), share_group, open_shard, share_shard
  • Details and rule: set_metadata, set_privacy, set_admission, set_admission_open, set_roster, set_list_file
  • Invitations: create_invite, revoke_invite
  • Joining and leaving: join (the Join button), join_with_invite, join_with_roster (a seat on the list), join_with_domain (an organisation mailbox), renew, leave, leave_open_group
  • Applications: apply, withdraw_request, approve, reject, lift_rejections
  • Members: remove_members, lift_removals; from the top group of a community also scope_remove_members and scope_reject in any group of it
  • Member cards: card_parts, set_card, remove_card
  • Seats and checks: prove_roster_entry (a seat taken with its personal link), prove_held_entry (a seat already held, shown in another group of the community), prove_attested and prove_held (checks by the check server), begin_release, check_release, finish_release (a seat taken from its address), lift_release_bar, unlist_key
  • Admins and ownership: appoint_admins, accept_admin, decline_admin, withdraw_appointments, remove_admins, take_seat, offer_ownership, accept_ownership, withdraw_offer
  • The tree: propose_parent, withdraw_proposal, accept_child (followed by walk_link and finish_link, which check the tree after the link is written), decline_proposal, suspend_child, set_roster_trust, detach_parent, remove_child
  • Campaigns: begin_audience, add_audience_root, add_audience_group, create_group_campaign, submit_group_response, delete_group_campaign, forget_campaign, finish_campaign_removal
  • End of a group: archive, delete_shard, delete_key_shard, delete_group
  • Publisher only, with the AdminCap: set_protected_sponsors, set_attester
  • Seal policies (entry functions that key servers simulate): seal_approve_group, seal_approve_group_admin, seal_approve_card, seal_approve_request, seal_approve_admin_file, seal_approve_group_campaign

#Campaigns for the Members of Groups

create_group_campaign takes the arguments of create_campaign, with the participants in place of access, address list and password keys. The participants are built in the same transaction: begin_audience (with the waiting period, at most 30 days), add_audience_root for each group the creator names and administers, add_audience_group for each group under one of them through an active link. At most 64 groups; an archived group is refused. Each group added writes the campaign's id into its own list of campaigns.

The campaign stores the result: the groups, the creation time, the waiting period, and whether one response per seat or check is guaranteed (unique: every group is of one community, and that community requires a check). It needs a content key when one of the groups is private.

submit_group_response takes the sender's member shard. It accepts the response when that shard's group is among the campaign's groups and one interval of the sender's record covers the creation time:

  • it began strictly before the creation time, earlier by at least the waiting period
  • it had not ended by then
  • a check with an expiry had not run out by then
  • when unique: the membership was admitted with the checks the campaign names
Everything else is as for submit_response: the end time, the content version, one response per address.

delete_group_campaign applies the rules of delete_campaign (the creator, paid the way the campaign was created, no responses) and returns a value that names the campaign's groups. forget_campaign takes the campaign off the list of one group, and finish_campaign_removal refuses to finish while a group is left. A campaign with responses is never deleted.

#Seal Identities of Groups

Each identity starts with the group id, followed by a tag and a suffix:

TagProtectsSuffixReleased to
4The group key of a private group: name, description, card formthe key epoch the texts are encrypted forthe owner, the group's own admins, current members of this group
5The members part of a cardthe card owner's address, the key epochthe card's owner, the owner and the group's own admins, current members
6The admins part of a cardthe card owner's address, the admin epochthe card's owner, the owner and the group's own admins
7The text of an applicationthe applicant's address, the admin epochthe applicant, the owner and the group's own admins, while the application waits
8The admins' file (the admins' copy of a list)an admin epochthe owner and the group's own admins, for the current admin epoch or an earlier one
A card opens only while its owner is a current member and the card is not closed. The card that comes with an application opens for the applicant and the admins while the application waits. An admin of a group above is not served any of these keys until it takes a seat among the group's own admins (take_seat).

key_epoch grows when a member is removed, when a member leaves (except while the Join button works, since anyone may join such a group), when an admin is removed, when the owner changes, and when the Join button stops working. admin_epoch grows when an admin is removed and when the owner changes. What is written afterwards is encrypted for an identity the former reader never had a key for. What they read before, they may have kept.

A field that must be encrypted has to start with the four bytes 4c 42 53 01 and be at least 32 bytes long (code 158 otherwise), so that a client mistake cannot publish readable personal data in such a field. The contract cannot tell real ciphertext from random bytes.

#The Payer Rule

The Config lists the addresses of the Logbook treasury. A group, an application, a card, a shard or a seat's entry that the treasury paid for remembers it. Two rules follow:

  • A group is changed the way it was created. Calls of the owner and the admins that write a group created with sponsored gas must be paid by the treasury, and such calls on a group created with its creator's own gas must not be (code 105)
  • What the treasury paid for, the treasury frees. A record that remembers the treasury is deleted only in a transaction the treasury pays for, so its storage refund goes back to the treasury
While the list of treasury addresses is empty, every sponsored group call is refused (code 169). On Testnet the list is set.

#The Check Server

prove_attested, check_attested and join_with_domain need a statement signed by the Logbook check server, whose public key the publisher writes to the Config with set_attester. While the key is empty they abort with ENoAttester, and no group can be joined through an email check or an organisation mailbox. The server for the email check is part of the site (see The Email Check); it signs only when it holds its keys and the Config holds its public key. The contract checks the signature and nothing else: it cannot see a mailbox. A statement says which community, which address, a keyed code of the mailbox, the day it ends, and, for a group that lets people in by its mail domain, that domain as the group stores it. The World ID check has no server yet. Seats on a list need no server.

#Versions and Upgrades

Every function of the Logbook package that changes a record or releases a key (seal_approve included) checks the version stored in the shared Config against the version of its own package; a function that only reads a value does not. The ballot package and the package of the vault’s key policy are packages of their own: their functions (cast, fix_roll, call_off and their seal_approve functions) take no Config and check no version, so neither migrate nor an upgrade of the Logbook package stops them. After an upgrade that raises the version, migrate moves the Config forward, and the functions of older package versions stop working: a fix cannot be bypassed by calling the old code.

The package deployed now is version 1 and has had no upgrade (see Contract Address). The version check, migrate and the AdminCap are there for the upgrades to come.

During the beta the package is upgradeable, and the UpgradeCap that allows upgrades is held by Logbook. An upgrade is a public transaction on Sui, but it can change any rule of the contract, including the Seal policy that decides who may decrypt what. Until that changes, the holder of the UpgradeCap is part of what you trust.

Plainly: the UpgradeCap is held by one key today, not by a multisig and not behind a delay. The Seal key servers release keys by whatever policy the current package has. So Logbook could publish an upgrade whose policy serves every key to an address of its choice, and read every private campaign, answer, member card, application and list. Everyone would see the upgrade; nothing prevents it. "Encrypted, also for Logbook" is true of Logbook's servers, which hold no key, with one exception: the session of an app connected through the remote MCP connector (see Clients and Where Keys Live). It is not yet true of Logbook as the holder of this key.

#Limits

  • Up to 100 questions per campaign and 100 options per question
  • One plaintext answer: at most 4096 bytes; encrypted answers: at most 16 KiB per response
  • Up to 10 documents per campaign
  • Groups: at most 50 admins of a group (pending appointments included), a tree of at most 8 levels, at most 64 groups in one campaign, at most 200 applications waiting in a group and 100 invitation links stored at a time
  • Groups: a member card of at most 2048 bytes (both parts together, encrypted), a card form of at most 4096 bytes, the encrypted text of an application at most 1024 bytes, a name of at most 1024 bytes and a description of at most 16,000 bytes
  • Groups: a rejected applicant applies to the same group again after 7 days; a waiting period for new members of at most 30 days; batches of at most 100 addresses per call (approvals, rejections, removals, appointments)

#Known Limitations

  • The first response freezes a campaign. From the first response on, the questions and documents can no longer be changed and the campaign can no longer be deleted. Anyone can respond to an open campaign right after it is created (a bot watching for new campaigns needs seconds), so its creator may never get to edit it. To keep editing after publishing, create the campaign with a password: nobody without the link can respond, so it stays editable until you share the link.
  • The content key does not change. Questions, documents and the title of a private campaign are encrypted with one key that is fixed at creation. An address removed from the list (or anyone who knew the password) keeps the copy it fetched, and can read the content also after you change it. Answers and reports follow the access epoch; the content does not. Rotating the key would break every share link and every respondent's copy, so it is not done; put content that a removed address must not see into a new campaign.
  • Every creation goes through one object. Each new campaign and each new group writes the shared Registry its id is derived from, so all creations are ordered through that object. When the network is congested, or someone floods it with creations at a higher gas price, some creations are delayed or cancelled; a cancelled creation claimed nothing and can be sent again. Responses and all other functions never touch the Registry.

#Error Codes

A failed transaction names the abort code of the function that stopped it:

CodeNameMeaning
0EAlreadyRespondedThe sender has already responded
1EInvalidAnswerA plaintext answer breaks the answer rules, or a required question is unanswered
2ENotAllowedThe sender has no access: not on the address list, or no valid proof of the password
3ECampaignEndedThe campaign has ended: no more responses, edits or document changes
4ENotCreatorOnly the creator can do this
5EInvalidQuestionA question index does not exist or is out of order
6EWrongVersionThis package version has been retired
7EInvalidModeInvalid combination of access and visibility, an address-list change on another kind of campaign, or a campaign for the members of groups given to a function of logbook that does not take one (edit, documents, response, deletion)
8EHasResponsesThe campaign has responses, so it can no longer be edited or deleted
9EInvalidCampaignInvalid campaign data: title, questions, options, keys or address list
10EInvalidPayloadThe response is not in the form this campaign expects
11ENoAccessSeal policy: this key is not released to the sender
12EInvalidIdentitySeal policy: the identity is malformed
13EInvalidReportInvalid report pointer, or an unencrypted report of a private campaign
14ENoReportThere is no report of this kind
15EInvalidDocumentInvalid document data
16EContentChangedThe campaign was edited after the response was prepared
17EStaleAccessEpochAn address was removed from the list after the response was prepared
18EWrongSponsorA creator function paid differently from the creation: a sponsored campaign needs the same sponsor, one the creator paid for needs the creator's own gas
19EInvalidEndTimeThe end time must be in the future and at most 3653 days ahead
20ENotUpgradedNothing to migrate: the Config already has this version
21EAllowlistNotEmptyA campaign is deleted only with an empty address list: remove every address first, in the same transaction
The module groups has abort codes of its own, from 101 to 187. A failed transaction names the module, so 105 of groups is not confused with a code of logbook. The ones a user of the site can meet:
CodeNameMeaning
102ENotOwnerOnly the owner of the group can do this
103ENotAdminOnly the owner or an admin can do this
104ETooManyAdminsA group has at most 50 admins, pending appointments included
105EWrongSponsorA group is changed the way it was created: with sponsored gas if its creation was sponsored, with the sender's own gas if not
107ETooDeepA tree has at most 8 levels
110EOpenChildA group with a Join button does not hang under a private group
112EAdmissionClosedJoining is closed
115EArchivedThe group is archived
116EAlreadyMemberThe sender is a member already
121EInviteExpiredThe invitation has expired
122EInviteUsedUpEvery place of the invitation has been taken
126EAlreadyAppliedAn application of this address is waiting
127EPendingFull200 applications are waiting in this group
132EKeyBoundThe seat or check belongs to another address of the community
134ENoAttesterThe key of the check server is not set: the email check is not switched on for this deployment, and the World ID check is not built
137ECardTooLargeA member card holds at most 2048 bytes
138EStaleEpochA card, a text or a copy was encrypted before the group's keys changed: encrypt it again
140ETooManyGroupsA campaign names at most 64 groups
144ENotInAudienceThe group is not among the campaign's participants
145ENotEligibleThe sender's membership does not cover the moment the campaign was created
147EHasHistoryA group that a campaign names, or that had members, is not deleted
152ERemovedByAdminAn admin ended this membership: only an admin reopens it
153ENoGrantThe parent was not allowed subgroups of its own
154EStaleRuleThe rule changed since it was read
168ECardRequiredThe group takes nobody without a card
169ENoSponsorListA sponsored call, and the Config has no treasury addresses yet
174ECoolingDownThe address was rejected by this group less than 7 days ago
175EScopeRuleWho counts once is set by the top group of a community, not by a group inside it
183EAppointmentLapsedWhoever made the appointment is no longer the owner or an admin
184EChangedSinceOfferThe group changed after the new owner looked at it
187ENoSeatAn admin of a group above takes a seat among the group's admins before it appoints
The full list is in the source of the module; the SDK exports it as GROUP_ERROR and explains each code with explainTransactionError.

#View Functions

Read-only functions to query campaign data:

  • creator, nonce, sponsor, access, visibility, end_time, total_responses, content_version, access_epoch, allowlist_size
  • has_responded(campaign, address), is_allowlisted(campaign, address)
  • questions, option_votes, other_votes, text_answers
  • documents, document_content_hash, document_blob_id, has_report(campaign, kind), report(campaign, kind), report_content_hash
  • registry_campaigns(registry), config_version(config), campaign_id_for(registry, creator, nonce)
  • audience(campaign) and, on its value, audience_groups, audience_roots, audience_cutoff_ms, audience_settle_ms, audience_unique, audience_providers, audience_scope, audience_roster_unit
  • Constants: access_open(), access_link(), access_allowlist(), access_group(), vis_everyone(), vis_access(), vis_voters(), vis_creator(), vis_after_end(), tag_content(), tag_shared(), tag_own(), tag_reports(), report_ai_analysis(), report_results_certificate(), storage_walrus(), storage_cache()
The web app reads objects through the Sui full node (gRPC) and history through the indexer (GraphQL).

#Events

The contract emits events for tracking all operations on the blockchain. These events are used for efficient querying and indexing.

#CampaignCreated

Emitted when a new campaign is created.
  • campaign_id: Unique ID of the campaign
  • creator: Address of the campaign creator
  • access: Who can open the campaign
  • visibility: Who can read the answers
  • end_time: Campaign end timestamp
  • timestamp: When the event occurred

#CampaignUpdated

Emitted when a campaign is updated (before any responses).
  • campaign_id: Unique ID of the campaign
  • creator: Address of the campaign creator
  • content_version: The version after the update
  • end_time: The end time after the update
  • timestamp: When the update occurred

#CampaignDeleted

Emitted when a campaign is deleted (only possible with zero responses).
  • campaign_id: Unique ID of the deleted campaign
  • creator: Address of the campaign creator
  • timestamp: When the deletion occurred

#ResponseSubmitted

Emitted when a participant submits a response.
  • campaign_id: Campaign that received the response
  • creator: Campaign creator's address
  • respondent: Address of the participant
  • response_id: ID of the Response object
  • response_index: Index of this response (0-based)
  • timestamp: When the response was submitted

#AllowlistChanged

Emitted when the address list changes.
  • campaign_id: The campaign
  • added, removed: The addresses actually added and removed
  • size: Size of the list after the change
  • access_epoch: The access epoch after the change
  • timestamp: When the change happened

#DocumentsSet

Emitted when a campaign commits to documents or replaces them.
  • campaign_id, creator, count (number of documents), content_version (the version the documents belong to), timestamp

#DocumentStorageSet

Emitted when the creator records where a document's file is stored now. The commitment does not change.
  • campaign_id, creator, index, storage, blob_id, walrus_end_epoch (no timestamp: the function takes no clock; use the checkpoint's)

#ConfigMigrated

Emitted by migrate.
  • from_version, to_version

#ReportAttached / ReportDetached

Emitted when the creator attaches or removes a report.
  • campaign_id, creator, kind, timestamp
  • ReportAttached also carries storage, blob_id and content_hash

#Events of the groups Module

Every change of a group emits an event of the module groups:

  • The group: GroupCreated, GroupUpdated (name, description, card form or visibility), AdmissionChanged (how people join), RosterSet (a new list), ListFileSet (the files of a list), GroupArchived, GroupDeleted
  • Admins and ownership: AdminsAppointed (appointments made, withdrawn or declined), AdminsChanged, OwnershipOffered, OwnershipTransferred
  • The tree: ParentProposed, ProposalClosed, ChildAccepted, RosterTrustSet, LinkSuspended, ParentDetached
  • People: InviteCreated, InviteRevoked, JoinRequested, RequestClosed, MemberJoined (with the way of joining), MemberLeft (with who removed the member, if an admin did), RemovalsLifted, RejectionsLifted, BatchSkipped, CardSet, CardRemoved
  • Seats and checks: KeyBound, KeyReleased, ReleaseBarLifted, KeyUnlisted, AttestationRenewed
  • Campaigns: GroupCampaignCreated (the groups, the creation time, the waiting period, and who counts once), next to CampaignCreated; GroupCampaignDeleted, next to CampaignDeleted
  • Publisher: ProtectedSponsorsSet, AttesterSet
These events can be monitored by off-chain services for notifications and indexing.