Protocol specification
Changelog v2.8 (M1 amendments, 2026-09-29)
Section titled “Changelog v2.8 (M1 amendments, 2026-09-29)”- D-023 (owner):
article_idis a contract-assigned sequence; the article box storesasa_id. Reopens the section 2 row “Article identity”; the table text is left as written and the amendment lives here and in DECISIONS.md. - D-024 (owner): ARC-19 template url and reserve = CID digest from M1 (supersedes the section 7.1 DECISION about M5).
- D-001 / D-003:
paytransaction argument on box/asset-creating methods;create(governance)method (section 12). - D-027 / D-045: pqsig fee is a flat +2 minimum fees (usage model), not a per-byte surcharge;
algokey pq check-addresswording (sections 12.1, 13.2). - D-026 (owner): Pinata replaces web3.storage (sections 3, 14.4).
- D-029: LocalNet pinned through a repo-owned docker-compose (section 3).
- D-002: section 20 status-matrix criterion split between LocalNet and offline tests.
- D-034 / D-047: taxonomy example wording and field-name limit of 96 bytes (section 4).
- D-035: section 14.5 canonical parameters completed.
- D-037: section 21 “decoding via ARC-56 client” replaced by the in-repo ARC-28 decoder / algokit-subscriber.
- Editorial: section 3.1/3.2 order, section 16 numbering,
bytes36= ARC-4byte[36], section 13.3 figures (article box 192 B, 82 900 µAlgo, publish deposit 212 200 µAlgo).
The inline edits are applied progressively during M1 and tagged EDIT D-xxx.
Changelog v2.9 (M2 amendments, 2026-09-29)
Section titled “Changelog v2.9 (M2 amendments, 2026-09-29)”- D-054 (owner): multi-author allocation = equal split of the vote weight among the accepted authors, remainder to the submitter. Closes the section 10.2 M2 gate and the section 22 open question. (Its first implementation, with three accumulators in the article box, was replaced by D-072: see changelog v2.10.)
- D-055:
payargument on every M2 method that creates a box; status columns of the M2 methods (sections 7.4, 12). - D-056: box sizes of review, reviewer index, comment, flag (section 13.3).
- D-057: pushed grants read the recipient’s box only for a real grant; event order (sections 10.2, 11).
- D-058 / D-020 / D-065:
Flaggedcarriesnew_status; flags keep accumulating while disputed;DisputeResolved.dispute_roundis the resolved round; the last round (65 535) takes no flags (sections 7.4, 9, 11). - D-059 / D-061: mentions are passed in strictly ascending order of the raw address (sections 8.3, 11, 12).
- D-060:
updated_atis stamped only by calls that change an article field (section 6.4). - D-064: helpers are inlined; every method fits one application call with at least 120 opcodes to spare (section 21 C).
- D-066: readings of section 8 confirmed by tests (reviewer who later becomes a co-author, votes on resolved comments).
- New section 20.1: M2 acceptance criteria, in the style of section 20.
Changelog v2.10 (co-author model, 2026-09-29)
Section titled “Changelog v2.10 (co-author model, 2026-09-29)”- D-072 (owner, reopens the section 2 row “Multi-author”): the co-authors are declared in
publish(at most 25, ascending) and never change; each one confirms by signing, in preprint, under_review or final, unless they already voted on or reviewed the article; every accepted author is entitled to an equal share of ALL votes since publication (floor for co-authors, ceiling for the submitter).invite_coauthorandCoauthorInvitedare removed; the article box is 192 bytes again. It supersedes the mechanics of D-054 described in changelog v2.9 (sections 2, 6.4, 7.4, 7.6, 8.0, 10.2, 10.3, 11, 12, 13.3, 17, 18, 20.1). - D-073:
extend()lends opcode budget and box references to a publication with many co-authors; the Layer C gate counts application calls (sections 12, 21 C).
Changelog v2.11 (M3 indexer and M4 hosting, 2026-10-01)
Section titled “Changelog v2.11 (M3 indexer and M4 hosting, 2026-10-01)”- D-076:
chain_eventsprimary key is(tx_id, log_index)(one application call emits up to three events); the indexer decodes the logs with the same ARC-56 definition as the contract tests (section 15). - D-077: the sample-article fixture’s
authors[0]is the real zero address; the fixture CID changed accordingly (section 14.5). - D-084: the manifest gains
startRound,algoIndexerandfetchGateway(section 3.2); the indexer pins a second copy through its own Kubo node and serves a gateway that never fetches (sections 14.4, 15). - D-085: Path A uploads the CAR built in the browser to Pinata with the author’s own key; Path B verifies through a public trustless gateway (section 14.4).
- D-086: hosting is one server with Docker Compose and Caddy; the app, the indexer APIs and the IPFS gateway are three origins (sections 15, 16).
Ariadne — Open Science on Algorand
Section titled “Ariadne — Open Science on Algorand”Protocol Specification v2.7
Section titled “Protocol Specification v2.7”Name: Ariadne (tagline: “Ariadne — open science on Algorand”). Domain and EU trademark availability still to be checked before launch.
This document is normative. Words in MUST / MUST NOT / SHOULD / MAY are requirements. Every MUST is expected to have a corresponding test. When something is ambiguous, prefer the simplest option, implement it, and record it in
DECISIONS.mdat the repo root. Do not silently reopen anything in §2.
1. Purpose and principles
Section titled “1. Purpose and principles”Ariadne is an open scientific publishing protocol where every action is a signed Algorand transaction: publishing, versioning, reviewing, voting, commenting, flagging. Article content lives on IPFS; Algorand holds canonical metadata, the content identifier and the complete action history. There is no editorial gatekeeper; quality emerges from field-specific, reputation-weighted voting.
Principles:
- Open access. No platform fees for authors or readers. Network fees and storage deposits exist and are always paid by the user performing the action (§13). The platform never subsidises and never pays on anyone’s behalf.
- Append-only. Nothing is ever deleted. State changes are appended; retraction is a state, not a removal.
- Permissionless. Anyone can act from day one with reputation 0. Reputation is earned, never granted.
- Durable content. Plain text (Markdown + LaTeX) plus its own assets, all under one content identifier, so it stays readable in 30 years.
- English as the language of publication. UI is minimalist, readable, modern.
- Chain is truth. Everything off-chain (indexer, web) is a projection that can be rebuilt from chain data.
[EDIT D-107/D-108: the checks of declared ORCID iDs and DOIs are cached results of outside services (ORCID, Zenodo, DataCite), like the indexer’s
contentrecord of IPFS: the declarations are rebuilt from the chain; the checks are asked again from those services.]
2. Closed product decisions (do not reopen)
Section titled “2. Closed product decisions (do not reopen)”| Topic | Decision |
|---|---|
| Storage | Article content on IPFS as a directory (index.md + assets/), pinned. Chain stores the directory CID + metadata. Contract never reads IPFS. |
| Article identity | Each article is an ASA created by the contract via inner transaction. article_id == asa_id. ARC-71 Non-Transferable ASA, the application being the Issuer (§7.1). Kept deliberately for wallet/explorer visibility; the article box, not the asset, is the authority on authorship. |
| Costs | Every fee and every minimum-balance deposit is paid by the user whose action causes it. See §13 and invariant 22. |
| Voting | Upvotes only. No downvotes. |
| Vote weight | w = 1 + isqrt(rep) where rep is the voter’s reputation in the article’s primary field at vote time. |
| Reputation | Per (address, field). Earned mostly from reviews others found useful; smaller amounts from article upvotes; tiny amounts from comments. Integer-valued, deterministic. Daily cap per (address, field). Article-vote reputation is pull-based: votes accumulate on the article; each author materialises their share with claim_reputation (§10.2). Review/comment reputation is pushed at vote time. |
| Decay | Very slow, years. Not implemented in M1. updated_at stored on every reputation record so lazy decay can be added later. |
| Garbage filtering | Bad content simply gets no votes and sinks. Unacceptable content (plagiarism, spam, fabricated data) is handled by flags (§9), which are not votes. |
| Fields | Fixed two-level taxonomy (area → subfield) based on OECD Fields of Science. Governance-managed. Article has one primary field (100% rep) and optional secondary (50% rep). Both immutable after publish. |
| Status / type | Two separate on-chain fields. status is a controlled lifecycle (§7.3). type is a controlled enum (§6.3); extending it requires governance or a contract version. |
| Format | Markdown with LaTeX math. Mandatory front-matter header. Validated off-chain (web + indexer). |
| Comments | Informal comments/questions with nested replies and @mentions. Tiny reputation. Author can mark a comment as resolved for a real grant. |
| Profiles | Not stored. Derived by the indexer from on-chain history. |
| Identity | Wallet is the identity. An ORCID iD can be linked by a signed declaration (contract v3), verified by a link back from the public ORCID record; it never confers reputation. [EDIT D-106/D-107: reopened by the owner on 2026-10-01 (was “ORCID link (signed attestation) is v2”).] |
| Multi-author | Protocol-native co-authorship (§7.6): the submitting wallet invites, each co-author accepts by signing. Only accepted co-authors count. Editing rights stay with the submitter. Author count limited only by the counter width, 65 535 (pull model makes vote cost independent of it). Implemented in M2; the data model reserves it from M1. |
[EDIT D-072 (OWNER, 2026-09-29; the table text above is left as written): the row “Multi-author” now reads: the submitting wallet DECLARES the co-authors when it publishes; each co-author accepts by signing; only accepted co-authors count; editing rights stay with the submitter; at most 25 co-authors per article; nobody can be added after publication.] [EDIT D-023 (owner decision): the row “Article identity” is amended: article_id is a contract-assigned sequence and the ASA id is stored in the article box; the ASA is still created (ARC-71). See the changelog above and DECISIONS.md.]
3. Tech stack
Section titled “3. Tech stack”- Chain: Algorand, node software ≥ 5.0 (native post-quantum accounts, AVM v13). LocalNet for development and tests, TestNet for deployment. MainNet deployment is a separate, later decision (after audit) but every component is multi-network from the start (§3.2). The AlgoKit LocalNet image MUST be pinned to a 5.x release so that
pqsigtransactions are accepted in tests. [EDIT D-029: AlgoKit 2.10.2 hardcodes algorand/algod:latest and has no pin option; the pin is the repo-owned localnet/docker-compose.yml (algod 5.0.2-stable by digest, consensus V42).] - Contracts: AlgoKit + Algorand Python (Puya). ABI: ARC-4. Application spec: ARC-56 (generated by Puya; used for typed clients). Events: ARC-28. State: box storage via
BoxMap. - Contract testing:
algorand-python-testing(offline AVM emulation) +algokit-utils(Python) against LocalNet +simulatebefore every real send. - Indexer: TypeScript (Node).
@algorandfoundation/algokit-subscriberfor event ingestion with watermark persistence and catch-up. SQLite (Postgres later). REST/JSON API. - Frontend: Next.js + TypeScript + Tailwind. Wallet via
@txnlab/use-wallet. Typed contract client generated from the ARC-56 spec. Markdown:react-markdown+remark-math+rehype-katex, with sanitisation (§18). CID handling:multiformats(+ipfs-unixfs/@helia/unixfsfor directory CIDs). - Storage: IPFS via a pinning provider (Pinata; web3.storage/Storacha was decommissioned in 2026-05, a second provider is chosen in M5 [EDIT D-026]) behind a
StorageProviderinterface that pins directories. - Versions: pin every tool and library version at init (AlgoKit CLI, Puya, algokit-utils, subscriber, Next.js) and record them in
DECISIONS.md. Never float onlatest.
3.2 Multi-network model
Section titled “3.2 Multi-network model”The same contract code is deployed once per network; each deployment is an independent Ariadne (different app id, different articles, different reputation). Nothing on-chain references a network; the transaction’s genesis hash does that. IPFS content is network-agnostic: the same CID can be published on TestNet and MainNet.
A single manifest, /spec/networks.json, is the only place networks are described. Deployment scripts write it; indexer, web and CLI read it:
{ "mainnet": { "genesisId": "mainnet-v1.0", "genesisHash": "...", "appId": 0, "algod": "https://...", "indexerApi": "https://api.ariadne.../mainnet", "ipfsGateway": "https://...", "explorer": "https://.../mainnet", "enabled": false }, "testnet": { "genesisId": "testnet-v1.0", "genesisHash": "...", "appId": 123456, "algod": "https://...", "indexerApi": "https://api.ariadne.../testnet", "ipfsGateway": "https://...", "explorer": "https://.../testnet", "enabled": true }, "localnet": { "genesisId": "...", "appId": 1001, "algod": "http://localhost:4001", "indexerApi": "http://localhost:3000", "enabled": true, "devOnly": true }}Rules:
[EDIT D-084: every entry may also carry startRound (the round the application was created in, written by the deploy script: a fresh indexer starts there), algoIndexer (an Algorand indexer used only to catch up) and fetchGateway (a public trustless gateway used by the web app to verify a CID pasted by an author).]
- A network with
appId = 0orenabled = falseis not offered in any UI. MainNet stays that way until the audited deployment exists; enabling it is a manifest change, not a code change. devOnlynetworks are compiled out of production builds.- Every signed transaction MUST carry the genesis hash of the selected network, and the web app MUST verify that the connected wallet’s network matches before enabling any sign button (§16.1).
- Governance (§12.1) is per network: a separate PQ governance account for TestNet and for MainNet, never shared.
3.1 Repo layout (AlgoKit workspace)
Section titled “3.1 Repo layout (AlgoKit workspace)”/ AlgoKit workspace root DECISIONS.md /spec this file, networks.json, taxonomy.json, article-template.md, notes-template.md /projects /contracts AlgoKit Python (Puya) project — official template, structure untouched /indexer TS service: chain → events → projections → SQLite → API /web Next.js app4. Taxonomy
Section titled “4. Taxonomy”/spec/taxonomy.json — OECD Fields of Science: 6 areas, ~40 subfields. Each subfield has a stable field_id: uint16 (area in the high byte, subfield in the low byte, e.g. 0x0303 = Medical and health sciences / Health sciences [EDIT D-034: sport science is a third-level example inside 3.3; 2.10/2.11 encode as 0x020A/0x020B; names are at most 96 bytes (D-047)]). 0 is reserved for “none”.
- Fields live in contract state as an allow-list (box
t+ uint16 → name). - Only the governance address MAY add fields. Fields are never removed or renamed (append-only).
primary_fieldMUST be a registered field.secondary_fieldMUST be0or a registered field different fromprimary_field.
5. Constants (initial values, tune after M1 measurements)
Section titled “5. Constants (initial values, tune after M1 measurements)”| Constant | Value | Meaning |
|---|---|---|
GRANT_REVIEW_NUM/DEN |
3 / 1 | review-useful grant multiplier |
GRANT_COMMENT_NUM/DEN |
1 / 5 | comment upvote grant multiplier |
GRANT_RESOLVED |
5 | flat grant when a comment is resolved |
REP_CAP_DAY |
50 | max reputation received from all sources per (address, field, epoch day) |
MIN_FLAG_REP |
10 | min reputation in the article’s primary field to flag |
FLAG_THRESHOLD |
30 | cumulative flag weight in the current dispute round that triggers disputed |
EPOCH_DAY_SECONDS |
86400 | epoch_day = latest_timestamp // 86400 |
MAX_CONTENT_BYTES |
20 MB | article directory size limit (off-chain, web + indexer) |
MAX_COMMENT_DEPTH |
6 | max nesting depth of a reply (top-level comment = depth 0) |
MAX_MENTIONS |
5 | max distinct addresses mentioned per comment |
All arithmetic is uint64 integer arithmetic. There are no decimals anywhere in the protocol.
6. On-chain data model
Section titled “6. On-chain data model”6.1 Encoding rules
Section titled “6.1 Encoding rules”- All box keys are binary, ≤ 64 bytes. Addresses are the raw 32-byte public key, never the 58-char text form.
- Integers are big-endian fixed width as stated.
- CIDs are stored as raw CIDv1 bytes, fixed 36 bytes:
0x01(version) + codec +0x12 0x20(sha2-256, 32 bytes) + 32-byte digest.- Article content CID MUST have codec
0x70(dag-pb: a UnixFS directory). - Review and comment CIDs MUST have codec
0x55(raw: a single Markdown file). - The contract MUST reject any other version/codec/hash prefix, any length ≠ 36, and the all-zero digest.
// DECISION: sha2-256 only in M1; extend via contract version.
- Article content CID MUST have codec
6.2 Box keys
Section titled “6.2 Box keys”[EDIT D-023: every uint64(asa_id) below reads uint64(article_id), the contract-assigned sequence; the ASA id is stored inside the article box.]
| Box | Key | Bytes |
|---|---|---|
| field | b"t" + uint16(field_id) |
3 |
| article | b"a" + uint64(asa_id) |
9 |
| co-author | b"au" + uint64(asa_id) + addr32 |
42 |
| review | b"r" + uint64(asa_id) + uint64(review_seq) |
17 |
| reviewer index | b"ri" + uint64(asa_id) + addr32 → uint64(review_seq) |
42 |
| comment | b"c" + uint64(asa_id) + uint64(comment_seq) |
17 |
| vote (article) | b"va" + uint64(asa_id) + addr32(voter) |
42 |
| vote (review) | b"vr" + uint64(asa_id) + uint64(review_seq) + addr32(voter) |
50 |
| vote (comment) | b"vc" + uint64(asa_id) + uint64(comment_seq) + addr32(voter) |
50 |
| flag | b"f" + uint64(asa_id) + uint16(dispute_round) + addr32(flagger) |
43 |
| reputation | b"p" + addr32 + uint16(field_id) |
35 |
Every key length MUST be asserted in tests.
6.3 Enums
Section titled “6.3 Enums”type: 1 article | 2 notes | 3 amendment | 4 dataset | 5 replicationstatus: 1 preprint | 2 under_review | 3 final | 4 disputed | 5 retractedrecommendation: 1 accept | 2 minor_revision | 3 major_revision | 4 rejectflag_reason: 1 plagiarism | 2 spam | 3 fabricated_data | 4 othervote_target: 1 article | 2 review | 3 commentdispute_outcome: 1 cleared | 2 retractedrep_reason: 1 article_upvote | 2 review_useful | 3 comment_upvote | 4 comment_resolved0 is invalid for every enum.
6.4 Box values
Section titled “6.4 Box values”article
author addr32 immutable (submitting wallet)primary_field uint16 immutablesecondary_field uint16 immutable (0 = none)type uint8 immutableparent_article uint64 immutable (required ≠ 0 iff type == amendment)status uint8status_before_dispute uint8 (0 unless status == disputed)version uint16 starts at 1current_cid bytes36prev_cid bytes36 (all-zero for version 1)created_at uint64updated_at uint64vote_total uint64 sum of weights, monotonicvote_count uint64 monotonicreview_seq_next uint64 starts at 1comment_seq_next uint64 starts at 1dispute_round uint16 starts at 1round_flag_weight uint64 cumulative flag weight in current roundauthor_count uint16 1 + accepted co-authorsinvited_count uint16 co-authors declared in publish (accepted or not), immutable <span class="spec-edit">[EDIT D-072]</span>claimed boolasa_id uint64 immutable (the ARC-71 ASA created by publish) <span class="spec-edit">[EDIT D-023]</span>[EDIT D-072: value = 192 bytes. The three accumulators added by the first implementation of D-054 are gone: with a fixed author list the shares are computed from vote_total at claim time.]
[EDIT D-060: updated_at is stamped by every call that changes an article field; vote_review, vote_comment and resolve_comment do not write the article box.]
co-author (one per accepted or invited author, including the submitter, whose box is created by publish with accepted = true)
invited_at uint64accepted boolaccepted_at uint64claimed_total uint64 entitlement already converted into reputation by this author; starts at 0 for everybody <span class="spec-edit">[EDIT D-072]</span>review
reviewer addr32cid bytes36recommendation uint8created_at uint64useful_total uint64 weighted "useful" votes, monotoniccomment
author addr32cid bytes36reply_to uint64 parent comment_seq (0 = top level)root_seq uint64 comment_seq of the thread root (== own seq for top level)depth uint8 0 for top level, parent.depth + 1 otherwisecreated_at uint64vote_total uint64reply_count uint64 direct replies, monotonicresolved boolresolved_at uint64Mentions are not stored in the comment box (no MBR for them); they live only in the Commented event and are projected by the indexer.
vote (all three kinds)
weight uint64 weight applied at vote time, immutablegranted uint64 reputation pushed at vote time (review/comment votes); always 0 for article votes (pull, §10.2)created_at uint64flag
reason uint8weight uint64created_at uint64reputation
rep uint64 never decreases in M1updated_at uint64cap_day uint32 epoch day of the last grantcap_today uint16 reputation received on cap_day (all sources)Full version history is not stored in boxes; it is reconstructed from Versioned events (§11). The prev_cid link is a convenience for light clients.
7. Article lifecycle
Section titled “7. Article lifecycle”7.1 ASA issuance and claim
Section titled “7.1 ASA issuance and claim”Because an ASA recipient MUST opt in before receiving the asset, and the asset id does not exist until the contract creates it, publishing and claiming are separate actions.
The article ASA is an ARC-71 Non-Transferable ASA (NTA). The application account is the ARC-71 Issuer (ARC-71 requires the Issuer to be a smart-contract account). The three ARC-71 states map onto the article lifecycle:
| ARC-71 state | Ariadne | ASA parameters |
|---|---|---|
| Issued | published, unclaimed | clawback = ZeroAddress, freeze = app, manager = app, reserve = app (or ARC-19 metadata, see below), held by the app |
| Held | claimed by the author | clawback = ZeroAddress, freeze = ZeroAddress, holder’s holding frozen, manager = app |
| Revoked | retracted |
manager = ZeroAddress (set by the contract on the transition to retracted, whether by author or governance) |
Creation parameters:
total = 1, decimals = 0default_frozen = trueclawback = ZeroAddress # never set; ARC-71 MUSTfreeze = application address # Issuedmanager = application addressreserve = digest of the current CID, as an address # EDIT D-024: ARC-19 from M1 (the url is immutable, so the template must exist at creation)unit_name = "ARIA"asset_name = "Ariadne Article"url = "template-ipfs://{ipfscid:1:dag-pb:reserve:sha2-256}" # EDIT D-024: ARC-19 template (51 bytes); reserve carries the digest and new_version moves itBecause default_frozen does not apply to the creator’s own holding, the application can transfer the asset without unfreezing itself.
publish()creates the ASA (inner asset-config), creates the article box, creates the sender’s co-author box withaccepted = true, claimed_total = 0, records the sender as immutable author, and leaves the ASA in the application account (ARC-71 Issued). It does not create reputation boxes (§10.5).claim_article()is optional. Preconditions: sender == author, sender opted in (paying their own opt-in MBR),claimed == false. The contract, in one atomic sequence of inner transactions: (1) unfreezes the author’s holding, (2) transfers the asset, (3) refreezes the author’s holding, (4) setsfreeze = ZeroAddressvia asset-config (ARC-71 Held, irreversible).claimed = true. [EDIT D-005/D-007: step (4) passes manager = app, reserve = digest(current_cid), freeze = Zero and clawback = Zero explicitly (an omitted address is zeroed forever); claim_article MUST fail once the article is retracted and is allowed in every other status.]- On every transition to
retractedthe contract setsmanager = ZeroAddress(ARC-71 Revoked); the asset parameters become permanently immutable, which is consistent withretractedbeing terminal. [EDIT D-033: the same inner AssetConfig passes reserve = digest(current_cid), freeze = ZeroAddress (unconditionally) and clawback = ZeroAddress.] - Algorand lets any holder close out a frozen ASA back to its creator; ARC-71 explicitly preserves that right. If an author does so, the asset returns to the application account and
claimedstaystrue; authorship is unaffected because it lives in the article box. Invariant 19 still holds. - After Held, no address can ever move the asset: clawback and freeze are both zero and the holding is frozen. Tests MUST attempt transfer, clawback and unfreeze from every role and expect failure.
7.2 Versions
Section titled “7.2 Versions”new_version()bumpsversionby exactly 1, setsprev_cid = current_cid,current_cid = new cid,updated_at. [EDIT D-024: it also moves the ASA reserve to the new digest through one inner AssetConfig (manager = app, freeze = Zero if claimed else app, clawback = Zero), so the fee pools one inner transaction.]- Allowed only while
status ∈ {preprint, under_review}. - The new CID MUST differ from
current_cid. - Version numbers are strictly increasing; history is append-only (events).
7.3 Status machine
Section titled “7.3 Status machine”| From | To | Actor | Via |
|---|---|---|---|
| preprint | under_review | author | set_status |
| under_review | final | author | set_status |
| preprint | retracted | author | set_status |
| under_review | retracted | author | set_status |
| final | retracted | author | set_status |
| preprint / under_review / final | disputed | contract | flag reaching FLAG_THRESHOLD |
| disputed | status_before_dispute | governance | resolve_dispute(cleared) |
| disputed | retracted | governance | resolve_dispute(retracted) |
All transitions not in this table MUST fail. preprint → final directly is not allowed. retracted is terminal. “author” here means the submitting wallet only. // DECISION: "cleared" restores the pre-dispute status instead of forcing final.
7.4 Permitted actions per status
Section titled “7.4 Permitted actions per status”| Status | new_version | invite/accept co-author | review | vote_article | claim_reputation | comment | flag |
|---|---|---|---|---|---|---|---|
| preprint | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| under_review | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| final | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
| disputed | ✗ | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ (recorded, no state change) |
| retracted | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ |
Votes on reviews and comments follow the article’s vote_article column. [EDIT D-005: claim_article is allowed in preprint, under_review, final and disputed and MUST fail in retracted; set_status follows §7.3.]
[EDIT D-072: the third column is now “accept co-authorship” (there are no invitations after publication) and it is ✓ in final too: the list is fixed at publication, so a late confirmation changes nothing for the others. It stays ✗ in disputed and retracted.]
[EDIT D-020 / D-055 / D-058: resolve_comment follows the vote_article column too; resolve_dispute is allowed only in disputed; in the disputed row “recorded, no state change” means no STATUS change: the flag box is written, round_flag_weight keeps accumulating and updated_at is stamped.]
7.5 Amendments
Section titled “7.5 Amendments”type == amendment requires parent_article ≠ 0, and the parent MUST exist. The parent MAY be in any status. An amendment’s fields SHOULD match the parent’s (enforced by the web app, not the contract).
7.6 Co-authorship (M2)
Section titled “7.6 Co-authorship (M2)”[EDIT D-072 (OWNER): the first two bullets of v2.7 (kept further down for the record) are replaced by these two rules.]
- Declared at publication.
publish(..., coauthors: address[])takes the complete list of co-authors: 0 to 25 addresses, strictly ascending by raw address (hence distinct), never the submitter.publishcreates one co-author box per declared address withaccepted = false(the submitter pays the MBR) and stores their number ininvited_count. The list never changes: nobody can be added, removed or replaced later. - Confirmed by signature. A declared address MAY
accept_coauthorship(article_id)once, while status ∈ {preprint, under_review, final}: setsaccepted = true,accepted_at, incrementsauthor_count;claimed_totalstays 0, because the share counts from publication. It MUST fail if that address already voted on the article or reviewed it. A declaration that is never confirmed is inert for that address (it may vote, review and flag like anybody) and its share is never claimed by anyone.
Original text (v2.7), no longer in force:
- The submitting wallet MAY
invite_coauthor(article_id, addr)while status ∈ {preprint, under_review};addr ≠ author; no existing box foraddr. Creates the co-author box withaccepted = false(submitter pays MBR). The only limit is theuint16counters: at most 65 535 invitations per article. - The invitee MAY
accept_coauthorship(article_id): setsaccepted = true,accepted_at,claimed_total = article.vote_total(only votes cast after acceptance ever count for this author), and incrementsauthor_count. One-time, irreversible. Invitations are never withdrawn or declined on-chain (append-only); an unaccepted invitation is simply inert. No reputation box is created here (§10.5).
In force:
- “Accepted author” = submitter or any co-author with
accepted = true. Accepted authors receive article-upvote reputation (§10.2) and are subject to every “not own article” rule (§8, §9). They have no editing, status or invitation rights. - On-chain authorship is canonical. Front-matter
authors[]is informative and frozen per version: it reflects the author set the submitter intended when that version was published, and it is never required to match later acceptances. The indexer displays the on-chain accepted-author set as the article’s authors and shows the front-matter list as “as stated in version N”; a mismatch is a notice, not a validation failure. Accepting a co-author never requires a new version.// DECISION: option 1 (canonical on-chain) over forcing a new version; published content never changes.
8. Reviews, votes, comments
Section titled “8. Reviews, votes, comments”8.0 Self-action rule (applies to §8 and §9)
Section titled “8.0 Self-action rule (applies to §8 and §9)”Every “not own article” / “not own comment” check is evaluated at the time the action is performed, against the accepted-author set as it exists in that round. Later changes to the accepted-author set (a commenter, voter, reviewer or flagger who is invited and accepts co-authorship afterwards) do not invalidate earlier actions; they remain on record and keep their effects. Under the pull model this is safe: a co-author’s claimed_total starts at the vote_total current at acceptance, which already includes any vote they cast before, so nobody ever claims reputation from their own vote. [EDIT D-072: the safety argument is now simpler: an address that voted on the article or reviewed it can never become an accepted author, so nobody claims from their own vote and no reviewer becomes an author of what they reviewed. Flags and comments made before confirming stay on record.]
8.1 Reviews
Section titled “8.1 Reviews”- One review per (article, reviewer). Enforced via the reviewer index box.
reviewerMUST NOT be an accepted author of the article (at submission time, §8.0).- Reviews are immutable once submitted. A reviewer who changes their mind comments.
8.2 Votes
Section titled “8.2 Votes”- One vote per (target, voter). Vote boxes are append-only and immutable.
- Article votes:
voterMUST NOT be an accepted author. An article vote writes only the vote box andarticle.vote_total / vote_count; it never touches any reputation box (pull model, §10.2), so its cost is constant regardless of author count. - Review votes:
voter ≠ reviewer. An accepted author of the article MAY vote a review “useful”, but that vote grants 0 reputation (recorded with its weight,granted = 0).// Rationale: the simplest review cartel is author↔reviewer; the author's judgement of a review of their own work is conflicted. - Comment votes:
voter ≠ comment.author. - Weight is computed once, at vote time, from the voter’s reputation in the article’s primary field, and stored in the vote box.
8.3 Comments
Section titled “8.3 Comments”- Anyone MAY comment, including accepted authors.
- Nested replies.
reply_toMUST be0(top-level) or an existingcomment_seqof the same article, at any depth. The contract setsdepth = parent.depth + 1,root_seq = parent.root_seq, incrementsparent.reply_count, and MUST fail ifdepth > MAX_COMMENT_DEPTH. A reply MAY target a resolved comment. Comments are allowed in every status (§7.4), includingretracted. - Mentions.
comment()takesmentions: address[](0–MAX_MENTIONSentries). Entries MUST be distinct and MUST NOT include the sender. [EDIT D-061: the list MUST be strictly ascending by the raw 32-byte address (big-endian unsigned); strict order is how distinctness is enforced in one pass. Clients sort before sending; the order carries no meaning.] Mentions carry no reputation effect and no on-chain obligation; they exist so the indexer can build a mentions feed without parsing IPFS content. The comment body SHOULD render each mention as@[label](algo:ADDRESS)(web app inserts and resolves it); the on-chain list, not the body, is authoritative. - Replies and mentions are the only notification primitives in the protocol; both are derived by the indexer from events (§15).
resolve_comment(): sender MUST be the submitting author; comment MUST exist;comment.authorMUST NOT be an accepted author;resolvedMUST be false. Resolution is one-time and irreversible. Any depth may be resolved.
9. Flags and disputes
Section titled “9. Flags and disputes”flag()requires the flagger’s reputation in the article’s primary field ≥MIN_FLAG_REP, flagger not an accepted author, and no existing flag by this flagger in the current dispute round.flag_weight = 1 + isqrt(flagger_rep_in_primary_field)at flag time, stored in the flag box.- The contract adds
flag_weighttoround_flag_weight. Whenround_flag_weight ≥ FLAG_THRESHOLDand status ∈ {preprint, under_review, final}, status becomesdisputedandstatus_before_disputeis set. - Flags are immutable and append-only. Each flag counts exactly once.
resolve_dispute()(governance only) sets the outcome, incrementsdispute_round, resetsround_flag_weightto 0, and clearsstatus_before_dispute. Flag history from earlier rounds is preserved (boxes and events) but does not count toward later rounds.FLAG_THRESHOLDis absolute in M1.// DECISION: revisit after observing real activity.[EDIT D-058 / D-065: the flagger needs an existing reputation box with rep >= MIN_FLAG_REP; round_flag_weight accumulates also while disputed; the transition emits no StatusChanged (Flagged carries new_status); resolve_dispute(retracted) revokes the ASA like an author retraction; dispute_round 65 535 takes no flags and cannot be resolved, so no article can be stuck in disputed.]
10. Reputation
Section titled “10. Reputation”10.1 Weight
Section titled “10.1 Weight”w(rep) = 1 + isqrt(rep) # isqrt = AVM `sqrt` opcode (integer)rep 0 → 1, rep 100 → 11, rep 10 000 → 101. A missing reputation box reads as rep = 0; reading never creates a box.
10.2 Grants (integer arithmetic, floor division)
Section titled “10.2 Grants (integer arithmetic, floor division)”| Event | Recipient(s) | Field(s) | Raw grant |
|---|---|---|---|
| article upvote | (deferred) | — | nothing at vote time; article.vote_total += w. Materialised later by each accepted author via claim_reputation (below) |
| review-useful upvote | review.reviewer | article primary | w * GRANT_REVIEW_NUM / GRANT_REVIEW_DEN; 0 if voter is an accepted author |
| comment upvote | comment.author | article primary | w * GRANT_COMMENT_NUM / GRANT_COMMENT_DEN |
| comment resolved | comment.author | article primary | GRANT_RESOLVED |
w is the voter’s weight in the article’s primary field at the moment of the vote.
Daily cap (all sources): for the recipient’s reputation box, if cap_day ≠ epoch_day then cap_day = epoch_day, cap_today = 0. granted = min(raw, REP_CAP_DAY − cap_today); then rep += granted, cap_today += granted. A grant of 0 still records the vote and emits Voted, but emits no ReputationChanged.
Pull: claim_reputation(article_id). Sender MUST hold an accepted co-author box for the article (the submitter always does). Allowed statuses per §7.4.
N = 1 + article.invited_count # EDIT D-072: fixed at publicationentitled = article.vote_total / N # floor; the submitter takes the ceilingclaimable = entitled − au.claimed_total # MUST be > 0, else fail (was article.vote_total − au.claimed_total)granted_primary = min(claimable, cap_remaining(sender, primary_field))granted_secondary = min(granted_primary / 2, cap_remaining(sender, secondary_field)) # only if secondary ≠ 0au.claimed_total += granted_primaryVote weight and article reputation are 1:1 by definition (there is no multiplier constant for article votes). Whatever the daily cap does not allow stays claimable for a later day, because claimed_total advances only by granted_primary. The secondary-field shortfall is not carried over. // DECISION: simplicity over exactness for the secondary field.
Every accepted author claims independently, on their own schedule, paying their own fee. An author who never claims simply never materialises that reputation; the indexer shows the claimable amount on their profile.
M2 gate. With more than one accepted author, the same votes can be claimed once per author (N× article-vote reputation for N authors). M2 MUST fix a deterministic allocation rule before implementing multi-author claims and record it in DECISIONS.md. Candidates: (a) full vote_total per author, inflation bounded only by REP_CAP_DAY (current text; rationale: reputation belongs to people, not to papers); (b) equal split with the submitter taking the remainder; (c) submitter full, co-authors half. M1 is single-author and unaffected.
[EDIT D-054 (OWNER, 2026-09-29): candidate (b) is the rule. EDIT D-072: implemented over a fixed author list.]
Allocation rule (D-054, D-072). The author list is fixed at publication, so the number of shares is a constant of the article: N = 1 + invited_count (the submitter plus every declared co-author, confirmed or not). With T = vote_total:
accepted co-author: entitled = T / N # floorsubmitter: entitled = (T + N − 1) / N # ceiling: the remainder, as far as it never decreasesBoth never decrease and together they never exceed T (they fall short by at most N − 1 weight units, which wait for the next votes). “Floor plus the whole remainder” is not used because it decreases when the total grows (N = 3: T = 5 gives 3, T = 6 gives 2). The share of a declared co-author who never confirms is never claimed by anyone. A single-author article behaves exactly as in M1.
10.3 Reputation invariants
Section titled “10.3 Reputation invariants”- No self-voting, no self-review, no self-resolution, no self-flagging, measured against the accepted-author set at action time (§8.0).
- No actor ever receives reputation from their own action.
- Grants are integer-valued and deterministic from on-chain state.
- Grants use the voter’s weight at vote time, never a later value.
- Grants are bounded per target (one vote per target) and per (address, field, epoch day).
- Reputation never decreases in M1.
- Every non-zero grant emits exactly one
ReputationChangedper (address, field). - Article-vote reputation is only ever materialised by the recipient’s own
claim_reputation;au.claimed_total ≤ article.vote_totalalways. [EDIT D-072: more precisely au.claimed_total ≤ entitlement ≤ article.vote_total, and the entitlements of all authors never add up to more than vote_total.] - A co-author accepted after votes were cast can never claim those earlier votes. [EDIT D-072 (OWNER): replaced by: the author list is fixed at publication; every accepted author is entitled to an equal share of all votes; an address that voted on or reviewed the article can never become an accepted author.]
10.4 Known limitation
Section titled “10.4 Known limitation”M1/M2 are not Sybil-resistant. Reciprocal voting between fresh accounts grows reputation slowly (weight 1 each, damped by isqrt, bounded by REP_CAP_DAY) but is not prevented. The author↔reviewer loop is closed (§8.2); the reviewer↔reviewer loop across two articles is not. Mitigations (stake, attestations, field-entry requirements) are post-M2 and MUST NOT be improvised earlier.
10.5 Reputation box funding
Section titled “10.5 Reputation box funding”A reputation box for (address, field) is created only by an action of that address, and paid by that address:
claim_reputation→ claimer’s boxes for the article’s primary (and secondary) field, created on first claim.submit_review→ reviewer’s box for the article’s primary field (reviews are pushed at vote time, so the box must pre-exist).comment→ commenter’s box for the article’s primary field (same reason).publishandaccept_coauthorshipcreate no reputation boxes.
Consequently a vote never creates a box for anyone else, and a voter needs no box of their own. If a grant targets an address whose box does not exist (impossible by construction, but asserted), the call MUST fail rather than create it.
11. Events (ARC-28)
Section titled “11. Events (ARC-28)”Every state-changing method emits exactly one canonical event plus zero or more ReputationChanged. Every event’s first field is schema_version: uint8 (currently 1). Payloads are compact: per application call, logs are limited to 32 entries and 1 024 bytes total. CIDs in events are the 36-byte raw form.
FieldAdded(v, field_id: uint16, name: string)GovernanceChanged(v, previous_governance: address, new_governance: address, ts: uint64)Published(v, article_id: uint64, asa_id: uint64, author: address, primary_field: uint16, secondary_field: uint16, type: uint8, parent_article: uint64, cid: bytes36, coauthors: address[], ts: uint64) # EDIT D-023: asa_id added; EDIT D-072: coauthors added (114 bytes + 32 per co-author)ArticleClaimed(v, article_id, author, ts)Versioned(v, article_id, version: uint16, cid: bytes36, prev_cid: bytes36, ts)StatusChanged(v, article_id, from: uint8, to: uint8, actor: address, ts)CoauthorInvited(v, article_id, coauthor: address, ts) # EDIT D-072: removedCoauthorAccepted(v, article_id, coauthor: address, author_count: uint16, claimed_total: uint64, ts)ReputationClaimed(v, article_id, author: address, consumed: uint64, ts) # followed by 1–2 ReputationChangedReviewed(v, article_id, review_seq: uint64, reviewer: address, recommendation: uint8, cid: bytes36, ts)Voted(v, target_kind: uint8, article_id, target_seq: uint64, voter: address, weight: uint64, ts)Commented(v, article_id, comment_seq: uint64, author: address, reply_to: uint64, root_seq: uint64, depth: uint8, cid: bytes36, mentions: address[], ts)CommentResolved(v, article_id, comment_seq, resolved_by: address, ts)Flagged(v, article_id, dispute_round: uint16, flagger: address, reason: uint8, weight: uint64, round_total: uint64, ts)DisputeResolved(v, article_id, dispute_round: uint16, outcome: uint8, new_status: uint8, ts)ReputationChanged(v, address, field_id: uint16, delta: uint64, new_total: uint64, reason: uint8, ts)# contract v3 declarations (EDIT D-106): one event per call, no boxIdentityDeclared(v, address, scheme: uint8, value: string, ts) # scheme 1 = ORCID iD; "" withdrawsDoiDeclared(v, article_id, version: uint16, doi: string, declared_by: address, ts) # version 0 = every version; "" withdrawsFollowed(v, follower: address, target: address, on: bool, ts)[EDIT D-017: bytes36 is the ARC-4 type byte[36]; selectors derive from the type-only signature.]
[EDIT D-020 / D-058: Flagged is (v, article_id, dispute_round, flagger, reason, weight, round_total, new_status: uint8, ts) = 73 bytes; new_status is the status after the call, so the transition to disputed needs no StatusChanged. DisputeResolved.dispute_round is the round that was resolved.]
[EDIT D-072: CoauthorAccepted.claimed_total is kept for compatibility and is always 0; the declared co-authors are in Published.coauthors, in ascending address order.]
[EDIT D-057: review / comment votes and resolve_comment emit the canonical event first, then at most one ReputationChanged.]
[EDIT D-061: Commented.mentions is in ascending address order (not the order of the comment body); with 5 mentions the log is exactly 278 bytes.]
target_seq is 0 for article votes. Commented with 5 mentions is ≈ 270 bytes, within budget. Events MUST contain enough information to rebuild every indexer projection without reading boxes. vote_article emits exactly one Voted and never a ReputationChanged.
12. ABI methods
Section titled “12. ABI methods”Every method: sender is the actor; validates status per §7.4; emits its event; fails on any violated precondition. Any method that creates a box or an asset MUST be grouped with a payment from the sender to the application account covering the exact MBR delta (§13); the contract MUST verify the payment and MUST fail on underpayment. Overpayment is accepted and kept (no refunds in M1; log as open question).
[EDIT D-001/D-003/D-023: create(governance: address) exists (creation only, no bare create, no update/delete paths); publish, vote_article, claim_reputation and add_field (in M2 also submit_review, vote_review, comment, vote_comment, flag) take a leading pay: PaymentTransaction argument whose amount must cover the application’s min-balance increase; article_id is the contract sequence.]
[EDIT D-072 / D-073: publish takes a last argument coauthors: address[]; invite_coauthor is removed; extend() changes nothing and emits nothing: a client adds ceil(boxes / 8) − 1 calls to it in the publish group, where boxes = 3 + (1 if secondary) + (1 if parent) + co-authors.]
[EDIT D-055 / D-061: accept_coauthorship, resolve_comment and resolve_dispute create nothing and take no payment; comment(..., mentions) expects the addresses in strictly ascending order.]
# governance (sender == governance address)add_field(field_id: uint16, name: string)resolve_dispute(article_id: uint64, outcome: uint8)set_governance(new_governance: address) # migrate governance (e.g. to PQ multisig)
# publishingpublish(cid: bytes36, primary_field: uint16, secondary_field: uint16, type: uint8, parent_article: uint64) -> uint64 # returns article_id (sequence; the asa_id is in the box and in Published) EDIT D-023claim_article(article_id: uint64) # sender == author, opted innew_version(article_id: uint64, cid: bytes36) # sender == authorset_status(article_id: uint64, new_status: uint8) # sender == author, table §7.3
# co-authorship (M2)invite_coauthor(article_id: uint64, coauthor: address) # sender == authoraccept_coauthorship(article_id: uint64) # sender == invited address
# reviewssubmit_review(article_id: uint64, cid: bytes36, recommendation: uint8) -> uint64 # review_seqvote_review(article_id: uint64, review_seq: uint64)
# votesvote_article(article_id: uint64)claim_reputation(article_id: uint64) # sender == accepted author
# commentscomment(article_id: uint64, cid: bytes36, reply_to: uint64, mentions: address[]) -> uint64 # comment_seqvote_comment(article_id: uint64, comment_seq: uint64)resolve_comment(article_id: uint64, comment_seq: uint64) # sender == author
# flagsflag(article_id: uint64, reason: uint8)
# declarations (contract v3, EDIT D-106): emit their event only; no box, no payment, network fee onlydeclare_identity(scheme: uint8, value: string) # scheme >= 1, value <= 64 bytes (D-107)declare_doi(article_id: uint64, version: uint16, doi: string) # sender == accepted author; version <= current; a non-empty # DOI starts with "10.", <= 200 bytes, article not retracted (D-108)follow(target: address, on: bool) # target != zero address, != sender (D-109)12.1 Governance account
Section titled “12.1 Governance account”Governance is a single address stored in global state, changeable only by a call signed by the current governance address (set_governance(new: address), emits GovernanceChanged), so it can migrate without redeploying the contract and the indexer can rebuild the governance history from events alone. Governance pays its own fees and MBR (field boxes) like any other actor.
The governance account is post-quantum from day one. At launch it is a native Falcon-1024 account (Algorand 5.0 pqsig), generated and backed up with algokey pq. The contract does not care how a transaction was signed (it only sees Txn.sender), so this is a deployment choice, not a contract feature; but the following are protocol requirements:
- The governance address MUST be a native PQ account (hash-derived, not an Ed25519 public key) on TestNet and MainNet, and MUST be a different account on each network (§3.2). A test asserts that the configured governance address is PQ-compliant:
algokey pq check-address ADDRESSexits 0 and prints “is PQ compliant”; in code,not is_ed25519_point(decode_address(addr)). [EDIT D-012: original sentence was inverted] - The application account needs no key at all: it is controlled exclusively by the contract’s inner transactions. Nothing to migrate.
- The deployer account MUST hold no power after deployment: the application is created with no update and no delete program paths (immutable), and every privileged method checks the governance address, never the creator.
- Governance transactions carry a Falcon-1024 signature (public key 1 793 B + signature ≈ 1.3 KB). Consensus v42 prices a pqsig-authorised transaction as a flat +2 minimum fees (usage 3 000 000), not per byte; the per-byte surcharge only applies to oversize notes, arguments and programs. The deployment scripts size the fee with the usage formula and confirm it with
simulate(group-usage), which accepts a scheme-only placeholder pqsig before signing. [EDIT D-027/D-045] - Multisig: native post-quantum multisig is on Algorand’s roadmap but not yet in the protocol. Launch governance is therefore a single PQ signer (the operator), documented as such. When native PQ multisig ships, governance migrates to an M-of-N PQ account via
set_governance.// DECISION: prefer waiting for native PQ multisig over a third-party contract-based Falcon vault; revisit if launch on MainNet comes first. - Authors, reviewers and voters MAY use PQ accounts too (e.g. a Pera Quantum account); the contract treats them like any other 32-byte address, and box keys are unaffected.
13. Costs
Section titled “13. Costs”13.1 Principle
Section titled “13.1 Principle”The platform pays nothing. Every network fee and every minimum-balance deposit caused by an action is paid by the account performing it. Concretely:
- Transaction fees, including inner-transaction fees via fee pooling, are paid by the sender of the outer call. Fee amounts are read from the network at send time (§13.2), never assumed.
- Every MBR increase on the application account (ASA creation, any box) is covered by a payment from the sender, grouped with the call and verified by the contract.
- MBR increases on the user’s own account (ASA opt-in) are naturally the user’s.
- The application account is funded once with its base minimum balance at deployment (operator cost, one-off). After that its spendable balance MUST never decrease as a result of any user action (invariant 22, tested).
User-facing wording: “No platform fees for authors or readers. You pay Algorand network fees (fractions of a cent) and a storage deposit, held by the protocol, for the data you publish.”
13.2 Components (µAlgo)
Section titled “13.2 Components (µAlgo)”- Network fee: determined by the network, never hard-coded. Clients MUST take
minFee/ suggested parameters from algod at send time. Inner transactions each cost the network minimum and MUST be covered by the caller through fee pooling. Post-quantum (pqsig) transactions pay a flat +2 minimum fees (usage model of consensus v42); the per-byte surcharge only applies to oversize notes, arguments and programs. The client sizes the fee asceil(min_fee × usage / 1e6)and confirms it withsimulate. [EDIT D-027] - ASA creation: +100 000 MBR on the application account.
- ASA opt-in (claim): +100 000 MBR on the author’s account.
- Box MBR:
2 500 + 400 × (key_len + value_len)per box, on the application account.
Because nothing is deleted, MBR is a permanent deposit, not a fee.
13.3 What M1/M2 MUST measure
Section titled “13.3 What M1/M2 MUST measure”| Operation | Payer | Components |
|---|---|---|
| publish | author | call fee + inner fee + ASA MBR + article box + submitter co-author box |
| claim | author | opt-in MBR + call fee + inner fee |
| vote_article | voter | vote box + fee (never a reputation box, never author-count dependent) |
| claim_reputation | author | 1–2 reputation boxes (if first in field) + fee |
| submit_review | reviewer | review box + reviewer index box + reputation box (if first) + fee |
| vote_review | voter | vote box + fee |
| comment | commenter | comment box + reputation box (if first) + fee |
| vote_comment | voter | vote box + fee |
| invite_coauthor | author | co-author box + fee |
| accept_coauthorship | co-author | fee only |
| flag | flagger | flag box + fee |
| add_field | governance | field box + fee |
[EDIT D-072 / D-056 (M2, measured in M2_REPORT.md): article box (9 + 192 bytes) = 82 900; publish deposit = 212 200 + 29 300 per declared co-author (944 700 µAlgo with 25); review box (17 + 85) = 43 300; reviewer index (42 + 8) = 22 500; comment box (17 + 118) = 56 500; flag box (43 + 17) = 26 500; first review = 91 100, first comment = 81 800 (they include the actor’s own reputation box).]
MBR figures (fees excluded, they are measured): article box (9 + 192 bytes) = 82 900; co-author box (42 + 25 bytes) = 29 300; reputation box (35 + 22 bytes) = 25 300; vote box (42 or 50 + 24 bytes) = 28 900 or 32 100. Publish total = 212 200 µAlgo deposit; new_version pools one inner transaction (ARC-19 reserve update); set_status to retracted pools one. [EDIT D-013/D-023/D-031: ARC-4 static tuple sizes, reconciled in M1_REPORT.md] Measure, don’t assume. M1_REPORT.md reports the actual fee paid per operation on LocalNet, separately for Ed25519 and pqsig senders.
14. IPFS content
Section titled “14. IPFS content”14.1 Article directory
Section titled “14.1 Article directory”An article’s CID is a UnixFS directory (dag-pb) with this layout:
index.md required — Markdown with front-matterassets/ optional — images, figures, small data files referenced from index.mdindex.mdMUST reference assets by relative path (assets/fig1.png), never by external URL. The web app rewrites relative paths to the gateway URL at render time.- Total directory size ≤
MAX_CONTENT_BYTES. Larger material (raw datasets) is published as a separatetype = datasetarticle and referenced by CID. - Reviews and comments are single raw Markdown files (codec
0x55).
14.2 Front-matter and templates
Section titled “14.2 Front-matter and templates”/spec/article-template.md:
---title: ""authors: ["ADDRESS_1", "ADDRESS_2"] # informative, frozen per version; first MUST be the submitter (§7.6)field: 0x0303 # MUST equal on-chain primary_fieldsecondary_field: 0x0501 # MUST equal on-chain secondary_field (omit if 0)type: article # MUST equal on-chain typeparent: 0 # MUST equal on-chain parent_articleabstract: ""keywords: []references: [] # list of {cid | doi | url}license: CC-BY-4.0language: en---
## Introduction...notes and amendment use /spec/notes-template.md (title, authors, field, type, parent, abstract, body).
14.3 Validation model
Section titled “14.3 Validation model”- On-chain metadata is canonical. The contract stores the CID and validates only its shape (§6.1).
- Web app validates front-matter and directory layout before upload, computes the directory CID locally with the canonical parameters (§14.5) before signing, and compares with the pinning provider’s returned CID (Path A) or with the gateway-fetched content (Path B).
- Indexer fetches the directory, parses
index.mdfront-matter, and setsvalidation_status ∈ {valid, mismatch, malformed, unavailable, oversized}by comparing against on-chain metadata (field, secondary field, type, parent, andauthors[0] == author). The rest ofauthors[]is compared only to produce a notice (§7.6), never amismatch. Non-valid content is displayed with a warning, never hidden; the article remains protocol-valid. - Content availability ≠ protocol validity. An article whose content is temporarily unreachable is still valid on-chain.
14.4 How a CID gets published
Section titled “14.4 How a CID gets published”The contract only ever sees a 36-byte CID. Getting the content onto IPFS is the author’s job, with the web app as the default helper. Three paths, all ending in the same publish() call:
Path A — web uploader (default, M4). The author drops index.md and assets/ into /publish. The browser builds the UnixFS DAG locally with the canonical parameters (§14.5), shows the resulting CID, then uploads the CAR to a pinning provider. The provider’s returned CID MUST equal the locally computed one, or the app refuses to continue. The author then signs the grouped payment + publish().
Path B — bring your own CID. The author pins the directory themselves (own Kubo node, Pinata, Filebase, anything [EDIT D-026: web3.storage no longer exists]) and pastes the CID into /publish. Before enabling the sign button the app MUST: fetch the CID through a gateway, check it resolves to a directory containing index.md, parse and validate the front-matter against the fields chosen in the form, and check total size ≤ MAX_CONTENT_BYTES. Exact commands for this path are in /spec/publishing-guide.md.
Path C — CLI (M5). ariadne publish ./my-article --wallet ... does A end to end from a terminal, for programmatic or batch publishing.
Pinning responsibility.
- The author is responsible for at least one pin. In Path A the app pins with the author’s own provider credentials (stored in the browser, never sent to Ariadne servers) or with the operator’s provider under a per-address quota.
// DECISION: quota size and whether it exists at all is an operator choice, not protocol. - The indexer pins every CID it validates, as a second copy (operator cost, ordinary infrastructure, not a user fee). [EDIT D-084: through the indexer’s own Kubo node (RPC never exposed), for article CIDs validated as
validand for every review and comment CID; the public gateway of that node serves only what it holds (Gateway.NoFetch), on its own origin. EDIT D-085: in Path A the browser uploads its CAR to Pinata with the author’s key (car=true, public network) and compares the returned CID; Path B fetches through a public trustless gateway and verifies every block, so that gateway is not trusted.] - Because content is addressed by hash, extra pins are always safe and anyone (including the author’s institution) may add one. The web app SHOULD show the CID and a “pin it yourself” hint on every article page.
Provider credentials are the author’s secrets and follow the same rule as wallet keys: the app never asks for them in chat, logs or telemetry, and stores them only in browser storage with a clear “forget” action.
14.5 Canonical UnixFS parameters
Section titled “14.5 Canonical UnixFS parameters”Different tools can produce different CIDs for the same bytes. The web app, the CLI and the publishing guide MUST all use exactly:
CID version: 1multibase: base32 (display) / raw bytes (on-chain)hash: sha2-256directory codec: dag-pb (UnixFS)raw leaves: truechunker: size-262144 (256 KiB)DAG layout: balancedwrap in directory: yes (the root is the directory, not index.md)[EDIT D-035: additionally max 174 links per file node, HAMT sharding above 256 KiB of links (links-bytes estimation, fanout 256), links-first field order, no mode/mtime, hidden files excluded, entries sorted by name; never use -w (it adds a second directory level). Reference implementation: spec/fixtures/compute-cid.mjs.]
Equivalent Kubo invocation, used as the reference for tests:
ipfs add -r --cid-version=1 --raw-leaves=true --chunker=size-262144 --hash=sha2-256 --pin=true ./my-articleTest fixture: a sample article directory in /spec/fixtures/sample-article/ with its expected CID committed; the web app’s local encoder and the indexer’s re-hash MUST reproduce it.
14.6 Durability
Section titled “14.6 Durability”- The CID is the canonical content identifier.
- At least one active pinning provider is required in M1; the provider is behind
StorageProvider. - The indexer records
available,fetched_at,last_checked_atper CID and re-checks availability periodically. - Long-term archival replication (second provider, Filecoin/Arweave mirror) is post-M1.
15. Indexer
Section titled “15. Indexer”Architecture (chain is truth, SQLite is cache):
Algorand app calls (ARC-28 logs) → @algorandfoundation/algokit-subscriber (watermark, catch-up, idempotent) → chain_events (normalized, append-only) → projections (articles, coauthors, versions, reviews, votes, comments, mentions, flags, disputes, reputation, fields, content) → SQLite → REST APITables:
chain_events(tx_id, round, intra, app_id, event_name, event_version, payload_json, ts)— primary key(tx_id, intra); ingestion is idempotent.watermark(app_id, last_round).- Projection tables as listed; every row traces to a
chain_eventsrow. content(cid, available, validation_status, fetched_at, last_checked_at, size_bytes, title, abstract, keywords).mentions(article_id, comment_seq, mentioned_address, by_address, ts)— fromCommented.mentions.notifications(address, kind, article_id, comment_seq, ts, read)— projection only;kind ∈ {reply, mention, comment_on_my_article, review_on_my_article, comment_resolved, coauthor_invite, followed}[EDIT D-072: coauthor_invite is derived from Published.coauthors; EDIT D-109: followed, for the first follow of a pair only]. Read state is off-chain and per indexer instance.identities(address, scheme, value),article_dois(article_id, version, doi),follows(follower, target, active, since)— projections of the v3 declarations. [EDIT D-106..D-109]orcid_checks,doi_checks— base tables, kept across rebuilds: the last check of each declared ORCID iD against its public record (it must list the address’s profile page) and of each DOI against its Zenodo or DataCite record (published, IsIdenticalTo the article page and, for a version, its CID); rechecked weekly, every ten minutes for two days after a declaration that is not verified yet. [EDIT D-107/D-108]
Requirements: replay from round N; rebuild all projections from chain_events; schema migrations; crash-safe restart. [EDIT D-076/D-084: chain_events key is (tx_id, log_index); round N is the manifest’s startRound; catch-up goes through algoIndexer when set; schema v2 adds the base table pins (second copy), kept across rebuilds.]
One indexer instance per network, each with its own database and its own base URL (indexerApi in networks.json). Databases are never shared or merged; every API response carries network and appId so a client can detect a mismatch. The indexer refuses to start if the algod it is pointed at reports a genesis hash different from the manifest entry it was configured with.
API:
GET /articles?field=&type=&status=&sort=score|newGET /articles/:id(authors, versions, reviews, comments as a tree, flags summary, dispute state)GET /articles/:id/comments?root=— one thread, ordered by (depth, created_at)GET /articles/:id/citations,GET /users/:address/citations— citations between ARIADNE articles [EDIT D-126: derived from what each current version links to (its text and front-matterreferences): an article page of this network, a declared DOI not contradicted by its record, a version CID, or the amended parent; retracted citing articles are not counted. EDIT D-127, D-128: Thread, the citation measure: each article’s percentile among the articles of its field (area, then all, when fewer than 20), every pair compared at the age of the younger, by citations from articles with no author in common; a person’s Thread is the mean percentile of their articles weighted by 1 / authors, with one article at 50 added; amendments and retracted articles take no part. EDIT D-131: the Peer-reviewed seal, a derived view like citations: an article holds it while at least 2 qualified reviews are favourable (accept, minor revision) and outnumber the qualified unfavourable ones; a review qualifies when its author is not an article author, has reputation >= 10 in the primary or secondary field or a verified ORCID iD with >= 3 OpenAlex works in the area, has no conflict of interest (same ORCID iD, ARIADNE co-authorship or OpenAlex shared works within 3 years of the review, same current employer on ORCID), has >= 200 words read from IPFS, and, if favourable, does not return a favourable review received from an author within 2 years; suspended while disputed, none when retracted;GET /articles/:idgivesseal_detail, lists giveseal,/search/articles?sealed=truefilters. Status 3 (“final”) is shown as “closed version”]GET /notifications?after=— every address’s notifications after an id, for the push sender [EDIT D-124: browser notifications by Web Push from the web server; the verified ORCID record’s public contact data (websites, emails, countries, current employments) is a cached result like the check itself]GET /cite?ids=or?owner=&list=— what a bibliography needs of each article [EDIT D-123]GET /users/:address/lists,GET /users/:address/lists/:id— public lists of articles [EDIT D-122: not contract events but notes of 0-ALGO payments to oneself (ariadne/lists/<appId>:+ JSON), read by the indexer with a second subscription filter and stored in chain_events as ListNote rows, so that rebuild replays them; a note that breaks the rules changes nothing. Private lists stay in the browser.]GET /search/articles,/search/posts,/search/people,/search/suggest— full-text search over articles (title, abstract, keywords, body, authors, fields, DOIs), reviews and comments, and people, with filters, facets and highlighted passages [EDIT D-120: the search indexes (search_articles,search_posts) and the texts of reviews and comments (post_texts, kept only when they hash to their CID) are derived views, rebuilt from the chain plus IPFS; never an authority.]GET /users/:address/notifications— replies, mentions and activity on the user’s articlesGET /users/:address→ derived profile: publications (as submitter and as co-author), reviews, comments, reputation by field, claimable reputation per article (vote_total − claimed_total), timelineGET /fieldsGET /users/:address/following,/followers,/feed— on-chain follows and the activity of everyone followed [EDIT D-109]GET /orcid/:id— addresses whose declaration of that ORCID iD is verified [EDIT D-107] [EDIT D-107/D-108:GET /users/:addressaddsidentity.orcid(iD, check status, the record’s name once verified) and follow counts;GET /articles/:idaddsdois[]with their checks andauthors[].orcid(verified only).]
Ranking (indexer projection, never on-chain):
age_days = floor((now - created_at) / 86400)score = vote_total / (age_days + 2) ** 0.8Default feed excludes retracted; shows disputed with an explicit warning; content with validation_status ≠ valid shows a warning.
16. Web app
Section titled “16. Web app”/— feed (top / new), filter by field and type./a/[id]— article: rendered Markdown with assets, authors (accepted), version history, reviews, threaded comments (collapse beyond depth 3, “continue thread” link), reply and @mention composer with address autocomplete (recent participants in this article first), vote, flag (if eligible), status and dispute banner./publish— two tabs: Upload (Path A: editor with template pre-filled, asset upload, front-matter validation, local CID computation, pin, sign) and I have a CID (Path B: paste CID → fetch, validate, sign). Both show the CID, the exact deposit and the fee before signing, and link to the publishing guide./u/[address]— derived profile; for the connected wallet, shows claimable reputation per article with a “Claim” action (one transaction each, or a grouped batch)./inbox— notifications for the connected wallet (replies, mentions, activity on own articles). Purely an indexer view; nothing on-chain./fields— taxonomy browser.
Every screen that triggers a transaction MUST show the user the total cost (fee + deposit) before they sign.
[EDIT D-107..D-109: the profile lets its owner link an ORCID iD (with the steps to add the profile to the ORCID record) and shows a verified iD to everyone; an article page shows its verified DOIs, and its accepted authors can prepare a Zenodo draft (published by the author on Zenodo) and declare a DOI; follows are signed transactions; citations use ORCID names and DOIs.]
16.1 Network switch
Section titled “16.1 Network switch”The web app runs against exactly one network at a time and lets the person switch between the enabled ones in networks.json.
- Where: a selector in the header, always visible, showing the current network. On anything other than MainNet the whole UI carries a persistent coloured banner (“TestNet — articles here are for testing and carry no reputation on MainNet”).
- Scope: the network is part of the URL, so links are never ambiguous:
/a/123is MainNet;/testnet/a/123is TestNet;/localnet/...exists only in dev builds. Switching rewrites the current route to the same page on the other network when it exists there, otherwise to that network’s feed. The last choice is remembered inlocalStorageonly as the default for a bare/visit. - Data isolation: the app talks only to the
indexerApiof the selected network; nothing from another network is ever shown, cached across, or mixed in feeds, profiles or notifications. Switching clears in-memory state and the notification inbox. - Wallet:
@txnlab/use-walletis initialised for the selected network. Before enabling any sign button the app checks that the connected wallet reports the same genesis hash; on mismatch it disables signing and shows “Your wallet is on X, Ariadne is on Y” with a one-click switch of the app (never of the wallet, which the app cannot control). Every transaction is built with the manifest’s genesis hash, so a mismatched signature would be rejected by the network anyway. - Publishing: the publish flow shows the target network next to the cost. Publishing the same CID on both networks is allowed and produces two independent articles.
- Profiles:
/u/[address]on TestNet shows only TestNet activity and reputation; the page says so. - Disabled networks (MainNet before launch) do not appear in the selector at all. Enabling MainNet is: deploy, audit sign-off, fill
appId, setenabled: true, redeploy the static site.
Design: monochrome, one accent colour, generous line-height, max text width ~68ch, system font stack or Inter, no cards or shadows. arXiv readability, modern layout.
17. Protocol invariants
Section titled “17. Protocol invariants”The contract MUST preserve:
- Article identity (
asa_id) is immutable. - Article submitting author is immutable.
- Article primary and secondary fields are immutable.
- Article type and parent are immutable.
- An article has exactly one current version.
- Version numbers are strictly increasing by 1.
- A version never points to itself as predecessor; consecutive CIDs differ.
- A voter votes at most once per target.
- A reviewer reviews an article at most once.
- At the time the action is performed, an accepted author MUST NOT review, vote on, or flag their own article. Later changes to the accepted-author set do not invalidate earlier actions (§8.0).
- At the time of resolution, the comment’s author MUST NOT be an accepted author of the article. Same rule for later changes.
- Reputation never decreases in M1/M2.
- Reputation grants are deterministic from on-chain state and integer-valued.
- Resolved comments cannot be resolved twice.
- Flags are append-only; each counts once, only in its round.
- Status transitions outside §7.3 always fail.
- Retracted and disputed content remains queryable.
- No article, co-author, version, review, comment, vote or flag record is ever deleted.
- The article ASA is never held by any account other than the application or the recorded author.
- Every box key is ≤ 64 bytes.
- Every state-changing call emits exactly one canonical event.
- The application account’s spendable balance never decreases as a result of a user action (every MBR delta is covered by the caller; every inner fee is pooled from the caller).
- Co-authorship exists only with the co-author’s signed acceptance.
- A reputation box is only ever created by, and paid for by, its own address.
- A reply’s
depthequals its parent’sdepth + 1, never exceedsMAX_COMMENT_DEPTH, and itsroot_seqequals its parent’sroot_seq. - Mentions are distinct, never include the commenter, and never exceed
MAX_MENTIONS. vote_articlenever writes a reputation box;claim_reputationnever advancesclaimed_totalby more than it grants to the primary field.- [EDIT D-072] The article-vote reputation materialised by all accepted authors of an article never exceeds
vote_total. - [EDIT D-072] The author list of an article never changes after publication; an address that voted on or reviewed an article is never an accepted author of it.
- [EDIT D-065] No article can be left in
disputedwithout a possible resolution.
18. Threat model
Section titled “18. Threat model”M1/M2 explicitly consider, and either mitigate or document as a known limitation:
| Threat | Handling |
|---|---|
| Sybil accounts, reciprocal vote/review farming across accounts | Known limitation, damped by isqrt and REP_CAP_DAY; no prevention |
| Author↔reviewer “useful” cartel on the same article | prevented: author’s useful votes grant 0 |
| Comment-resolution farming | prevented: commenter not an author, one-time resolution |
| Mention spam / harassment | MAX_MENTIONS per comment; mentions grant nothing; inbox mute per address (indexer-side, M5) |
| Deep-nesting abuse | MAX_COMMENT_DEPTH enforced on-chain |
| Attaching someone as co-author without consent | prevented: signed acceptance required [EDIT D-072: being declared restricts nothing; the declared address may still vote, review and flag] |
| Late co-author harvesting earlier votes | prevented: claimed_total initialised to vote_total at acceptance [EDIT D-072: prevented by the closed list: nobody can be added after publication] |
| Vote cost or log budget exploding with author count | prevented: pull model; vote touches no reputation box |
| Flag spam | MIN_FLAG_REP, one flag per round per address, weight-based threshold |
| Voter forced to fund others’ storage | prevented: reputation boxes self-funded (§10.5) |
| Platform drained by MBR | prevented: invariant 22 |
| Malformed / oversized / mismatched IPFS content | indexer validation status; size limit at upload and fetch |
| External assets disappearing | prevented: assets must live inside the article directory |
| Duplicate CIDs across articles | allowed on-chain; indexer surfaces duplicates |
| Duplicate versions | rejected (CID must change) |
| Repeated event ingestion | idempotent chain_events primary key |
| Indexer crash / restart | watermark + replay |
| Missing pins | availability tracking; article stays valid |
| Malicious Markdown / HTML | sanitise rendered HTML; no raw HTML passthrough; asset paths restricted to assets/ |
| Malicious LaTeX | KaTeX with throwOnError: false, trust: false, maxSize/maxExpand limits |
| Unauthorised governance calls | sender check against global governance address |
| Quantum compromise of the governance key | governance is a native Falcon-1024 (post-quantum) account from launch; migrable via set_governance |
| Deployer key compromise | application immutable (no update/delete); creator has no privileged path |
| ASA transfer attempts | ARC-71: clawback always zero, freeze zero after claim, holding frozen; tests attempt transfer/clawback/unfreeze from every role and expect failure |
| Underpaid MBR | contract verifies the grouped payment amount |
19. Milestones
Section titled “19. Milestones”M1 — Core protocol (LocalNet): taxonomy, publish (incl. submitter co-author box), claim_article, new_version, set_status, vote_article, claim_reputation (single author), reputation with daily cap, all events for these, MBR verification and invariant 22 test. Acceptance criteria in §20.
M2 — Community layer: co-authorship (invite/accept [EDIT D-072: declare at publication / accept]; multi-author claim allocation decided per §10.2 gate before coding), reviews, review votes (author-vote = 0 grant), threaded comments with mentions, comment votes, resolution, flags, disputes, governance resolution. Same rigour as M1.
M3 — Indexer: subscriber → chain_events → projections → API. Rebuild-from-zero test. Directory content validation.
M4 — Web: feed, article page with assets, publish flow with wallet + IPFS directory pin, network switch (§16.1) with TestNet + LocalNet enabled and MainNet present but disabled in the manifest. TestNet deploy.
M5 — Polish: search, ranking, reputation display, ORCID attestation, second storage provider. [EDIT D-106..D-110: ORCID attestation, DOIs through Zenodo and on-chain follows are done in contract v3 (TestNet application restarted, D-110).]
Do not scaffold M3/M4 until M1 and M2 acceptance pass.
20. M1 acceptance criteria
Section titled “20. M1 acceptance criteria”M1 is complete only when all of the following pass on LocalNet:
Article lifecycle
publishcreates exactly one ASA;article_id == asa_id.- Author, primary/secondary field, type and parent are immutable (mutation attempts fail).
- Only
status,status_before_dispute,version,current_cid,prev_cid,updated_at, counters andclaimedever change. - ASA parameters at creation match ARC-71 Issued exactly (
clawback = ZeroAddress,freeze = app); afterclaim_articlethey match ARC-71 Held (freeze = ZeroAddress, holder frozen); afterretractedmanager = ZeroAddress. - ASA cannot be transferred to any account other than the recorded author; a claimed ASA cannot be transferred, clawed back or unfrozen by anyone (tested from author, app-call paths, governance and a stranger). A close-out by the author back to the app succeeds and leaves the article box untouched.
claim_articlefails when the author is not opted in, and when the sender is not the author.- Version numbers strictly increase;
new_versionfails infinal,retracted(anddisputed); fails when CID is unchanged. - Every row of the §7.3 table succeeds; every transition not in the table fails (parametrised test over all 5×5 pairs and both actor kinds). [EDIT D-002: on LocalNet the matrix is
set_statusover the 25 pairs × {author, governance, stranger}; rows 6-8 (flag, resolve_dispute) belong to M2 acceptance; disputed-state negatives run offline with a seeded box.] publishcreates the submitter’s co-author box (accepted = true, claimed_total = 0) and no reputation box.
Taxonomy
- Unknown field ids fail. Primary is mandatory. Secondary is 0 or a valid distinct field. Only governance adds fields.
Voting and reputation
- Weight equals
1 + isqrt(rep)for a table of reputation values including 0, 1, 3, 4, 99, 100, 10 000. - Weight uses the reputation snapshot at vote time (test: vote, then raise rep, then check stored weight unchanged).
- A voter with no reputation box votes successfully with weight 1 and no box is created for them.
- Duplicate votes fail. Self-votes fail. Votes in
disputedandretractedfail. vote_articleincrementsvote_totalby the voter’s weight and writes no reputation box (asserted by box listing before/after).claim_reputation: after N votes of known weights, the submitter’s claim grants exactlyΣwto the primary field andΣw / 2to the secondary; creates the reputation boxes on first claim (claimer pays); a second claim with nothing new fails; a claim by a non-author fails; claims indisputedandretractedfail.- Reputation is per (address, field); integer arithmetic verified against a reference implementation.
- Daily cap: grants within one epoch day never exceed
REP_CAP_DAYper (address, field); a capped claim advancesclaimed_totalonly by what was granted and the remainder is claimable the next epoch day (test spans two epoch days). - Reputation never decreases. Every non-zero grant emits
ReputationChanged. - Reputation is replayable: replaying all events yields the same totals as the boxes.
Costs
- Every box- or asset-creating call verifies a grouped payment from the sender equal to the MBR delta; underpayment fails; a payment from a third party fails.
- Invariant 22 test: across a scripted sequence of every M1 operation, the application account’s
(balance − min_balance)is identical before and after. - MBR and fees measured for publish, claim_article, vote_article, claim_reputation, add_field; recorded in
M1_REPORT.mdwith the exact byte sizes of every box.
Storage
- Every box key is ≤ 64 bytes (asserted per key type).
- Article CID with codec ≠
0x70fails; malformed lengths and all-zero digest fail.
Events
- Every state-changing call emits exactly one canonical ARC-28 event with
schema_version = 1. - Events are sufficient to rebuild article and reputation state without reading boxes (test does this).
Security
- Unauthorised governance calls fail. Invalid enums fail. Amendment without parent fails; parent must exist.
- Application update and delete transactions fail from every account, including the creator.
set_governancesucceeds only from the current governance address. - A full M1 test run executes every governance call from a native Falcon-1024 (
pqsig) account on LocalNet 5.x; the fee is sized viasimulate.
20.1 M2 acceptance criteria [EDIT: added in v2.9; section 19 asks for “the same rigour as M1”]
Section titled “20.1 M2 acceptance criteria [EDIT: added in v2.9; section 19 asks for “the same rigour as M1”]”M2 is complete only when all of the following pass on LocalNet, on top of every section 20 criterion:
Co-authorship and allocation (D-054, D-072) [EDIT D-072: rewritten for the list declared at publication]
publishaccepts 0 to 25 co-authors in ascending order, never the submitter, never a duplicate; it creates their boxes and the submitter pays them; 26 fail; with many co-authors the group carriesextendcalls. Nobody can be added later.- A declared address confirms once, in preprint, under_review or final; an address that voted on or reviewed the article cannot confirm; flags and comments do not prevent it. Neither publish nor accept creates a reputation box.
- After votes of known weights and any sequence of confirmations, every author’s claim equals the reference implementation (floor for co-authors, ceiling for the submitter) whenever they confirmed; the share of a co-author who never confirms is claimed by nobody; the reputation materialised by all authors never exceeds
vote_total(property test). - Accepted authors cannot vote on, review or flag their article; a declared, unconfirmed address still can. Co-authors cannot version, change status or resolve.
Reviews
- One review per (article, reviewer), raw-codec CID, recommendation 1–4, not by an accepted author, allowed until final and in disputed, never in retracted. The reviewer pays the review box, the index box and their own reputation box.
- A useful vote grants
w × 3to the reviewer under the daily cap; an accepted author’s vote is recorded and grants 0;useful_totalalways grows by the weight; the voter never gets a box.
Comments
- Threads: depth = parent.depth + 1 ≤ 6, root propagated, direct replies counted, replies only to comments of the same article, comments in every status.
- Mentions: at most 5, strictly ascending, never the sender, stored only in the event.
- A comment upvote grants
w / 5(floor); resolution grants 5, only by the submitter, only once, only for a comment of a non-author; all under the daily cap shared by every source.
Flags and disputes
- A flag needs reputation ≥ 10 in the article’s primary field, weighs
1 + isqrt(rep), is unique per (article, round, flagger) and is never an author’s. The call that makes the round total reach 30 moves preprint / under_review / final to disputed (section 7.3 row 6). - Section 7.4 holds while disputed. Only governance resolves: cleared restores the previous status (row 7), retracted revokes the ASA (row 8); both start a new round from zero and keep the old flags.
Costs, events, security
- Every M2 deposit equals the MBR delta to the microAlgo; underpayment and third-party payment fail; invariant 22 holds across a scripted sequence of every M2 operation and every rejected call.
- Every M2 call emits exactly one canonical event; replaying all events rebuilds every box of the application, byte for byte, with nothing missing and nothing extra.
- Every method fits one application call: measured opcode budget ≤ 600, ≤ 8 box references, ≤ 1 024 log bytes; the program fits 8 192 bytes. Results in
M2_REPORT.md, for Ed25519 andpqsigsenders.
21. Testing strategy
Section titled “21. Testing strategy”A. Offline unit tests (algorand-python-testing): weight(), every grant formula, daily cap rollover, state transition table, key encoding lengths, CID validation (both codecs), enum validation, comment depth/root propagation, mention list validation.
B. LocalNet integration tests (algokit-utils, pytest): real ASA creation, inner transactions, opt-in and claim, frozen-transfer failure, grouped MBR payments (correct, under, third-party), invariant 22 sequence, box MBR measurement, event emission and decoding via the in-repo ARC-28 decoder built from the ARC-56 events list (the app clients do not decode ARC-28; the indexer uses algokit-subscriber) [EDIT D-037].
C. Simulation: every integration test runs simulate before sending, asserting resource/box references and opcode budget. Any method that needs more than one app call’s budget MUST be flagged in the milestone report. [EDIT D-073: the gate is 600 opcodes and 8 box references per application call of the group; only publish with more than 4-5 co-authors needs more than one call, through extend().]
22. Open questions (decide as they come up, log in DECISIONS.md)
Section titled “22. Open questions (decide as they come up, log in DECISIONS.md)”- Whether
asset_namecan carry the id at creation time; ARC-19 reserve encoding for the current-version pointer (M5). - Refund of MBR overpayment (M1 keeps it; revisit).
FLAG_THRESHOLDabsolute vs relative to article activity.- Whether
disputedshould also blocksubmit_review. [EDIT D-020: closed, reviews stay allowed in disputed] - Multi-author allocation rule (§10.2 M2 gate) — MUST be decided before M2 implementation, not after TestNet. [EDIT D-054: closed by the owner, equal split with the remainder to the submitter]
- [EDIT D-066] Whether a review should stop earning once its reviewer becomes an accepted co-author of the article.
- [EDIT D-057]
GRANT_COMMENT = 1/5grants nothing for voters of weight below 5 (reputation below 16): tune with real activity. - Batch claiming across several articles in one call (M5 convenience).
- Whether MainNet articles should be able to reference TestNet ones (currently: no; networks are fully independent).
- Second pinning provider choice for M5.
- Timing of the governance migration to native post-quantum multisig once Algorand ships it.