Skip to content

feat(state): canonical fingerprint commitment for native module state (SRC-20 + NFT)#776

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/native-module-state-commitment
Jun 3, 2026
Merged

feat(state): canonical fingerprint commitment for native module state (SRC-20 + NFT)#776
github-actions[bot] merged 1 commit into
mainfrom
feat/native-module-state-commitment

Conversation

@satyakwok
Copy link
Copy Markdown
Collaborator

Draft to prevent auto-merge — do not merge; for review only.

Closes the native-module state-commitment gap for both SRC-20 ContractRegistry and NFT NftRegistry, and fixes 2 of the 3 CodeRabbit findings from #775.

Problem

SENTRIX_STATE_FINGERPRINT (STATE-FP) hashed only accounts + total_minted + EVM code/storage. Native-module state (SRC-20 contracts, NFT registry) was invisible → two validators could diverge purely in token/NFT state and print identical [STATE-FP] lines (CodeRabbit #1).

Change — canonical fingerprint commitment

  • NftCollection/NftRegistry::canonical_hash() — fixed field order; all maps (tokens, balances, token_approvals, operator_approvals, collections) folded with keys sorted → HashMap-order-independent. Read-only; no new mutable surface on the domain crate.
  • SRC20Contract/ContractRegistry::canonical_hash() — same approach (balances + allowances sorted).
  • compute_state_fingerprint() — extracted from emit_state_fingerprint (testable, no env gate); folds contracts.canonical_hash() + nft_registry.canonical_hash() into the combined fingerprint.
  • Address validation (CodeRabbit fix: remove unsupported GHA cache from docker build #3) — NFT apply path validates to/from/spender/operator via is_spendable_/is_valid_sentrix_address before mutating, mirroring SRC-20 M-02.

Design decision (explicit)

Requirements allow "state_root OR fingerprint." I took the fingerprint arm:

  • Consensus-enforced trie/state_root commitment of native state is a breaking change to live SRC-20 → needs a fork gate + deterministic migration at activation + testnet soak (the STATE_ROOT_V2 incident class). That's a soak-gated follow-up, not one safe PR.
  • Fingerprint commitment is consensus-neutral, needs no fork gate/migration, satisfies all stated requirements + tests, and closes the debug-visibility gap now. The canonical serialization built here is the foundation for the heavier trie commitment.

Deferred (documented)

  • Consensus-enforced trie/state_root commitment of native-module state (fork-gated + migrated + soaked).
  • Event-staging (CodeRabbit ci: add GHCR + auto-deploy pipeline #2): NFT emits inline in Pass-2 exactly like SRC-20; fixing NFT alone would diverge from the precedent. Uniform repo-wide event-staging is a separate PR.

Tests

  • sentrix-nft: canonical_hash order-independence, state sensitivity (deploy/mint/approve/transfer/burn), owner distinction, empty stability.
  • sentrix-core: fingerprint tracks SRC-20 + NFT changes, replay-deterministic, SRC-20 insertion-order-independent, distinct supply/owner differ, invalid-address rejection at the apply boundary.

Commands

  • cargo test -p sentrix-nft ✅ (43 + 2 doctests)
  • cargo test --workspace ✅ (66 suites, 0 failures)
  • cargo fmt --check ✅ · cargo clippy --workspace --all-targets -- -D warnings
  • cargo llvm-cov -p sentrix-nft ✅ 92.13% lines

No deploy. No RPC/explorer/wallet/marketplace/bridge/image-bytes. No state_root/trie/MDBX change, no fork gate.

Native-module state (SRC-20 ContractRegistry + NFT NftRegistry) was
absent from the STATE-FP fingerprint, so two validators could diverge
purely in token/NFT state and still print identical [STATE-FP] lines
(CodeRabbit #1 on #775). Close the debug-visibility gap consistently for
both native modules with canonical, HashMap-order-independent hashing.

- NftCollection/NftRegistry: canonical_hash() — fixed field order, all
  maps folded with keys sorted (read-only, no new mutable surface).
- SRC20Contract/ContractRegistry: canonical_hash() — same approach.
- block_executor: extract compute_state_fingerprint() (testable, no env
  gate) and fold both registries' canonical_hash into the combined fp.
- nft apply path: validate to/from/spender/operator addresses via
  is_spendable/is_valid_sentrix_address before mutating (CodeRabbit #3),
  mirroring SRC-20 M-02.

Scope: this is fingerprint (debug) commitment only. Consensus-enforced
trie/state_root commitment of native-module state is the heavier
fork-gated + migrated + soaked follow-up; canonical serialization here is
its foundation. No state_root/trie/MDBX change, no fork gate, no consensus
impact. Event-staging refactor (CodeRabbit #2, shared with SRC-20) stays
deferred — fixing NFT alone would diverge from the SRC-20 precedent.

Tests: sentrix-nft canonical_hash (order-independence, state sensitivity,
owner distinction, empty stability); sentrix-core fingerprint tracks
SRC-20 + NFT changes, replay-deterministic, order-independent, distinct
states differ; invalid-address rejection at the apply boundary.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

❌ Patch coverage is 90.38462% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/sentrix-nft/src/collection.rs 81.01% 15 Missing ⚠️

📢 Thoughts on this report? Let us know!

@satyakwok satyakwok self-assigned this Jun 3, 2026
@satyakwok satyakwok marked this pull request as ready for review June 3, 2026 17:02
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Warning

Review limit reached

@satyakwok, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 35 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d24bb01d-bbf5-4c5b-b117-4e30e4325d4e

📥 Commits

Reviewing files that changed from the base of the PR and between 54f311f and eadfc19.

📒 Files selected for processing (6)
  • crates/sentrix-core/src/block_executor.rs
  • crates/sentrix-core/src/nft.rs
  • crates/sentrix-core/src/vm.rs
  • crates/sentrix-nft/src/collection.rs
  • crates/sentrix-nft/src/lib.rs
  • crates/sentrix-nft/src/registry.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-module-state-commitment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot enabled auto-merge (squash) June 3, 2026 17:02
@github-actions github-actions Bot merged commit a545a67 into main Jun 3, 2026
20 checks passed
satyakwok added a commit that referenced this pull request Jun 3, 2026
Patch coverage on #776 flagged 15 uncovered lines in collection.rs
canonical_hash — the branches the existing tests didn't hit:
bounded max_supply (Some arm), token uri_hash/metadata_hash (Some arm),
and the operator-approvals loop.

- lib.rs: black-box test covering Some(max_supply), non-empty token URI,
  single-token approval, and operator-for-all (toggle moves the hash).
- collection.rs: white-box test for uri_hash/metadata_hash — these
  integrity hashes have no public setter yet, so the Some arm is only
  reachable by setting the fields directly on a live token.

canonical_hash is now fully covered in both collection.rs and registry.rs.
github-actions Bot pushed a commit that referenced this pull request Jun 3, 2026
Patch coverage on #776 flagged 15 uncovered lines in collection.rs
canonical_hash — the branches the existing tests didn't hit:
bounded max_supply (Some arm), token uri_hash/metadata_hash (Some arm),
and the operator-approvals loop.

- lib.rs: black-box test covering Some(max_supply), non-empty token URI,
  single-token approval, and operator-for-all (toggle moves the hash).
- collection.rs: white-box test for uri_hash/metadata_hash — these
  integrity hashes have no public setter yet, so the Some arm is only
  reachable by setting the fields directly on a live token.

canonical_hash is now fully covered in both collection.rs and registry.rs.
satyakwok added a commit that referenced this pull request Jun 3, 2026
…ted) (#779)

* feat(state): commit native module state into trie/state_root (fork-gated)

Native-module state (SRC-20 ContractRegistry + NFT NftRegistry) was
committed only to the STATE-FP fingerprint (#776), not the consensus
state_root — so a divergent registry was not rejected by peers. Commit
both registries' canonical hashes into the state trie behind a new fork
gate, mirroring the proven SIP-6 STATE_IN_TRIE pattern.

- new gate NATIVE_STATE_IN_TRIE_HEIGHT, default u64::MAX on BOTH nets.
  Deliberately NOT reusing STATE_IN_TRIE_HEIGHT (already active on testnet
  at 6,026,000) — reusing it would retroactively fork testnet's state_root.
- two fixed trie keys: sentrix/v1/native_src20_registry +
  sentrix/v1/native_nft_registry; value = registry canonical_hash (sorted,
  HashMap-order-independent — from #776). Always-insert post-fork (empty
  registry has a stable hash), same semantics as total_minted/epoch.
- update_trie_for_block captures both hashes before the trie mut-borrow
  (Phase 2f), independent of the SIP-6 gate.

Pre-fork: state_root is bit-identical to today (native state stays
off-trie). Post-fork: SRC-20 / NFT state changes move state_root.

Commitment model: the trie stores the registry canonical HASH, not the
full registry — divergence is detected (peers reject the block); recovery
is via peer resync (the blob remains the data source). Activation requires
halt-all + state-root-alignment pre-flight + simul-start so every validator
commits identical native state at the activation block (STATE_IN_TRIE
playbook). NFT_TOKENOP_HEIGHT stays disabled — the NFT registry commits as
empty until it is enabled.

Tests: pre-fork preserved; post-fork SRC-20 + NFT changes move state_root;
replay-deterministic; different supply/owner differ; deploy-order
independent; empty commitment stable; fork-gate default-disabled on both
nets + activates at pinned height.

* test(trie): cover native registry key builders

codecov/patch on #779 flagged 10 uncovered lines (0%) in
sentrix-trie/src/address.rs: the two new native_*_registry_key builders.
They're exercised by sentrix-core's integration tests, but codecov
measures sentrix-trie in its own package run where its unit tests didn't
call them. Add a unit test asserting both keys are deterministic, mutually
distinct, and distinct from total_minted/epoch/account keys.
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