Skip to content

Move spend limit check to finalize phase#3242

Open
vicsn wants to merge 13 commits into
stagingfrom
move_spend_limits
Open

Move spend limit check to finalize phase#3242
vicsn wants to merge 13 commits into
stagingfrom
move_spend_limits

Conversation

@vicsn
Copy link
Copy Markdown
Collaborator

@vicsn vicsn commented May 5, 2026

Motivation

This is a lenient version of the block spend limit check, allowing for num_certificates * proposal_spend_limit worth of compute (same as snarkOS allows at the moment).

Test Plan

Besides CI, we should ensure:

  • sync test passing

Backwards compatibility

The new check is guarded by a consensus version.

Comment thread synthesizer/src/vm/finalize.rs Outdated
Comment thread synthesizer/src/vm/mod.rs Outdated
Comment thread synthesizer/src/vm/finalize.rs
Comment thread synthesizer/src/vm/finalize.rs Outdated
Comment thread synthesizer/src/vm/finalize.rs Outdated
Comment thread ledger/src/check_next_block.rs Outdated
Comment thread synthesizer/src/vm/finalize.rs Outdated
Comment thread synthesizer/src/vm/mod.rs Outdated
// Determine the block spend limit.
let block_spend_limit = if let Authority::Quorum(subdag) = block.authority() {
Some(
subdag.values().map(|certificates| certificates.len() as u64).sum::<u64>()
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.

This doesn't exactly translate to batch_spend_limit for each batch.

Now it's that the entire block can't exceed num_certificates * batch_spend_limit, so some batches can have more as long as the avg is <= batch_spend_limit

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Indeed, it is a bit more lenient. I don't see issues with it though. An alternative would be to compute and pass around a mapping of {proposal => {limit, transactions}} which seems like a lot of work.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Also note that since my last commits, I'm now also tracking compute_spend even for transactinos which may not be ultimately accepted. Seems like a very clean setup to me.

PrepareSpeculateResult::Finalize(compute_spend) => {
    if consensus_version >= ConsensusVersion::V15 {
        // Track the compute_spend used so far.
        block_spend = block_spend.saturating_add(compute_spend);
    }
}

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.

Can this be attacked by a malicious validator?

A validator could just fill their batch with to exceed the batch_spend_limit, and then everyone else's transactions will be aborted. Granted, this would require them to spend the fee.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Gave it some more thought and tried some prototyping. Sticking to a batch_spend_limit would really be better, but also a lot more refactoring, so I'd suggest we wrap this up and leave it as future work. This PR + the snarkOS PR is already an improvement over the status quo in that it prevents a halt under honest conditions.

Added one more commit e026b3c which clarifies the TODOs, tightly integrates fn batch_spend_limit again, and to reduce the need for further regression analysis it keeps the batch spend limit equal to the transaction spend limit.

Copy link
Copy Markdown
Collaborator

@raychu86 raychu86 May 19, 2026

Choose a reason for hiding this comment

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

Can you make an issue for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@mohammadfawaz
Copy link
Copy Markdown
Collaborator

LGTM! Deferring final approval to @raychu86.

Comment thread synthesizer/src/vm/finalize.rs Outdated
Comment thread synthesizer/src/vm/tests/test_v15/mod.rs
/// The block-specific random seed.
random_seed: [u8; 32],
/// The block spend limit.
block_spend_limit: Option<u64>,
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.

Is there a reason why this needs to live in FinalizeGlobalState instead of just being declared during VM::finalize?

The objects in FinalizeGlobalState were originally intended for objects that the actual finalize operations could pull from or execute on.

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.

The block_spend_limit also is not included in the pre-image of the seed. And it should not need to be.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

TL;DR: to reduce refactoring.

I'm computing the spend limit based on the number of certificates in the block in order to achieve more fairness. Our consensus algorithm moves in mysterious ways - under heavy load sometimes anchors may be skipped, a block contains 3x the amount of certificates, and I think we'd rather want to optimize for guaranteed transaction inclusion than stable blocktimes at this point in time. We don't have access to the number of certificates in atomic_speculate.

With that background in mind, the other IMO inferior options are:

  1. pass a new block_spend_limit variable along to atomic_speculate, speculate, and likely more call sites, which touches 10+ files and 30+ functions.
  2. similar to the pending_rejection_reasons (which carries over data from atomic_speculate to add_next_block, introduce a pending_block_spend, which carries over data into atomic_speculate.

@Antonio95 Antonio95 force-pushed the move_spend_limits branch from 957ac8a to a73d81e Compare May 18, 2026 13:51
@vicsn
Copy link
Copy Markdown
Collaborator Author

vicsn commented May 19, 2026

@raychu86 I further improved the spend_limit calculation and ran a regression test to see it would have been sufficient for all historic blocks: 27e1842

@raychu86
Copy link
Copy Markdown
Collaborator

@vicsn Fixes look good. My only remaining concern is - #3242 (comment)

Comment on lines +75 to +76
if height >= N::CONSENSUS_HEIGHT(ConsensusVersion::V15).unwrap() {
consensus_config_value!(N, TRANSACTION_SPEND_LIMIT, height).unwrap()
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.

I don't understand this. This is now saying that after ConsensusVersion::V15, a batch can't spend more than a singular transaction's TRANSACTION_SPEND_LIMIT

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In the previous versions of this PR it was much lower, but in order to reduce the scope of this PR and to avoid further regression analysis, I think a minimum of TRANSACTION_SPEND_LIMIT suffices. Documented this in more detail in the follow-up issue: #3264

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.

3 participants