Skip to content

[Feat] Implement RejectedReason#3159

Open
raychu86 wants to merge 13 commits intostagingfrom
feat/rejection-reason
Open

[Feat] Implement RejectedReason#3159
raychu86 wants to merge 13 commits intostagingfrom
feat/rejection-reason

Conversation

@raychu86
Copy link
Copy Markdown
Collaborator

@raychu86 raychu86 commented Feb 21, 2026

Motivation

This PR introduces RejectedReasons that stores why a transaction was rejected in the Rejected object. Depending on the reason, there may be a locator, index, and command stored as well. This will help with developer debugging as they can better trace where their errors are coming from.

The indexing is done on the Aleo Instruction level, so we may eventually want a tool that points to where in Leo code the error originates from.

Currently the 3 types are:

  • DuplicateProgramID (Duplicate program ID deployment in the same block)
  • Finalize (Finalize/constructor command failed)
  • NonFinalize (VM error such as stack failure or bond_public restrictions)

Alternative to #3101.

Test Plan

Tests have been added to ensure that the RejectedReasons are properly constructed and only occur after the desired consensus version.

Backwards compatibility

This functionality is guarded with ConsensusVersion::V14, where rejected reasons are only stored after V14, and are invalid before then.

TODO

  • Contemplate adding the arguments that caused the error to the RejectedReason (This can cause state bloat and is a DOS vector)
  • Consider introducing an AbortedReason as well.

Copy link
Copy Markdown
Collaborator

@ljedrz ljedrz left a comment

Choose a reason for hiding this comment

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

The general approach looks reasonable and maintainable to me.

Storage/serialization-wise, we need to ensure that the error values are constructed in a way that ensures that the database compression does its job well, which is even more important than keeping them short. For instance, a value like xxxxxxxxxxxxVARyyyyyyyyyy is strictly worse than xxxxxxxxxxxxyyyyyyyyyyVAR (where xs and ys are constant, and VAR varies) for dictionary-based compression that's used by rocksDB+LZ4.

We may also want to create some new test cases ensuring that the constant parts of the error strings do not change (I saw some new test cases, but I think they were concentrated on ensuring that the right errors are returned, as opposed to their string values).

@raychu86
Copy link
Copy Markdown
Collaborator Author

Storage/serialization-wise, we need to ensure that the error values are constructed in a way that ensures that the database compression does its job well, which is even more important than keeping them short. For instance, a value like xxxxxxxxxxxxVARyyyyyyyyyy is strictly worse than xxxxxxxxxxxxyyyyyyyyyyVAR (where xs and ys are constant, and VAR varies) for dictionary-based compression that's used by rocksDB+LZ4.

We may also want to create some new test cases ensuring that the constant parts of the error strings do not change (I saw some new test cases, but I think they were concentrated on ensuring that the right errors are returned, as opposed to their string values).

@ljedrz I've refactored RejectedReason to store concrete objects rather than strings, this should help alleviate the compression concerns - 990183e.

/// The transaction was rejected due to a failed finalize command. (program ID, resource, index, command).
/// Note: We do not log the actual error message from the finalize command, as it may contain
/// sensitive information or lead to DOS vectors by storing string representations of large structs.
Finalize(ProgramID<N>, Identifier<N>, usize, Command<N>),
Copy link
Copy Markdown
Collaborator

@d0cd d0cd Mar 4, 2026

Choose a reason for hiding this comment

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

An erroring assert.eq command can be up to 2KB.
It's not that large, but it's worth considering removing Command<N> from the enum variant.
The other fields provide enough information for tools to index and find the right command.
Some benefits include smaller enums and a simpler implementation for from_indexed_finalize_error.

One added note, we should probably also track the program edition.

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.

Added edition here - 70bb92e.

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.

Is the 2KB maximum cause for concern? It is a DOS vector, but not sure if it's worth saving on the abstraction of requiring state and doing the query.

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.

A bandaid solution for the future: we can consider disabling the serialization if the bloat gets out of hand.

Though it would be best to have a proper decision now.

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.

Personally, I'd lean towards removing Command<N>.
Dapps and client code can easily cache programs and do the lookups on their own.
That said, it's probably worth getting the opinions of those who'd use this feature (partners and the Explorer/SDK team).

@vicsn vicsn requested review from Copilot and removed request for kaimast March 23, 2026 17:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a structured RejectedReason that records why a transaction was rejected (with optional locator/index/command context), and gates inclusion on ConsensusVersion::V14 to preserve backwards compatibility.

Changes:

  • Adds RejectedReason + serialization (bytes + human-readable JSON) and wires it into Rejected / ConfirmedTransaction.
  • Threads indexed finalize error context (IndexedFinalizeError) through finalize paths and converts it into RejectedReason (V14+ only).
  • Adds/updates tests to validate rejected-reason behavior pre-/post-V14.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
synthesizer/src/vm/tests/test_v14.rs Adds V14-focused tests asserting rejected reasons are absent pre-V14 and present post-V14 for deploy/execute cases.
synthesizer/src/vm/mod.rs Updates speculation call path (coinbase_reward param) and imports for rejected-reason support.
synthesizer/src/vm/finalize.rs Attaches rejected reasons during speculation (V14+), introduces indexed error propagation, and enhances execution preparation errors.
synthesizer/program/src/logic/command/mod.rs Changes finalize command error type to FinalizeError to support structured finalize error propagation.
synthesizer/process/src/lib.rs Adds helper to fetch latest program edition for error context.
synthesizer/process/src/finalize.rs Propagates IndexedFinalizeError through finalize paths and adds typed atomic batch scopes for structured errors.
synthesizer/error/src/process.rs Introduces IndexedFinalizeError, IntoIndexedFinalize, and indexed_finalize_bail! for location-aware finalize errors.
synthesizer/error/Cargo.toml Adds snarkvm-console-program dependency for program ID / identifier types in errors.
ledger/test-helpers/src/lib.rs Updates rejected tx helpers for the new Rejected constructors (optional reason).
ledger/store/src/helpers/mod.rs Extends atomic_batch_scope! with a typed error variant to preserve structured errors.
ledger/src/tests.rs Updates expected rejected execution construction to include rejected_reason: None.
ledger/src/check_next_block.rs Enforces “no rejected reasons pre-V14” during block checks.
ledger/block/src/transactions/serialize.rs Updates transaction test helpers usage to account for rejected-reason presence flags.
ledger/block/src/transactions/rejected_reason/* Adds new RejectedReason type plus bytes/string/serde implementations and tests.
ledger/block/src/transactions/rejected/* Extends Rejected to carry Option<RejectedReason> and updates (de)serialization for backward compatibility.
ledger/block/src/transactions/mod.rs Exposes rejected_reason module and updates imports accordingly.
ledger/block/src/transactions/confirmed/mod.rs Adds ConfirmedTransaction::rejected_reason() accessor and updates test helpers for rejected reasons.
ledger/block/Cargo.toml Adds dependency on snarkvm-synthesizer-error for IndexedFinalizeError.
Cargo.lock Locks new workspace dependency edges.

Comment on lines +1999 to +2005
// Execute `failing_program.aleo/fail_here` and `call_failing_program/call_fail_here`.
// The transaction is rejected in finalize, but before V14 no rejected reason is attached.
let inputs = [Value::from_str("true").unwrap()].into_iter();
let fail_transaction =
vm.execute(&private_key, ("failing_program.aleo", "fail"), inputs.clone(), None, 0, None, rng).unwrap();
let call_fail_transaction = vm
.execute(&private_key, ("call_failing_program.aleo", "call_fail"), inputs.clone(), None, 0, None, rng)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The comment references fail_here / call_fail_here, but the code executes failing_program.aleo/fail and call_failing_program.aleo/call_fail. Please update the comment to avoid confusion when debugging test failures.

Copilot uses AI. Check for mistakes.
use snarkvm_ledger_committee::{MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE};
#[cfg(feature = "history-staking-rewards")]
use snarkvm_ledger_store::helpers::Map;
use snarkvm_synthesizer_error::{FinalizeError, IndexedFinalizeError, IntoIndexedFinalize, indexed_finalize_bail};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

FinalizeError is imported but never used in this module. This will trigger an unused-import warning (and can fail CI if warnings are denied). Remove the import or use it directly if needed.

Suggested change
use snarkvm_synthesizer_error::{FinalizeError, IndexedFinalizeError, IntoIndexedFinalize, indexed_finalize_bail};
use snarkvm_synthesizer_error::{IndexedFinalizeError, IntoIndexedFinalize, indexed_finalize_bail};

Copilot uses AI. Check for mistakes.
mod tests;

use crate::{Restrictions, Stack, cast_mut_ref, cast_ref, convert, process};
use crate::{Command, Restrictions, Stack, cast_mut_ref, cast_ref, convert, process};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Command is added to the use crate::{...} list but does not appear to be used anywhere in this module. Please remove it to avoid unused-import warnings.

Suggested change
use crate::{Command, Restrictions, Stack, cast_mut_ref, cast_ref, convert, process};
use crate::{Restrictions, Stack, cast_mut_ref, cast_ref, convert, process};

Copilot uses AI. Check for mistakes.
Ratifications,
Ratify,
Rejected,
RejectedReason,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

RejectedReason is imported from snarkvm_ledger_block but does not appear to be used in this file. Please remove it to avoid unused-import warnings.

Suggested change
RejectedReason,

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +38
Identifier,
ProgramID,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

These console::program imports (Identifier, ProgramID) appear unused in this module. Consider removing them to avoid unused-import warnings.

Suggested change
Identifier,
ProgramID,

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +50
use snarkvm_synthesizer_error::IndexedFinalizeError;
use snarkvm_synthesizer_program::{Command, FinalizeOperation};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

These imports (IndexedFinalizeError, Command) appear unused in this module. Consider removing them to avoid unused-import warnings.

Suggested change
use snarkvm_synthesizer_error::IndexedFinalizeError;
use snarkvm_synthesizer_program::{Command, FinalizeOperation};
use snarkvm_synthesizer_program::FinalizeOperation;

Copilot uses AI. Check for mistakes.
assert_eq!(block.transactions().num_rejected(), 1);
assert_eq!(block.aborted_transaction_ids().len(), 0);
let rejected_tx = block.transactions().iter().find(|tx| tx.is_rejected()).unwrap();
// The locator points to the constructor of `failing_constructor.aleo`.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This comment says the locator points to the constructor of failing_constructor.aleo, but the assertions below expect credits.aleo and bond_validator. Please update the comment to match the actual expectation.

Suggested change
// The locator points to the constructor of `failing_constructor.aleo`.
// The locator points to the `bond_validator` transition in `credits.aleo`.

Copilot uses AI. Check for mistakes.
@vicsn vicsn requested a review from kaimast March 23, 2026 17:13
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.

4 participants