A Move primitive for typed, on-chain attestations (e.g. audits) about arbitrary
subjects (packages, addresses, anything that has an ID), plus a shell CLI demo
that exercises it.
The design is off-chain-primary: attestations are stored under a deterministic, type-filterable on-chain layout that's cheap to enumerate from indexers; cross-cutting behaviors like expiration are expressed as Display-field conventions rather than additional Move types.
packages/
attestations/ — the only deployable: Registry, Box, Attestation
examples/ — reusable schema patterns for third-party attesters
auditor/ — reference auditor schema (`Audit` + `AuditAdminCap`)
demo/ — fixtures that exist only to drive the local demo (see demo/README.md)
auditor_a/ — copy of examples/auditor + an AuditV2 upgrade; TRUSTED in the demo
auditor_b/ — a second copy; NOT trusted (the identity-based-trust demo)
auditor_c/ — a third copy; a second TRUSTED attester
dependency_example/ — a subject; dependency of subject_example
subject_example/ — the browsed subject (depends on dependency_example)
scripts/ — demo orchestration: run-demo.sh, test-publish.sh, demo.sh
scripts/ — reusable CLI ops: create-box, attest-audit, revoke-audit
CONVENTIONS.md — Display-field conventions (expires_at, …)
FUTURE-EXTENSIONS.md — design memos for surfaces deliberately deferred from v0
Registry (shared singleton)
├── active box (a claimed `Box`) ──owns──▶ un-revoked Attestation<T> (TTO)
└── revoked address (no object) ──owns──▶ revoked Attestation<T>
A Registry is a shared singleton, parent of two derived box addresses per
subject — active and revoked. Each address is
derived_object::derive_address(registry, BoxKey { subject, revoked }) —
computable off-chain — so consumers enumerate every un-revoked attestation about
a subject via getOwnedObjects(active_box, filter={StructType: …}), with
server-side type filtering. An attestation carries no status field: it is
revoked iff it lives at the revoked address.
Only the active address holds a claimed Box object, because revoke needs a
&mut UID there to transfer::receive from. Nothing ever receives from the
revoked address, so it needs no object.
The key design feature is that the schema package has complete control over
its attestations. attest, revoke, and register_display are all gated by
Permit<T>, which Move lets only T's defining package mint — so only that
package can issue, revoke, or set the Display for Attestation<T>. The
recorded attester is therefore T's package — bound to the type at compile
time, not denormalized into a field — and each schema defines its own revocation
authority (an admin cap, a per-attestation bearer cap, or none at all).
Revocation moves an attestation from the subject's active box to its revoked
address. Time-based effectiveness (expiration) and other cross-cutting concerns
sit in the Display layer per CONVENTIONS.md — the registry itself stays minimal.
("Negative" attestations — vulnerability disclosures that propagate from a
dependency to its dependents — are a planned fast-follow, not in this positive
MVP.)
bash scripts/check.sh # every package
bash scripts/check.sh packages/attestations # just the ones namedcheck.sh attempts every package even if one fails, and exits nonzero if any
did. Each Move package also builds and tests independently, but they pin
env-specific dependencies, so a direct sui move test needs a build env:
cd packages/attestations && sui move test --build-env testnetThe demo creates Boxes for two real subjects (dependency_example and the
subject_example that depends on it), issues audits (an Audit on the
dependency; an AuditV2 and a v1 Audit on the subject) plus two
attestations a trust consumer must filter out, then revokes the dependency's
audit and the subject's v1 Audit — showing each leave its active box (the
subject keeps its AuditV2 as the live signal).
bash demo/scripts/demo-up.sh # chain only
MVR_DIR=/path/to/mvr bash demo/scripts/demo-up.sh # + demo_server :8000, app :3000
bash demo/scripts/demo-down.sh # idempotent; always safedemo-up.sh brings up the stack and tears it all down on Ctrl-C. Underneath it,
demo/scripts/run-demo.sh owns the chain lifecycle: it starts a fresh localnet,
waits for readiness, faucets gas, test-publishes every package, registers
Displays, runs the demo, and stops the localnet and its Postgres on exit.
Override the sui CLI binary with SUI=/path/to/sui.
SIGKILL (kill -9, pkill -9, a reaped background shell) skips every shell
trap, so nothing gets to clean up and the demo's Postgres is left running. Use
Ctrl-C, and if a stack does get killed that way run demo-down.sh — it reclaims
the ports and removes the scratch dirs without relying on any trap having run.
The defaults target a local sui network (sui start --with-faucet); to point
at testnet or another remote network, pass --rpc <url> and --pubfile <path>.
Prerequisites:
-
Start a localnet in another shell:
sui start --force-regenesis --with-faucet
This serves gRPC + JSON-RPC on
127.0.0.1:9000and a faucet on:9123. -
Switch your sui CLI to it and faucet a bit of gas:
sui client switch --env local # use whichever env points at 127.0.0.1:9000 sui client faucet
-
Test-publish all packages with one shared pubfile and register the Displays. The script does the whole sequence in one go and prints the
REGISTRY_ID=…export line you'll need next:./demo/scripts/test-publish.sh
That writes
Pub.localnet.tomlat the repo root (gitignored — ephemeral and per-user). -
Export the printed Registry id:
export REGISTRY_ID=0x…
Then run the demo (it reads Pub.localnet.toml and REGISTRY_ID):
bash demo/scripts/demo.shdemo/scripts/demo.sh composes the scripts/ CLI ops (create-box, attest-audit,
revoke-audit): it creates the boxes, issues the audits, revokes two of them, and
writes demo-ids.json for the MVR seeder, printing each step's object ids.
Options:
--rpc <url>— override the default localnet gRPC endpoint.--pubfile <path>— use a different pubfile (e.g.Pub.testnet.tomlif you've published to testnet instead).--subject <hex-id>— re-use a specific subject ID. Default: a fresh random ID per run, socreate_boxdoesn't collide on re-runs.
Off-chain consumers decide whether an attestation is effective: it must be
unexpired per the expires_at Display convention (if present). Revocation is
handled upstream by box membership — a revoked attestation lives at the revoked
address, not the active box — so it isn't part of that check. See CONVENTIONS.md
for the conventions and their evaluation rules.
CONVENTIONS.md— schema-level conventions for cross-cutting behaviors.FUTURE-EXTENSIONS.md— the deferred on-chain inspection API (borrow/put_back hot potato) with the design memo preserved.