Skip to content

Add root finalization delay or challenge period before minting from new epoch #571

Description

@Oluwaseyi89

Summary

Add root finalization delay or challenge period before minting from new epoch — merkle_bridge's update_root makes a newly-published Merkle root immediately usable by mint_wrapped in the same transaction block, giving no window to detect and freeze a bad root before wrapped tokens are minted against it.

Social Media Link

Let's collaborate on Discord. And ensure to star our repo.

Problem Statement

Confirmed in stellar-core/verifiable-registry/contracts/merkle_bridge/src/lib.rs:

  1. update_root writes and finalizes a root in one step: lib.rs:251-293 stores DataKey::MerkleRoot(epoch_id), updates DataKey::CurrentEpoch, and emits RootUpdatedEvent — there is no intermediate "proposed" state and no delay before the root becomes mintable.

  2. mint_wrapped accepts proofs against any root the instant it's stored: lib.rs:307-395 reads DataKey::MerkleRoot(epoch_id) directly (line 330-335) with no check on how long ago the root was published — a malicious or compromised Updater key (the sole require_updater-gated caller of update_root, line 258) could publish a fraudulent root and have credits minted against it within the same ledger close.

  3. require_updater is the only defense on root publication: lib.rs:503-514 checks caller == updater, a single address with no multisig, timelock, or secondary review — this contract has exactly one trusted key gating the entire bridge's integrity, and set_updater (lib.rs:224-239) lets the admin rotate it just as instantly.

  4. No DataKey exists for a pending/unconfirmed root: the enum (lib.rs:9-28) has Admin, Updater, CarbonAssetContract, CurrentEpoch, MerkleRoot(u64), MintedCredit(String), RetiredCredit(String), NextTokenId — no PendingRoot(u64) or RootPublishedAt(u64) to gate a delay window against.

  5. No mechanism exists to freeze or reject a root before it's used: unlike mark_retired, which lets the updater invalidate a credit after the fact (lib.rs:406-432), there is no way to invalidate a root before any mint_wrapped calls have consumed it — once update_root succeeds, the root is immediately live.

  6. NonSequentialEpoch enforcement makes roots strictly ordered but not delayed: lib.rs:267-270 only checks epoch_id != current_epoch + 1 — sequential ordering is unrelated to timing; a bad actor publishing epoch N+1 correctly in sequence is not slowed down at all.

  7. No event exists for "root published, pending finalization" vs. "root finalized": events.rs-equivalent event definitions in lib.rs:39-63 (CreditBridgedEvent, RootUpdatedEvent, CreditRetiredEvent) have no distinct pending/finalized pair that off-chain monitors could use to raise an alert during the window before finalization.

  8. Tests only cover immediate-availability behavior: test_update_root, test_multiple_epochs, and the various mint_wrapped tests in the #[cfg(test)] mod tests block all call update_root immediately followed by mint_wrapped in the same test, with no delay simulated — there is no test asserting a root is unusable until some challenge period elapses.

Required Changes

  1. Add DataKey::PendingRoot(u64) (mirroring MerkleRoot(u64)) and DataKey::RootPublishedAt(u64) to store a newly-submitted root separately from the finalized/mintable one, along with the ledger timestamp it was published.

  2. Add a configurable DataKey::ChallengePeriod: u64 (seconds), settable only by admin, defaulting to a conservative value (e.g. 3600 seconds).

  3. Change update_root to write into PendingRoot(epoch_id) / RootPublishedAt(epoch_id) and emit RootProposedEvent { epoch_id, root_hash, proposed_by } instead of immediately populating MerkleRoot(epoch_id).

  4. Add finalize_root(env, caller: Address, epoch_id: u64) -> Result<(), MerkleBridgeError> that requires env.ledger().timestamp() >= RootPublishedAt(epoch_id) + ChallengePeriod, then copies PendingRoot(epoch_id) into MerkleRoot(epoch_id), updates CurrentEpoch, and emits RootUpdatedEvent — callable by anyone (permissionless finalization) once the delay has elapsed, or restricted to updater/admin if a stricter model is preferred.

  5. Add freeze_pending_root(env, caller: Address, epoch_id: u64) -> Result<(), MerkleBridgeError> restricted to admin, allowing a suspicious pending root to be discarded before finalization — emit RootFrozenEvent { epoch_id, frozen_by }.

  6. Add MerkleBridgeError::RootNotFinalized, MerkleBridgeError::ChallengePeriodNotElapsed, and MerkleBridgeError::RootAlreadyFrozen variants.

  7. Update mint_wrapped to continue reading only from finalized MerkleRoot(epoch_id) (unchanged), so it naturally rejects proofs against a still-pending root with the existing RootNotFound error, or a more specific RootNotFinalized if distinguishing the two states is preferred.

  8. Add get_pending_root(env, epoch_id: u64) -> Option<(BytesN<32>, u64)> (root hash + published-at timestamp) as a view function.

  9. Update existing tests to insert a ledger-time advance (via env.ledger().set_timestamp(...)) between update_root/propose and any mint_wrapped call, and add new tests: minting against a root before the challenge period elapses fails; finalize_root before the delay elapses fails with ChallengePeriodNotElapsed; finalize_root after the delay succeeds and subsequent mint_wrapped calls work; freeze_pending_root prevents finalization of a discarded root.

Acceptance Criteria

  1. A newly-submitted root is not usable by mint_wrapped until finalize_root succeeds.
  2. finalize_root enforces the configured ChallengePeriod has elapsed since the root was proposed.
  3. freeze_pending_root lets admin discard a pending root before it is finalized.
  4. RootProposedEvent, RootUpdatedEvent (on finalize), and RootFrozenEvent are each emitted at the correct stage.
  5. CurrentEpoch only advances on finalization, not on proposal.
  6. Existing sequential-epoch enforcement (NonSequentialEpoch) continues to apply to proposed epochs.
  7. get_pending_root correctly exposes an outstanding proposal's root hash and publish timestamp.
  8. Test coverage includes: premature mint rejection, premature finalize rejection, successful delayed finalize-then-mint flow, and admin freeze of a pending root.

Directory to Work on:

stellar-core/verifiable-registry/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    RustThis issue is to be implemented with Rust programming language.SorobanThis issue is to be implemented with Soroban SDK

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions