Skip to content

feat: cumulative confirmations tier per BTC block - #48

Merged
karim-en merged 22 commits into
devfrom
block-limit
Aug 9, 2026
Merged

feat: cumulative confirmations tier per BTC block#48
karim-en merged 22 commits into
devfrom
block-limit

Conversation

@olga24912

Copy link
Copy Markdown

Confirmations tier now computed against the SUM of bridge tx amounts in a BTC
block, closing the split-deposit bypass (one big deposit chunked into many
small to fall under a lower tier).

Per-block sums in a fixed-capacity ring keyed by block_height % cap. Uses
verify_transaction_inclusion_with_heights (Near-One/btc-light-client-contract#140) to get tip + block height in one call; confirmations check moves to our callback.

Comment thread contracts/satoshi-bridge/src/api/management.rs Outdated
Comment thread contracts/satoshi-bridge/src/btc_light_client/deposit.rs
Comment thread contracts/satoshi-bridge/src/btc_light_client/mod.rs
Comment thread contracts/satoshi-bridge/src/btc_light_client/mod.rs Outdated
Comment thread contracts/satoshi-bridge/src/block_amount_ring.rs
Comment thread contracts/satoshi-bridge/src/config.rs Outdated
Comment thread contracts/satoshi-bridge/src/legacy.rs
Comment thread contracts/satoshi-bridge/src/lib.rs
Comment thread contracts/satoshi-bridge/src/legacy.rs
Comment thread contracts/satoshi-bridge/src/refund.rs Outdated
Comment thread contracts/satoshi-bridge/src/btc_light_client/mod.rs Outdated
@olga24912
olga24912 marked this pull request as ready for review August 7, 2026 13:43
@olga24912
olga24912 requested a review from a team August 7, 2026 13:43
@olga24912
olga24912 changed the base branch from omni-main to dev August 7, 2026 16:47

@frolvanya frolvanya left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a breaking change for our relayer, since it relies on get_confirmations method that was replaced by relayer_delta_for_predecessor, so we need to prepare all off-chain services for this before the upgrade

dao_call(
&context,
"extend_relayer_white_list",
json!({ "relayer_ids": [context.relayer.id()] }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every test here operate only with whitelisted relayer, so confirmations_delta is 0, while old tests operate without whitelisted relayer, so we won't see possible regression. Let's add a couple new tests without whitelisting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: 39fb3d1

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-BTC-block cumulative deposit confirmation tiers to prevent split-deposit bypasses.

Changes:

  • Introduces a fixed-capacity block-amount ring and callback-side confirmation checks.
  • Integrates the light client’s inclusion-with-heights API across deposits and refunds.
  • Adds migrations, mocks, and tests for cumulative tiers and ring behavior.

Reviewed changes

Copilot reviewed 19 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
.gitignore Ignores environment files.
Cargo.lock Updates workspace package versions.
contracts/mock-btc-light-client/Cargo.toml Bumps mock version.
contracts/mock-btc-light-client/src/lib.rs Mocks inclusion height responses.
contracts/satoshi-bridge/Cargo.toml Bumps bridge version.
contracts/satoshi-bridge/src/api/bridge.rs Passes mandatory coinbase proofs.
contracts/satoshi-bridge/src/api/management.rs Resizes the ring after configuration changes.
contracts/satoshi-bridge/src/block_amount_ring.rs Implements cumulative per-block tracking.
contracts/satoshi-bridge/src/btc_light_client/active_utxo_management.rs Preserves relayer confirmation deltas.
contracts/satoshi-bridge/src/btc_light_client/deposit.rs Applies cumulative confirmation checks to deposits.
contracts/satoshi-bridge/src/btc_light_client/mod.rs Adds inclusion proof and height APIs.
contracts/satoshi-bridge/src/btc_light_client/withdraw.rs Preserves withdrawal confirmation behavior.
contracts/satoshi-bridge/src/config.rs Adds maximum-confirmation helpers.
contracts/satoshi-bridge/src/legacy.rs Migrates legacy state with an initialized ring.
contracts/satoshi-bridge/src/lib.rs Adds ring state and versioned data.
contracts/satoshi-bridge/src/refund.rs Requires maximum-tier depth for refunds.
contracts/satoshi-bridge/src/upgrade.rs Migrates V6 contract state.
contracts/satoshi-bridge/src/utils.rs Adds inclusion-result size limit.
contracts/satoshi-bridge/src/zcash_utils/mod.rs Removes obsolete lint suppression.
contracts/satoshi-bridge/tests/test_block_limit.rs Tests cumulative tiers and ring behavior.
contracts/satoshi-bridge/tests/test_upgrade.rs Updates supported upgrade fixtures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +410 to +412
confirmations_delta: u64,
) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
self.process_inclusion_and_check(&pending_utxo_info, confirmations_delta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cumulative only ever rises, so required only ever rises, therefore this can never accept an under-confirmed deposit and the worse case is delaying other transfers in same block

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fixing: the bump can only over-count, so failures only make the tier stricter (fail-closed). A correct undo needs eviction/saturation handling and adds more risk than it removes.

Comment on lines +108 to +113
let cumulative = self
.data_mut()
.block_bridge_amounts
.bump(block_height, amount)
.unwrap_or(u128::MAX);
let required = self.internal_config().get_confirmations(cumulative) + delta;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: 3a7b418

@karim-en
karim-en requested a review from kiseln August 7, 2026 22:54
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
self.process_inclusion_and_check(&pending_utxo_info, confirmations_delta);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In verify_safe_deposit_callback, process_inclusion_and_check bumps the ring before the safe_mint cross-contract call. If safe_mint returns U128(0) (account not registered), safe_mint_callback removes verified_deposit_utxo and refunds the storage NEAR — but the ring bump is not unwound (it lives in a prior tx and can't be atomically reverted from a later callback).

  • Attacker pays real BTC to lock at the deposit address, plus storage NEAR.
  • Ring cumulative for that block is permanently inflated by the deposit amount.
  • Future legitimate deposits at the same BTC block face a higher confirmations tier than they otherwise would.
  • Attack strengthens the security requirement, doesn't weaken it. No fund loss.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the one hand, I completely agree with the point above. However, I'm not sure this actually needs to be fixed. Maybe we should leave it as is, and if we start seeing it become a problem, then we can fix it?

}
}

pub fn resize(&mut self, new_capacity: usize) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the DAO changes config in a way that alters capacity_for(config), entries whose new-modulo slots collide are resolved by keeping the higher block height; the loser's cumulative_sats is discarded. Any subsequent deposit for a discarded block returns None from bump() → max tier. Fail-closed, but worth documenting that a config change during heavy deposit load transiently forces some in-flight blocks to max-tier.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shrinking is provably safe. On a resize collision the discarded entry is always the older block, and colliding heights differ by a multiple of the new capacity, so the discarded block is at least capacity_new deep. Since capacity_new = max_required_confirmations + slack, its depth already exceeds the max tier by more than the slack — the None → max tier fallback is trivially satisfied at the moment of eviction, so no deposit is actually delayed.

The real transitional effect is in the opposite direction: when the window grows, blocks the ring had legitimately forgotten re-enter the new window with their cumulative reset to zero, so a deposit from such a block can temporarily pass under a lower tier than the full block sum would require under the new policy. I consider this acceptable: the under-count per block is bounded by the amount deposited before the config change, and that amount was fully subject to the then-active policy — during the transition the system is never weaker than the old rules. The effect is one-off and disappears within capacity_new blocks after the change. Raising tiers is a rare DAO action, so I wouldn't add extra handling for this.

Comment thread contracts/satoshi-bridge/src/block_amount_ring.rs Outdated
Comment thread contracts/satoshi-bridge/src/block_amount_ring.rs Outdated
Comment thread contracts/satoshi-bridge/src/config.rs Outdated
Comment thread contracts/satoshi-bridge/src/config.rs Outdated
Comment thread contracts/satoshi-bridge/src/legacy.rs
Comment thread contracts/satoshi-bridge/src/config.rs Outdated
}

pub fn get_confirmations(&self, config: &Config, satoshi_amount: u128) -> u64 {
/// Must be called at the synchronous entry point of a verify_* function —

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For that reason I think it's less error-prone to keep predecessor call out of this method and instead pass it as "relayer_account_id"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed: 0f96f8d

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relayer_delta should be removed in the future

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.

5 participants