Skip to content

Commit e97009d

Browse files
authored
Merge pull request #3214 from ProvableHQ/feat/rejection-reason-2
[Feat] Tx rejection reasons (take 3)
2 parents 60322dd + d8986c1 commit e97009d

28 files changed

Lines changed: 1188 additions & 189 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

console/network/src/consensus_heights.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub enum ConsensusVersion {
5656
V14 = 14,
5757
/// V15: Introduces the record-existence check and `commit.*.raw` instruction variants.
5858
/// Increase the anchor time to 35.
59+
/// Unconditionally stores transaction rejection reasons.
5960
V15 = 15,
6061
}
6162

ledger/block/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ workspace = true
6161
[dependencies.snarkvm-ledger-puzzle]
6262
workspace = true
6363

64+
[dependencies.snarkvm-synthesizer-error]
65+
workspace = true
66+
6467
[dependencies.snarkvm-synthesizer-program]
6568
workspace = true
6669

ledger/block/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ impl<N: Network> Block<N> {
145145

146146
/// Initializes a new block from the given previous block hash, block header, authority,
147147
/// ratifications, solutions, aborted solution IDs, transactions, and aborted transaction IDs.
148-
pub fn from(
148+
fn from(
149149
previous_hash: N::BlockHash,
150150
header: Header<N>,
151151
authority: Authority<N>,

ledger/block/src/transactions/confirmed/mod.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -473,18 +473,22 @@ pub mod test_helpers {
473473
sample_rejected_deploy(2, 2, Uniform::rand(rng), false, true, rng),
474474
sample_rejected_deploy(2, 2, Uniform::rand(rng), true, true, rng),
475475
sample_rejected_execute(3, true, rng),
476+
sample_rejected_execute(3, true, rng),
477+
sample_rejected_execute(3, false, rng),
476478
sample_rejected_execute(3, false, rng),
477479
sample_accepted_execute(Uniform::rand(rng), true, rng),
478480
sample_accepted_execute(Uniform::rand(rng), false, rng),
479-
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, true, rng),
480-
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, false, rng),
481-
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, true, rng),
482-
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, false, rng),
483-
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, true, rng),
484-
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, false, rng),
485-
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, true, rng),
486-
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, false, rng),
481+
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, Uniform::rand(rng), rng),
482+
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, Uniform::rand(rng), rng),
483+
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, Uniform::rand(rng), rng),
484+
sample_rejected_deploy(Uniform::rand(rng), 1, Uniform::rand(rng), false, Uniform::rand(rng), rng),
485+
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, Uniform::rand(rng), rng),
486+
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, Uniform::rand(rng), rng),
487+
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, Uniform::rand(rng), rng),
488+
sample_rejected_deploy(Uniform::rand(rng), 2, Uniform::rand(rng), false, Uniform::rand(rng), rng),
487489
sample_rejected_execute(Uniform::rand(rng), true, rng),
490+
sample_rejected_execute(Uniform::rand(rng), true, rng),
491+
sample_rejected_execute(Uniform::rand(rng), false, rng),
488492
sample_rejected_execute(Uniform::rand(rng), false, rng),
489493
]
490494
}

ledger/block/src/transactions/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub use confirmed::*;
1919
pub mod rejected;
2020
pub use rejected::*;
2121

22+
pub mod rejected_reason;
23+
pub use rejected_reason::*;
24+
2225
mod bytes;
2326
mod merkle;
2427
mod serialize;
@@ -31,6 +34,8 @@ use console::{
3134
Ciphertext,
3235
FINALIZE_ID_DEPTH,
3336
FINALIZE_OPERATIONS_DEPTH,
37+
Identifier,
38+
ProgramID,
3439
ProgramOwner,
3540
Record,
3641
TRANSACTIONS_DEPTH,
@@ -41,7 +46,8 @@ use console::{
4146
};
4247
use snarkvm_ledger_committee::Committee;
4348
use snarkvm_ledger_narwhal_batch_header::BatchHeader;
44-
use snarkvm_synthesizer_program::FinalizeOperation;
49+
use snarkvm_synthesizer_error::IndexedFinalizeError;
50+
use snarkvm_synthesizer_program::{Command, FinalizeOperation};
4551

4652
use indexmap::IndexMap;
4753

ledger/block/src/transactions/rejected/mod.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,23 @@ impl<N: Network> Rejected<N> {
5252
/// Returns the program owner of the rejected deployment.
5353
pub fn program_owner(&self) -> Option<&ProgramOwner<N>> {
5454
match self {
55-
Self::Deployment(program_owner, _) => Some(program_owner),
56-
Self::Execution(_) => None,
55+
Self::Deployment(program_owner, ..) => Some(program_owner),
56+
Self::Execution(..) => None,
5757
}
5858
}
5959

6060
/// Returns the rejected deployment.
6161
pub fn deployment(&self) -> Option<&Deployment<N>> {
6262
match self {
6363
Self::Deployment(_, deployment) => Some(deployment),
64-
Self::Execution(_) => None,
64+
Self::Execution(..) => None,
6565
}
6666
}
6767

6868
/// Returns the rejected execution.
6969
pub fn execution(&self) -> Option<&Execution<N>> {
7070
match self {
71-
Self::Deployment(_, _) => None,
71+
Self::Deployment(..) => None,
7272
Self::Execution(execution) => Some(execution),
7373
}
7474
}
@@ -148,17 +148,27 @@ pub mod test_helpers {
148148
pub(crate) fn sample_rejected_transactions() -> Vec<Rejected<CurrentNetwork>> {
149149
let rng = &mut TestRng::default();
150150

151-
vec![
152-
sample_rejected_deployment(1, 0, false, false, rng),
153-
sample_rejected_deployment(1, 0, false, true, rng),
154-
sample_rejected_deployment(2, 0, false, false, rng),
155-
sample_rejected_deployment(2, 0, false, true, rng),
156-
sample_rejected_deployment(1, 1, false, false, rng),
157-
sample_rejected_deployment(1, 1, false, true, rng),
158-
sample_rejected_deployment(2, 1, true, false, rng),
159-
sample_rejected_deployment(2, 1, true, true, rng),
160-
sample_rejected_execution(true, rng),
161-
sample_rejected_execution(false, rng),
162-
]
151+
let mut txs = Vec::new();
152+
153+
// Sample the deployments.
154+
for version in 1..=2 {
155+
for edition in 0..=1 {
156+
for has_translation_keys in [true, false] {
157+
for is_fee_private in [true, false] {
158+
let tx =
159+
sample_rejected_deployment(version, edition, has_translation_keys, is_fee_private, rng);
160+
txs.push(tx);
161+
}
162+
}
163+
}
164+
}
165+
166+
// Sample the executions.
167+
for is_fee_private in [true, false] {
168+
let tx = sample_rejected_execution(is_fee_private, rng);
169+
txs.push(tx);
170+
}
171+
172+
txs
163173
}
164174
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) 2019-2026 Provable Inc.
2+
// This file is part of the snarkVM library.
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at:
7+
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use super::*;
17+
18+
impl<N: Network> FromBytes for RejectedReason<N> {
19+
/// Reads the rejected reason from a buffer.
20+
fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
21+
// Read the variant.
22+
let variant = u8::read_le(&mut reader)?;
23+
match variant {
24+
0 => {
25+
let program_id = ProgramID::<N>::read_le(&mut reader)?;
26+
Ok(Self::DuplicateProgramID(program_id))
27+
}
28+
1 => {
29+
let program_id = ProgramID::<N>::read_le(&mut reader)?;
30+
let edition = u16::read_le(&mut reader)?;
31+
let resource = Identifier::<N>::read_le(&mut reader)?;
32+
let index = u32::read_le(&mut reader)? as usize;
33+
let command = Command::<N>::read_le(&mut reader)?;
34+
Ok(Self::Finalize { program_id, edition, resource, index, command: Box::new(command) })
35+
}
36+
2 => {
37+
// Read the optional program ID and edition.
38+
let program_id = match u8::read_le(&mut reader)? {
39+
0 => None,
40+
1 => {
41+
let id = ProgramID::<N>::read_le(&mut reader)?;
42+
let edition = u16::read_le(&mut reader)?;
43+
Some((id, edition))
44+
}
45+
flag => return Err(error(format!("Invalid program_id presence flag {flag}"))),
46+
};
47+
// Read the optional resource.
48+
let resource = match u8::read_le(&mut reader)? {
49+
0 => None,
50+
1 => Some(Identifier::<N>::read_le(&mut reader)?),
51+
flag => return Err(error(format!("Invalid resource presence flag {flag}"))),
52+
};
53+
Ok(Self::VM(program_id, resource))
54+
}
55+
3.. => Err(error(format!("Failed to decode rejected reason variant {variant}"))),
56+
}
57+
}
58+
}
59+
60+
impl<N: Network> ToBytes for RejectedReason<N> {
61+
/// Writes the rejected reason to a buffer.
62+
fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
63+
match self {
64+
Self::DuplicateProgramID(program_id) => {
65+
0u8.write_le(&mut writer)?;
66+
program_id.write_le(&mut writer)
67+
}
68+
Self::Finalize { program_id, edition, resource, index, command } => {
69+
1u8.write_le(&mut writer)?;
70+
program_id.write_le(&mut writer)?;
71+
edition.write_le(&mut writer)?;
72+
resource.write_le(&mut writer)?;
73+
u32::try_from(*index).map_err(|_| error("Command index exceeds u32::MAX"))?.write_le(&mut writer)?;
74+
command.write_le(&mut writer)
75+
}
76+
Self::VM(program_id, resource) => {
77+
2u8.write_le(&mut writer)?;
78+
// Write the optional program ID and edition.
79+
match program_id {
80+
None => 0u8.write_le(&mut writer)?,
81+
Some((id, edition)) => {
82+
1u8.write_le(&mut writer)?;
83+
id.write_le(&mut writer)?;
84+
edition.write_le(&mut writer)?;
85+
}
86+
}
87+
// Write the optional resource.
88+
match resource {
89+
None => 0u8.write_le(&mut writer),
90+
Some(resource) => {
91+
1u8.write_le(&mut writer)?;
92+
resource.write_le(&mut writer)
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use super::*;
103+
104+
type CurrentNetwork = console::network::MainnetV0;
105+
106+
#[test]
107+
fn test_bytes() {
108+
for expected in test_helpers::sample_rejected_reasons::<CurrentNetwork>() {
109+
// Check the byte representation.
110+
let expected_bytes = expected.to_bytes_le().unwrap();
111+
assert_eq!(expected, RejectedReason::read_le(&expected_bytes[..]).unwrap());
112+
}
113+
}
114+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) 2019-2026 Provable Inc.
2+
// This file is part of the snarkVM library.
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at:
7+
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
mod bytes;
17+
mod serialize;
18+
mod string;
19+
20+
use super::*;
21+
22+
/// The reason a transaction was rejected.
23+
#[derive(Clone, PartialEq, Eq)]
24+
pub enum RejectedReason<N: Network> {
25+
/// The transaction was rejected due to a duplicate program ID deployment in the same block.
26+
DuplicateProgramID(ProgramID<N>),
27+
28+
/// The transaction was rejected due to a failed finalize command. (program ID, edition, resource, index, command).
29+
/// Note: We do not log the actual error message from the finalize command, as it may contain
30+
/// sensitive information or lead to DOS vectors by storing string representations of large structs.
31+
Finalize { program_id: ProgramID<N>, edition: u16, resource: Identifier<N>, index: usize, command: Box<Command<N>> },
32+
33+
/// The transaction was rejected due to a VM error not captured by a finalize command.
34+
/// The programID and resource are logged if they are available.
35+
VM(Option<(ProgramID<N>, u16)>, Option<Identifier<N>>),
36+
}
37+
38+
impl<N: Network> RejectedReason<N> {
39+
/// Initializes the rejected reason from an indexed finalize error.
40+
///
41+
/// `C` may be any type whose `Display` output is a valid `Command<N>` string (e.g. `Command<N>`
42+
/// itself or `String`). If the command string cannot be re-parsed, the reason falls back to
43+
/// `VM` so that a bad string never causes a panic in consensus code.
44+
pub fn from_indexed_finalize_error<C: ToString>(error: IndexedFinalizeError<N, C>) -> Self {
45+
let program_id = error.program_id;
46+
let resource = error.resource;
47+
match error.command.map(|b| *b) {
48+
Some((index, command)) => {
49+
// Parse the command from its display string. Falls back to VM on failure.
50+
match (program_id, resource, command.to_string().parse::<Command<N>>()) {
51+
(Some((program_id, edition)), Some(resource), Ok(command)) => {
52+
Self::Finalize { program_id, edition, resource, index, command: Box::new(command) }
53+
}
54+
(program_id, resource, _) => Self::VM(program_id, resource),
55+
}
56+
}
57+
None => Self::VM(program_id, resource),
58+
}
59+
}
60+
}
61+
62+
#[cfg(test)]
63+
pub mod test_helpers {
64+
use super::*;
65+
use std::str::FromStr;
66+
67+
/// Returns one instance of each `RejectedReason` variant for testing.
68+
pub(crate) fn sample_rejected_reasons<N: Network>() -> Vec<RejectedReason<N>> {
69+
let program = ProgramID::<N>::from_str("dummy_program.aleo").unwrap();
70+
let credits = ProgramID::<N>::from_str("credits.aleo").unwrap();
71+
let transfer = Identifier::<N>::from_str("transfer_public").unwrap();
72+
let bond = Identifier::<N>::from_str("bond_public").unwrap();
73+
let command = Command::<N>::from_str("assert.eq r0 r1;").unwrap();
74+
vec![
75+
RejectedReason::DuplicateProgramID(program),
76+
RejectedReason::Finalize {
77+
program_id: credits,
78+
edition: 1,
79+
resource: transfer,
80+
index: 3,
81+
command: Box::new(command),
82+
},
83+
RejectedReason::VM(Some((credits, 0u16)), Some(bond)),
84+
RejectedReason::VM(None, Some(bond)),
85+
RejectedReason::VM(Some((credits, 0u16)), None),
86+
RejectedReason::VM(None, None),
87+
]
88+
}
89+
}

0 commit comments

Comments
 (0)