Skip to content

feat(usage-meter): link usage records to verified attestations - #84

Merged
joelpeace48-cell merged 1 commit into
FinesseStudioLab:mainfrom
Alheriii:feat/usage-attestation-link-70
Aug 24, 2026
Merged

joelpeace48-cell merged 1 commit into
FinesseStudioLab:mainfrom
Alheriii:feat/usage-attestation-link-70

Conversation

@Alheriii

Copy link
Copy Markdown
Contributor

Closes #70

Summary

A usage record is only as trustworthy as its link back to the evidence behind it. Metering could happen with no attestation and attestations could exist with nothing metered against them, so an invoice could not be traced to what justified it — which is the entire pitch.

Every record now carries the attestation it was priced against, verified against audit-registry at write time rather than taken from the caller. Verification checks the attestation exists, is not superseded, and belongs to the claimed payer; the registry's own verify_attestation is the single predicate, so the two contracts cannot drift on what "valid" means.

Interface into audit-registry

Declared locally as a #[contractclient] trait rather than by depending on that crate. Depending on the contract crate would pull its #[contractimpl]-generated WASM exports (initialize, version, …) into this binary and collide at link time with this contract's own exports of the same names. It also means this PR needs no changes when #83 merges, in either order.

Double-metering

An attestation id is burned on first use; a second record against it fails with AlreadyMetered.

Only a verified attestation is burned. An id that failed verification has metered nothing, so marking it would let a typo — or a race against the registry write — permanently block the real attestation that later occupies that id. There is a test for exactly that sequence: fail, then attest, then succeed.

Unattested-usage policy

Configurable per payer, defaulting to the strict option. An unset policy is Reject, rather than defaulting to lenient and relying on someone to tighten it — a payer is only ever billed for usage nothing vouches for after explicitly opting in.

The switch is authorized by the payer, not the admin. It decides whether the payer can be billed on trust, so it is not the admin's call to make; there is a test asserting the admin's signature is insufficient.

Opted-in records are flagged attested: false, have the unverified reference dropped so they do not read as sourced, and accumulate against a separate total (unattested_units vs attested_units) so downstream pricing can treat them differently without re-deriving which was which.

Cross-contract call cost, measured

The issue asks for this to be quantified rather than assumed. Measured on soroban-sdk 27.0.6, comparing an attested write (one cross-contract call) against an unattested one (no call), same contract, same storage writes otherwise:

write CPU instructions memory bytes
unattested (no call) 186,318 77,223
attested (one call) 256,825 97,795
verification 70,507 20,572

About 0.07% of the 100,000,000-instruction transaction budget. The per-record call is affordable and the batch-root alternative the issue raises is not needed at this cost. The measurement lives in a test (test_cross_contract_verification_cost_is_measured, run with --nocapture) so it stays honest across SDK upgrades; the assertion is direction-and-magnitude rather than an exact figure, so an SDK bump does not fail CI on a number.

Authorization

Follows the pattern in #74 / #83: the payer's signature is bound to the attestation id and the unit count with require_auth_for_args, so an intermediate cannot take a signature given for one metering and spend it on a larger one.

No allowlist is needed on this edge — usage-meter is the caller here, and verify_attestation is an unauthorized read by design. When payment-router later calls into this contract, that edge will need one.

Also in this change

The scaffold's Symbol admin placeholder becomes a real Address, plus typed errors, checked arithmetic on the unit totals (a wrapped total silently zeroes an invoice rather than failing it), TTL extension on every persistent write, and #[contractevent] types for the observable state changes — events().publish is deprecated in SDK 27 and CI runs with -D warnings.

Tests

  1. The two that are about authorization do not use mock_all_auths, since it would approve every require_auth in the transaction and make them pass regardless. The rest use a StubRegistry implementing only verify_attestation, which keeps this suite about the link — pass, fail, replay — rather than about attestation semantics that belong to the registry's own tests.

Migration note

initialize(admin: Symbol) becomes initialize(admin: Address). ping is gone; version returns 2. A deployment must call set_registry before any usage can be recorded — record_usage fails with RegistryNotSet rather than silently recording everything as unattested.

Conflicts

One of three PRs. File sets are disjoint by crate: this one touches usage-meter/ only, #83 touches audit-registry/ plus docs/, #73's touches payment-router/ only. All six merge orders were tested locally against main — clean in every order, with fmt, clippy -D warnings, cargo test --workspace (45 tests) and the wasm32v1-none release build passing on the combined result.

A usage record is only as trustworthy as its link back to the evidence
behind it. Metering could happen with no attestation and attestations could
exist with nothing metered against them, so an invoice could not be traced
to what justified it — which is the entire pitch.

Every record now carries the attestation it was priced against, and that
reference is verified against audit-registry at write time rather than taken
from the caller. Verification checks the attestation exists, is not
superseded, and belongs to the claimed payer; the registry's own
verify_attestation is the single predicate, so the two contracts cannot
drift on what "valid" means.

The interface into audit-registry is declared locally as a #[contractclient]
trait rather than by depending on that crate. Depending on the contract crate
would pull its #[contractimpl]-generated WASM exports (initialize, version)
into this binary and collide at link time with this contract's own exports of
the same names.

Double-metering is rejected: an attestation id is burned on first use and a
second record against it fails with AlreadyMetered. Only a *verified*
attestation is burned — an id that failed verification has metered nothing,
so marking it would let a typo or a race against the registry write
permanently block the real attestation.

Unattested usage is configurable per payer and defaults to the strict option.
An unset policy is Reject, so a payer is only ever billed for usage nothing
vouches for after explicitly opting in. That switch is authorized by the payer
rather than the admin — it decides whether the payer can be billed on trust,
so it is not the admin's call. Opted-in records are flagged attested: false,
have the unverified reference dropped so they do not read as sourced, and
accumulate against a separate total so downstream pricing can treat them
differently without re-deriving which was which.

Cost of the cross-contract call, measured rather than assumed (soroban-sdk
27.0.6, attested write vs unattested write, same storage writes otherwise):

  unattested (no call)   cpu=186,318   mem=77,223
  attested (one call)    cpu=256,825   mem=97,795
  verification           cpu= 70,507   mem=20,572

About 0.07% of the 100,000,000-instruction transaction budget, so the
per-record call is affordable and the batch-root alternative the issue raises
is not needed at this cost. The measurement lives in a test so it stays
honest across SDK upgrades.

Authorization follows the pattern from FinesseStudioLab#74: the payer's signature is bound to
the attestation id and the unit count with require_auth_for_args, so an
intermediate cannot take a signature given for one metering and spend it on a
larger one. usage-meter is the caller on this edge and verify_attestation is
an unauthorized read, so no allowlist is needed here.

Also replaces the Symbol admin placeholder with a real Address, adds typed
errors, checked arithmetic on the unit totals (a wrapped total silently zeroes
an invoice rather than failing it), TTL extension on every persistent write,
and #[contractevent] types for the observable state changes.

Tests: 13. The two that are about authorization do not use mock_all_auths.

fmt, clippy -D warnings, cargo test --workspace and the wasm32v1-none release
build all pass.
@joelpeace48-cell
joelpeace48-cell merged commit 978dd09 into FinesseStudioLab:main Aug 24, 2026
2 checks passed
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.

usage-meter: link usage records to attestation ids across contracts

2 participants