Skip to content

feat(core): balance conservation + signed policy_commit binding (#448, §9.5)#474

Merged
cryptskii merged 6 commits into
mainfrom
fix/balance-conservation-policy-binding
Jun 5, 2026
Merged

feat(core): balance conservation + signed policy_commit binding (#448, §9.5)#474
cryptskii merged 6 commits into
mainfrom
fix/balance-conservation-policy-binding

Conversation

@cryptskii
Copy link
Copy Markdown
Collaborator

What

Closes #448. Closes #467.

Enforces balance conservation by construction on the live state-advance path and binds the token's CPTA
policy_commit into the signed Operation::Transfer per whitepaper §9.5. This is the first mandatory blocker
of the custom-token policy + per-transfer readiness doctrine (added as a normative spec in this PR); the remaining
enforcement surfaces are tracked as Layer-B follow-ups.

Why

  • The canonical balance-mutation chokepoint DeviceState::advance applied caller-supplied BalanceDeltas blindly
    — conservation was enforced by convention at callers, not by construction; the real check
    (verify_token_balance_consistency) was dead (test-only, &State-typed).
  • §9.5 ("All TokenOps MUST include policy_commit; verifiers reject if it differs from the token's creation
    policy_commit") was normative but Operation::Transfer carried only token_id (a spec↔code gap).
  • The bilateral BLE path silently skipped the credit on an unresolved custom token instead of failing closed.

How

  • Spec doctrine (.github/instructions/token-policy-readiness.instructions.md): authoritative-source install,
    no absorption from peer/inbox/contact/offline, policy_commit bound in every transfer, per-transfer
    counterparty readiness, fail-closed. Each surface marked Layer A (shipped) or Layer B (planned, tracked).
  • policy_commit field added (required [u8;32]) to Operation::Transfer, threaded through all three
    serializers in lockstep (canonical to_bytes/from_bytes, storage codecs, offline encoder) and every
    construction site. Production send sites resolve from the local source-of-truth installed policy via
    resolve_policy_commit_strict (builtins → constants; custom → BCR-history anchor walk; unknown → fail closed).
  • Conservation guard at DeviceState::advance: for a Transfer, exactly one delta, amount == op.amount,
    direction Credit iff this device is the recipient else Debit, and delta.policy_commit == op.policy_commit;
    Mint = one Credit==amount; Burn = one Debit==amount; every other op carries no deltas. Rejects value
    creation/substitution regardless of caller. Code correspondence: lean4 DSMOfflineFinality.lean
    commit_conservation (no Lean change — the code realizes the existing theorem).
  • Bilateral fail-closed: the three sender/receiver delta builders now resolve the device's own installed
    policy_commit via the new AppRouter::resolve_policy_commit_strict (no silent skip, no peer-policy
    absorption); unresolved → empty deltas → guard rejects. §9.5 receive enforcement is realized end-to-end: signed
    op carries policy_commit, receiver independently resolves the delta's commit, guard rejects unless they match.

Migration

The required field changes the persisted Transfer-op wire layout (bcr_chain_states, bilateral_sessions), so
beta installs must wipe their local DB on upgrade (no auto-reset machinery). Early beta — acceptable.

Tests / verification

  • New conservation_guard_rules unit test + round-trip tests for all three serializers.
  • cargo clippy -p dsm -p dsm_sdk --all-features clean.
  • cargo test -p dsm --lib 1434 passed; dsm integration tests pass.
  • cargo test -p dsm_sdk --lib (single-threaded) 1506 passed; offline dsm_sdk integration tests pass.
  • Live-AWS e2e tests not run (need infra).

Tracked Layer-B follow-ups

#466 (issuer/root-signed anchor verification), #468 (receiver-side install verification), #469 (sender readiness
preflight), #470 (online published readiness object), #471 (offline readiness exchange), #472 (fail-closed test
matrix), #473 (extend policy_commit binding to Mint/Burn/CreateToken). Custom-token transfer readiness is marked
INCOMPLETE until these close.

Coordination with sibling PRs

cryptskii added 6 commits June 3, 2026 23:09
…#448)

Normative doctrine governing token-policy installation (authoritative-source only,
no absorption from peer/inbox/contact/offline/envelope), policy_commit binding in
every transfer (§9.5), and per-transfer counterparty readiness (online published
surface, offline direct exchange), with fail-closed semantics. Each enforcement
surface is marked Layer A (shipped) or Layer B (planned), cross-referencing tracked
issues #466-#473. Custom-token transfer readiness is marked INCOMPLETE until the
Layer-B issues close.

The first mandatory blocker (policy_commit binding into the signed Transfer op +
conservation + fail-closed) lands in this branch.
…, #467)

Per whitepaper §9.5 ("All TokenOps MUST include policy_commit") and the
token-policy doctrine §4, bind the token's CPTA policy commitment into the
signed Transfer operation. Adds a required [u8;32] `policy_commit` field to
Operation::Transfer and threads it through both serializers in lockstep
(operations.rs canonical to_bytes/from_bytes tag-3, and storage codecs).

Production send sites resolve policy_commit from the local source-of-truth
installed policy via CoreSDK/TokenSDK::resolve_policy_commit_strict (builtins ->
canonical constants; custom -> BCR-history anchor walk; unknown -> fail closed).
The b0x receiver reconstruction takes the sender's signed policy_commit from the
canonical preimage. Commitment-scaffolding/placeholder and test literals use a
zero placeholder.

Wire-format change: persisted Transfer operation bytes (bcr_chain_states,
bilateral_sessions) change layout — beta installs must reset their local DB.

Round-trip verified for both serializers. Conservation guard + receive-side
mismatch rejection follow in subsequent commits.
…te::advance (#448)

Add validate_conservation() at the sole balance-mutation chokepoint
(DeviceState::advance), called before the mutation loop. It requires the supplied
BalanceDeltas to exactly realize the signed operation:
- Transfer: exactly one delta, amount == op.amount, direction Credit iff this
  device is the recipient (op.to_device_id == self.devid) else Debit, and
  delta.policy_commit == op.policy_commit (§9.5 token binding).
- Mint: one Credit == amount. Burn: one Debit == amount.
- Every other operation: no balance deltas.

This rejects value creation/substitution regardless of caller, replacing the
previous enforce-by-convention model. Ports the rules of the dead, &State-typed
verify_token_balance_consistency onto &[BalanceDelta]. Code correspondence: lean4
DSMOfflineFinality.lean commit_conservation / commitTransfer (no Lean change).

Test fixtures that paired a Generic op with non-empty deltas (or empty deltas with
a Transfer) updated to matching value ops (mint_op/burn_op/value_op helpers); two
SMT-advance state_machine tests switched to a non-balance Generic op. Added
conservation_guard_rules unit test. Full dsm lib suite green.
…ding (#448, #467)

Make the bilateral BLE path consistent with the online path: the three
sender/receiver delta builders now independently resolve the device's own
installed policy_commit via the new AppRouter::resolve_policy_commit_strict
(builtins -> constants; custom -> BCR-history anchor walk) instead of the
builtin-only resolver that silently dropped the credit on any custom token. An
unresolved/uninstalled policy yields empty deltas, which the conservation guard
now rejects (fail closed) — no silent value skip, no peer-policy absorption.

§9.5 receive-side enforcement is realized end-to-end: the signed op carries
policy_commit, the receiver derives the balance delta's policy_commit by
independent resolution, and DeviceState::advance's conservation guard rejects
unless delta.policy_commit == op.policy_commit (i.e. the receiver's installed
policy matches the sender's committed policy).

Adds AppRouter::resolve_policy_commit_strict (default fails closed; AppRouterImpl
delegates to CoreSDK).
… to guard (#448)

- wallet.sendOffline: encode_offline_transfer_operation_canonical now writes the
  token's policy_commit (resolved from the local installed policy) so the
  offline-authored Transfer decodes and binds policy_commit like every other path.
- Test fixtures aligned to the conservation guard: bcr sample_operation is now a
  recipient-credit bound to its delta's policy_commit with matching amounts;
  smart_commitment_sdk execute_commitment tests use the builtin ERA token so
  policy resolution succeeds.

dsm (1434) and dsm_sdk (1506) lib suites pass; clippy clean.
#448)

CI builds the whole workspace; the validation harness binaries construct
Operation::Transfer and broke on the new required policy_commit field. These
tools build+sign ops and never apply through DeviceState::advance (no guard
interaction), so they carry a zero placeholder policy_commit. 7 sites across
adversarial_bilateral, bilateral_throughput, implementation_traces (x3),
property_tests, tla_trace_replay. Full workspace build + vertical_validation
tests (19) pass; clippy clean.
@cryptskii cryptskii merged commit 192803e into main Jun 5, 2026
9 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

1 participant