Skip to content

Attestation Module - #430

Open
Primata wants to merge 1 commit into
m1from
feat-attestation-module
Open

Primata wants to merge 1 commit into
m1from
feat-attestation-module

Conversation

@Primata

@Primata Primata commented Sep 25, 2026 •

Copy link
Copy Markdown

Description

Adds an on-chain attestation framework to 0x1: independent sources assert facts about addresses (verified, at what level, with what attributes, who is excluded), and businesses consume them through policies without deploying any code. Five new framework modules:

Module Role
0x1::attestation Sources, facts, denials, issuer epochs, Merkle roots. The single write funnel.
0x1::attestation_policy A business's rules over sources: three-valued evaluation (allow / deny / step-up) with a reason code.
0x1::attestation_authorization Short-lived ed25519-signed capabilities that satisfy a step-up decision, with a nonce table for replay protection.
0x1::zktls Enrollment from zkTLS attestations: m-of-n secp256k1 attestor signatures over a claim, written as a fact.
0x1::merkle_proof OpenZeppelin-compatible Merkle verification (sorted pairs, double-hashed leaves).

Nothing here is globally trusted. A consumer names the sources it trusts or gets no answer, and two sources may disagree about the same subject without anything breaking.


Sources and facts (attestation)

A source is a resource account created with account::create_resource_account, following the same pattern as timelock (#304). The deployer authorizes creation and pays gas but gains no role unless listed. A resource account has no owner, so it cannot be transferred or burned out from under integrators that hardcoded its address.

Five roles per source, stored as vector<address> with add/remove pairs:

  • Admins configure the source, register and rotate issuers, and grant roles. At least one is always required.
  • Issuers write facts. This is the hot key, and it cannot touch denials or roles.
  • Sentinels can only add denials.
  • Removers can only remove denials. Mistaken denial is the most common real failure, so reversing one is a designed path with a separate key.
  • Guardians pause and unpause writes.

A fact is a Record per (source, subject): lifecycle state (NONE / ACTIVE / SUSPENDED / REVOKED), a u8 level, attributes (u16 key → bytes), expiry, issuer id and epoch, the digest of the attestation it came from, and a bounded change history.

All three positive write paths converge on one private record_fact, so precedence lives in one place:

  1. Issuer batch (issue_batch), the path that can grandfather an existing population with no user action.
  2. Permissionless relay (redeem_attestation) of an issuer-signed ed25519 attestation: anyone submits, the chain verifies, the submitter pays. The attestation must be newer than the stored record, can bind an optional nullifier, and the message format is published via the attestation_message view.
  3. zkTLS enrollment through zktls (friend-only record_verified_claim), with no issuer key involved.

Denials live in a separate table, written only by sentinels. No positive write path can create, modify or clear one.

Revocation primitives

  • bump_issuer_epoch invalidates everything one issuer wrote in a single O(1) write. Issuer id 0 is the zkTLS cohort.
  • set_floor_epoch invalidates everything below a source-wide floor. It is strictly increasing.
  • The effective issuer epoch is max(issuer epoch, floor), so writes made after a floor raise stay usable.

Read path
is_verified / active_with_level check, in order: denial, record exists, state is ACTIVE, not expired, issuer epoch current, not below the floor. Configuration (Source) and facts (Facts) are separate resources. The check path reads only Facts, whose own fields never change after creation (only table entries do), so gated transactions don't conflict under Block-STM. Pausing blocks writes and never changes what is_verified returns.


Policies (attestation_policy)

A policy is also a resource account. Its body contains require_any and require_all over (source, min_level), deny_any over sources, an optional chain_deny source, attribute predicates (IN, NOT_IN, EQ, GTE), and per-action step-up thresholds. evaluate returns (decision, reason) in a fixed order:

  1. paused → deny
  2. empty body → deny, so an unconfigured policy fails loudly
  3. chain deny
  4. deny_any
  5. require_all
  6. require_any
  7. attribute predicates
  8. step-up threshold for (action, amount)
  9. allow

Body changes are staged with an activation time (stage_body, stage_attr_rules), and anyone can call activate_pending once that time arrives. Source lists are capped at MAX_SOURCES and predicates at MAX_RULES, and a source that doesn't exist is rejected at staging time rather than failing later at evaluation.

A consumer gates its own entry function with one call:

attestation_policy::require(POLICY, signer::address_of(user), ACTION_TRANSFER, amount);
// or, when the policy may demand step-up:
attestation_policy::require_authorized(POLICY, signer::address_of(user), ACTION_TRANSFER, amount, auth);

simulate and simulate_counts dry-run a policy against a list of subjects before a change is staged.

Authorization (attestation_authorization)

The policy's authorizer key signs (policy, subject, action, amount_bucket, nonce, issued_at, expires_at), bound to the chain id and a domain separator. The amount is committed as a power-of-ten bucket, not an exact value, because authorizations are public forever. The policy caps the TTL. The nonce is burned only after the signature verifies, and prune_nonces can free expired nonces permissionlessly, which is safe because expiry is checked before the nonce. A nonce table is used rather than binding to the sequence number, because orderless transactions leave the sequence number untouched.

zkTLS (zktls)

A per-source Verifier stores attestor sets per epoch with an m-of-n threshold, plus a template allowlist.

enroll:

  • recomputes the Ethereum-prefixed keccak256 claim digest
  • recovers each signer with secp256k1::ecdsa_recover (accepting v as 0–3 or 27/28)
  • rejects unknown and duplicate signers, and checks the threshold
  • requires the claim to contain the subject address and template id

Safeguards:

  • Claims are single-use.
  • Only the current attestor epoch verifies, or the previous one until an admin-chosen grace deadline (previous_grace_secs, where 0 means cut off immediately).
  • A non-empty nullifier must appear inside the signed claim.

This path is for enrollment only and is never used per action.


Not included

  • Prologue enforcement. Gating inside transaction_validation.move needs a new mapped validation status and a Rust converter arm, and should not ship without a dead-man deadline. It is deliberately out of scope.
  • Fees. Nothing pays source operators to stay online, serve renewals or relay revocations.
  • CLI subcommands. The generated SDK builder entries cover every entry function, and aptos move run works today.
  • Enum V1 wrappers. Resources are plain structs, matching timelock. Because Move forbids adding fields to a published struct, this should be decided before the first release.

How has this been tested?

Move unit tests: 149 total.

  • attestation: 47, covering creation invariants, roles, the three write paths, lifecycle transitions, epoch and floor semantics, denial precedence, pause, attributes, relay replay/tamper/stale epoch/removed issuer, nullifiers, and roots.
  • attestation_policy: 37, covering every evaluation branch and reason code, staging and activation timing, the pause, simulation, step-up, and authorization.
  • zktls: 55, using fixed secp256k1 vectors, covering thresholds, duplicate and unknown signers, epoch grace windows, single-use claims, revoked templates, nullifier binding, the Ethereum v encoding, and cohort revocation.
  • merkle_proof: 10, using vectors generated with @openzeppelin/merkle-tree.

Rust e2e tests (aptos-move/e2e-move-tests/src/tests/attestation.rs): 34 tests against the real VM. Every signed payload (the relay attestation, the authorization and the zkTLS claim digest) is built independently in Rust and compared byte for byte with the module's published view before being submitted. A consumer package (attestation.data/gated) exercises require and require_authorized from inside a third-party entry function:

  • allowed, denied and unverified subjects
  • the step-up boundary
  • single-use authorizations
  • expired, over-bucket, over-TTL and wrong-subject authorizations
  • 2-of-3 zkTLS enrollment and its rejections

Move Prover: movement move prove --filter <module> returns Success for all five modules. Each *.spec.move file carries a <high-level-req> block mapping the invariants to the functions that enforce them. The key specs were checked for vacuity by deliberately breaking them and confirming the prover rejected each broken version. pragma verify = false is used only on the two create entry points, because of create_resource_account's cross-module effects; their *_internal bodies are verified. aborts_if_is_partial is used where aborts sit inside havocked loops (for_each_ref / while) or depend on early returns, and each use is annotated in the spec file.

Other:

  • internal_indexer_test::test_db_indexer_data is updated with the five new module names and passes.
  • cargo build -p aptos-cached-packages regenerates the docs and SDK builder. It also adds the builder entry for delegation_pool::enable_partial_governance_voting_if_needed, which was already stale on m1.

Type of Change

  • New feature

Components Impacted

  • Aptos Framework (new 0x1::attestation, 0x1::attestation_policy, 0x1::attestation_authorization, 0x1::zktls, 0x1::merkle_proof modules + specs)
  • Move/Aptos Virtual Machine (e2e coverage only; no VM changes)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I tested both happy and unhappy path of the functionality

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Adds 0x1::attestation, attestation_policy, attestation_authorization, zktls and merkle_proof with prover specs, unit tests, e2e tests, regenerated docs and SDK builder entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant