Skip to content
10 changes: 10 additions & 0 deletions ledger/store/src/helpers/memory/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::{
use console::{
prelude::*,
program::{Identifier, Plaintext, ProgramID, Value},
types::Field,
};
use snarkvm_ledger_committee::Committee;

Expand All @@ -39,6 +40,8 @@ pub struct FinalizeMemory<N: Network> {
program_id_map: MemoryMap<ProgramID<N>, IndexSet<Identifier<N>>>,
/// The key-value map.
key_value_map: NestedMemoryMap<(ProgramID<N>, Identifier<N>), Plaintext<N>, Value<N>>,
/// The rejection reason map.
rejection_reason_map: MemoryMap<Field<N>, String>,
/// The storage mode.
storage_mode: StorageMode,
}
Expand All @@ -48,6 +51,7 @@ impl<N: Network> FinalizeStorage<N> for FinalizeMemory<N> {
type CommitteeStorage = CommitteeMemory<N>;
type ProgramIDMap = MemoryMap<ProgramID<N>, IndexSet<Identifier<N>>>;
type KeyValueMap = NestedMemoryMap<(ProgramID<N>, Identifier<N>), Plaintext<N>, Value<N>>;
type RejectionReasonMap = MemoryMap<Field<N>, String>;

/// Initializes the finalize storage.
fn open<S: Into<StorageMode>>(storage: S) -> Result<Self> {
Expand All @@ -59,6 +63,7 @@ impl<N: Network> FinalizeStorage<N> for FinalizeMemory<N> {
committee_store,
program_id_map: MemoryMap::default(),
key_value_map: NestedMemoryMap::default(),
rejection_reason_map: MemoryMap::default(),
storage_mode: storage,
})
}
Expand All @@ -78,6 +83,11 @@ impl<N: Network> FinalizeStorage<N> for FinalizeMemory<N> {
&self.key_value_map
}

/// Returns the rejection reason map.
fn rejection_reason_map(&self) -> &Self::RejectionReasonMap {
&self.rejection_reason_map
}

/// Returns the storage mode.
fn storage_mode(&self) -> &StorageMode {
&self.storage_mode
Expand Down
3 changes: 3 additions & 0 deletions ledger/store/src/helpers/rocksdb/internal/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ pub enum TransitionMap {
pub enum ProgramMap {
ProgramID = DataID::ProgramIDMap as u16,
KeyValueID = DataID::KeyValueMap as u16,
RejectionReason = DataID::RejectionReasonMap as u16,
}

/// The RocksDB map prefix for test-related entries.
Expand Down Expand Up @@ -303,6 +304,8 @@ enum DataID {
IDEditionMap,
// Track deployments that contain an optional checksum
DeploymentChecksumMap,
// Track rejection reasons for rejected transactions
RejectionReasonMap,

// Testing
#[cfg(test)]
Expand Down
10 changes: 10 additions & 0 deletions ledger/store/src/helpers/rocksdb/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::{
use console::{
prelude::*,
program::{Identifier, Plaintext, ProgramID, Value},
types::Field,
};
use snarkvm_ledger_committee::Committee;

Expand All @@ -39,6 +40,8 @@ pub struct FinalizeDB<N: Network> {
program_id_map: DataMap<ProgramID<N>, IndexSet<Identifier<N>>>,
/// The key-value map.
key_value_map: NestedDataMap<(ProgramID<N>, Identifier<N>), Plaintext<N>, Value<N>>,
/// The rejection reason map.
rejection_reason_map: DataMap<Field<N>, String>,
/// The storage mode.
storage_mode: StorageMode,
}
Expand All @@ -48,6 +51,7 @@ impl<N: Network> FinalizeStorage<N> for FinalizeDB<N> {
type CommitteeStorage = CommitteeDB<N>;
type ProgramIDMap = DataMap<ProgramID<N>, IndexSet<Identifier<N>>>;
type KeyValueMap = NestedDataMap<(ProgramID<N>, Identifier<N>), Plaintext<N>, Value<N>>;
type RejectionReasonMap = DataMap<Field<N>, String>;

/// Initializes the finalize storage.
fn open<S: Into<StorageMode>>(storage: S) -> Result<Self> {
Expand All @@ -59,6 +63,7 @@ impl<N: Network> FinalizeStorage<N> for FinalizeDB<N> {
committee_store,
program_id_map: rocksdb::RocksDB::open_map(N::ID, storage.clone(), MapID::Program(ProgramMap::ProgramID))?,
key_value_map: rocksdb::RocksDB::open_nested_map(N::ID, storage.clone(), MapID::Program(ProgramMap::KeyValueID))?,
rejection_reason_map: rocksdb::RocksDB::open_map(N::ID, storage.clone(), MapID::Program(ProgramMap::RejectionReason))?,
storage_mode: storage,
})
}
Expand All @@ -78,6 +83,11 @@ impl<N: Network> FinalizeStorage<N> for FinalizeDB<N> {
&self.key_value_map
}

/// Returns the rejection reason map.
fn rejection_reason_map(&self) -> &Self::RejectionReasonMap {
&self.rejection_reason_map
}

/// Returns the storage mode.
fn storage_mode(&self) -> &StorageMode {
&self.storage_mode
Expand Down
33 changes: 32 additions & 1 deletion ledger/store/src/program/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
type ProgramIDMap: for<'a> Map<'a, ProgramID<N>, IndexSet<Identifier<N>>>;
/// The mapping of `(program ID, mapping name)` to `[(key, value)]`.
type KeyValueMap: for<'a> NestedMap<'a, (ProgramID<N>, Identifier<N>), Plaintext<N>, Value<N>>;
/// The mapping of `transaction ID` to `rejection reason`.
type RejectionReasonMap: for<'a> Map<'a, Field<N>, String>;

/// Initializes the program state storage.
fn open<S: Into<StorageMode>>(storage: S) -> Result<Self>;
Expand All @@ -86,6 +88,8 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
fn program_id_map(&self) -> &Self::ProgramIDMap;
/// Returns the key-value map.
fn key_value_map(&self) -> &Self::KeyValueMap;
/// Returns the rejection reason map.
fn rejection_reason_map(&self) -> &Self::RejectionReasonMap;

/// Returns the storage mode.
fn storage_mode(&self) -> &StorageMode;
Expand All @@ -95,48 +99,55 @@ pub trait FinalizeStorage<N: Network>: 'static + Clone + Send + Sync {
self.committee_store().start_atomic();
self.program_id_map().start_atomic();
self.key_value_map().start_atomic();
self.rejection_reason_map().start_atomic();
}

/// Checks if an atomic batch is in progress.
fn is_atomic_in_progress(&self) -> bool {
self.committee_store().is_atomic_in_progress()
|| self.program_id_map().is_atomic_in_progress()
|| self.key_value_map().is_atomic_in_progress()
|| self.rejection_reason_map().is_atomic_in_progress()
}

/// Checkpoints the atomic batch.
fn atomic_checkpoint(&self) {
self.committee_store().atomic_checkpoint();
self.program_id_map().atomic_checkpoint();
self.key_value_map().atomic_checkpoint();
self.rejection_reason_map().atomic_checkpoint();
}

/// Clears the latest atomic batch checkpoint.
fn clear_latest_checkpoint(&self) {
self.committee_store().clear_latest_checkpoint();
self.program_id_map().clear_latest_checkpoint();
self.key_value_map().clear_latest_checkpoint();
self.rejection_reason_map().clear_latest_checkpoint();
}

/// Rewinds the atomic batch to the previous checkpoint.
fn atomic_rewind(&self) {
self.committee_store().atomic_rewind();
self.program_id_map().atomic_rewind();
self.key_value_map().atomic_rewind();
self.rejection_reason_map().atomic_rewind();
}

/// Aborts an atomic batch write operation.
fn abort_atomic(&self) {
self.committee_store().abort_atomic();
self.program_id_map().abort_atomic();
self.key_value_map().abort_atomic();
self.rejection_reason_map().abort_atomic();
}

/// Finishes an atomic batch write operation.
fn finish_atomic(&self) -> Result<()> {
self.committee_store().finish_atomic()?;
self.program_id_map().finish_atomic()?;
self.key_value_map().finish_atomic()
self.key_value_map().finish_atomic()?;
self.rejection_reason_map().finish_atomic()
}

/// Initializes the given `program ID` and `mapping name` in storage.
Expand Down Expand Up @@ -752,6 +763,26 @@ impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
}
}

impl<N: Network, P: FinalizeStorage<N>> FinalizeStore<N, P> {
/// Stores the rejection reason for the given transaction ID.
pub fn insert_rejection_reason(&self, transaction_id: Field<N>, reason: String) -> Result<()> {
self.storage.rejection_reason_map().insert(transaction_id, reason)
}

/// Returns the rejection reason for the given transaction ID.
pub fn get_rejection_reason(&self, transaction_id: &Field<N>) -> Result<Option<String>> {
match self.storage.rejection_reason_map().get_speculative(transaction_id)? {
Some(reason) => Ok(Some(reason.into_owned())),
None => Ok(None),
}
}

/// Returns `true` if a rejection reason exists for the given transaction ID.
pub fn contains_rejection_reason(&self, transaction_id: &Field<N>) -> Result<bool> {
self.storage.rejection_reason_map().contains_key_speculative(transaction_id)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
137 changes: 125 additions & 12 deletions synthesizer/src/vm/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
// Define the closure for processing a rejected deployment.
let process_rejected_deployment =
|fee: &Fee<N>,
deployment: Deployment<N>|
deployment: Deployment<N>,
rejection_reason: String|
-> Result<Result<ConfirmedTransaction<N>, String>> {
process
.finalize_fee(state, store, fee)
Expand All @@ -397,24 +398,35 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
.map(|(fee_tx, finalize)| {
let rejected = Rejected::new_deployment(*program_owner, deployment);
ConfirmedTransaction::rejected_deploy(counter, fee_tx, rejected, finalize)
.and_then(|confirmed_tx| {
// Store the rejection reason.
store.insert_rejection_reason(
*confirmed_tx.id(),
rejection_reason,
).map_err(|e| anyhow!("Failed to store rejection reason: {e}"))?;
Ok(confirmed_tx)
})
.map_err(|e| e.to_string())
})
};

// Check if the program has already been deployed in this block.
match deployments.contains(deployment.program_id()) {
// If the program has already been deployed, construct the rejected deploy transaction.
true => match process_rejected_deployment(fee, *deployment.clone()) {
Ok(result) => result,
Err(error) => {
// Note: On failure, skip this transaction, and continue speculation.
dev_eprintln!("Failed to finalize the fee in a rejected deploy - {error}");
// Store the aborted transaction.
aborted.push((transaction.clone(), error.to_string()));
// Continue to the next transaction.
continue 'outer;
true => {
let rejection_reason = format!("Program {} has already been deployed in this block", deployment.program_id());
match process_rejected_deployment(fee, *deployment.clone(), rejection_reason) {
Ok(result) => result,
Err(error) => {
// Note: On failure, skip this transaction, and continue speculation.
dev_eprintln!("Failed to finalize the fee in a rejected deploy - {error}");
// Store the aborted transaction.
aborted.push((transaction.clone(), error.to_string()));
// Continue to the next transaction.
continue 'outer;
}
}
},
}
// If the program has not yet been deployed, attempt to deploy it.
false => match process.finalize_deployment(state, store, deployment, fee) {
// Construct the accepted deploy transaction.
Expand All @@ -427,7 +439,8 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
// Construct the rejected deploy transaction.
Err(error) => {
trace!("Failed to finalize deploy tx {} - {error}", transaction.id());
match process_rejected_deployment(fee, *deployment.clone()) {
let rejection_reason = format!("Failed to finalize deployment: {error}");
match process_rejected_deployment(fee, *deployment.clone(), rejection_reason) {
Ok(result) => result,
Err(error) => {
// Note: On failure, skip this transaction, and continue speculation.
Expand Down Expand Up @@ -457,6 +470,7 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
// Construct the rejected execute transaction.
Err(error) => {
trace!("Failed to finalize execute tx {} - {error}", transaction.id());
let rejection_reason = format!("Failed to finalize execution: {error}");
match fee {
// Finalize the fee, to ensure it is valid.
Some(fee) => {
Expand All @@ -470,6 +484,14 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
ConfirmedTransaction::rejected_execute(
counter, fee_tx, rejected, finalize,
)
.and_then(|confirmed_tx| {
// Store the rejection reason.
store.insert_rejection_reason(
*confirmed_tx.id(),
rejection_reason,
).map_err(|e| anyhow!("Failed to store rejection reason: {e}"))?;
Ok(confirmed_tx)
})
.map_err(|e| e.to_string())
}
Err(error) => {
Expand Down Expand Up @@ -3624,4 +3646,95 @@ finalize compute:
assert!(expected_withdraw.contains(entry));
}
}

#[test]
fn test_rejection_reason_storage() {
let rng = &mut TestRng::default();

// Sample a private key.
let private_key = test_helpers::sample_genesis_private_key(rng);
let address = Address::try_from(&private_key).unwrap();

// Initialize the vm.
let vm = test_helpers::sample_vm_with_genesis_block(rng);

// Deploy a new program.
let genesis =
vm.block_store().get_block(&vm.block_store().get_block_hash(0).unwrap().unwrap()).unwrap().unwrap();

// Get the unspent records.
let mut unspent_records = genesis
.transitions()
.cloned()
.flat_map(Transition::into_records)
.map(|(_, record)| record)
.collect::<Vec<_>>();

// Construct the deployment block.
let deployment_block = {
let program = Program::<CurrentNetwork>::from_str(
"
program testing.aleo;

mapping entries:
key as address.public;
value as u8.public;

function compute:
input r0 as u8.public;
async compute self.caller r0 into r1;
output r1 as testing.aleo/compute.future;

finalize compute:
input r0 as address.public;
input r1 as u8.public;
get.or_use entries[r0] r1 into r2;
add r1 r2 into r3;
set r3 into entries[r0];
get entries[r0] into r4;
add r4 r1 into r5;
set r5 into entries[r0];
",
)
.unwrap();

// Prepare the additional fee.
let view_key = ViewKey::<CurrentNetwork>::try_from(private_key).unwrap();
let credits = Some(unspent_records.pop().unwrap().decrypt(&view_key).unwrap());

// Deploy.
let transaction = vm.deploy(&private_key, &program, credits, 10, None, rng).unwrap();

// Construct the new block.
sample_next_block(&vm, &private_key, &[transaction], &genesis, &mut unspent_records, rng).unwrap()
};

// Add the deployment block to the VM.
vm.add_next_block(&deployment_block).unwrap();

// Create an execution transaction, that will be rejected.
let r0 = Value::<CurrentNetwork>::from_str("100u8").unwrap();
let rejected_tx =
create_execution(&vm, private_key, "testing.aleo", "compute", vec![r0], &mut unspent_records, rng);

// Construct the next block with the rejected transaction.
let next_block =
sample_next_block(&vm, &private_key, &[rejected_tx], &deployment_block, &mut unspent_records, rng)
.unwrap();

// Check that the transaction was rejected.
assert_eq!(next_block.transactions().len(), 1);
let rejected_transaction = next_block.transactions().iter().next().unwrap();
assert!(rejected_transaction.is_rejected());

// Add the next block to the VM.
vm.add_next_block(&next_block).unwrap();

// Check that the rejection reason was stored.
let tx_id = *rejected_transaction.id();
let rejection_reason = vm.finalize_store().get_rejection_reason(&tx_id).unwrap();
assert!(rejection_reason.is_some(), "Rejection reason should be stored");
let reason_str = rejection_reason.unwrap();
assert!(reason_str.contains("Failed to finalize execution"), "Rejection reason should contain error message");
}
}