From 5a87ddcb1de20dd7839bb8b1e73f6060382a334c Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 28 Dec 2023 17:02:26 +0100 Subject: [PATCH 01/26] Removes panics from masp vp --- shared/src/ledger/native_vp/masp.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index 31ee79848b..e6d244a6c7 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -60,11 +60,15 @@ fn asset_type_from_epoched_address( epoch: Epoch, token: &Address, denom: token::MaspDenom, -) -> AssetType { +) -> Result { // Timestamp the chosen token with the current epoch let token_bytes = (token, denom, epoch.0).serialize_to_vec(); // Generate the unique asset identifier from the unique token address - AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") + AssetType::new(token_bytes.as_ref()).map_err(|()| { + Error::NativeVpError(native_vp::Error::SimpleMessage( + "Unable to create asset type", + )) + }) } /// Checks if the reported transparent amount and the unshielded @@ -91,12 +95,16 @@ fn convert_amount( token: &Address, val: token::Amount, denom: token::MaspDenom, -) -> I128Sum { - let asset_type = asset_type_from_epoched_address(epoch, token, denom); +) -> Result { + let asset_type = asset_type_from_epoched_address(epoch, token, denom)?; // Combine the value and unit into one amount I128Sum::from_nonnegative(asset_type, denom.denominate(&val) as i128) - .expect("invalid value or asset type for amount") + .map_err(|()| { + Error::NativeVpError(native_vp::Error::SimpleMessage( + "Invalid value for amount", + )) + }) } impl<'a, DB, H, CA> MaspVp<'a, DB, H, CA> @@ -406,7 +414,7 @@ where &transfer.token, transfer_amount, denom, - ); + )?; // Non-masp sources add to transparent tx pool transparent_tx_pool += transp_amt; @@ -518,7 +526,7 @@ where &transfer.token, transfer_amount, denom, - ); + )?; // Non-masp destinations subtract from transparent tx pool transparent_tx_pool -= transp_amt; From d9ec171262ee326b8a0b400383555e643086c41a Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 28 Dec 2023 19:01:13 +0100 Subject: [PATCH 02/26] Helper functions for masp keys --- .../lib/node/ledger/shell/finalize_block.rs | 265 +++++++----------- apps/src/lib/node/ledger/shell/init_chain.rs | 27 +- benches/native_vps.rs | 19 +- core/src/ledger/masp_conversions.rs | 7 +- core/src/ledger/masp_utils.rs | 32 +-- core/src/types/token.rs | 55 ++++ shared/src/ledger/native_vp/masp.rs | 49 ++-- 7 files changed, 199 insertions(+), 255 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 0e46577a0d..4fff595bad 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -19,12 +19,8 @@ use namada::proof_of_stake::storage::{ find_validator_by_raw_hash, read_last_block_proposer_address, write_last_block_proposer_address, }; -use namada::types::address::MASP; use namada::types::key::tm_raw_hash_to_string; use namada::types::storage::{BlockHash, BlockResults, Epoch, Header}; -use namada::types::token::{ - MASP_NOTE_COMMITMENT_ANCHOR_PREFIX, MASP_NOTE_COMMITMENT_TREE_KEY, -}; use namada::types::transaction::protocol::{ ethereum_tx_data_variants, ProtocolTxType, }; @@ -567,21 +563,16 @@ where tracing::info!("{}", stats.format_tx_executed()); // Update the MASP commitment tree anchor if the tree was updated - let tree_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let tree_key = namada::core::types::token::masp_commitment_tree_key(); if let Some(StorageModification::Write { value }) = self.wl_storage.write_log.read(&tree_key).0 { let updated_tree = CommitmentTree::::try_from_slice(value) .into_storage_result()?; - let anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") - .push(&namada::core::types::hash::Hash( - bls12_381::Scalar::from(updated_tree.root()).to_bytes(), - )) - .expect("Cannot obtain a storage key"); + let anchor_key = + namada::core::types::token::masp_commitment_anchor_key( + &bls12_381::Scalar::from(updated_tree.root()), + ); self.wl_storage.write(&anchor_key, ())?; } @@ -1874,11 +1865,9 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.to_vec(), vec![], None); - assert!( - rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); // FINALIZE BLOCK 2. Tell Namada that val1 is the block proposer. // Include votes that correspond to block 1. Make val2 the next block's @@ -1893,11 +1882,9 @@ mod test_finalize_block { assert!(rewards_prod_2.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_4.is_empty(&shell.wl_storage).unwrap()); - assert!( - !rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(!rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); // Val1 was the proposer, so its reward should be larger than all // others, which should themselves all be equal let acc_sum = get_rewards_sum(&shell.wl_storage); @@ -2029,11 +2016,9 @@ mod test_finalize_block { None, ); } - assert!( - rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); let rp1 = rewards_prod_1 .get(&shell.wl_storage, &Epoch::default()) .unwrap() @@ -2121,11 +2106,9 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.to_vec(), vec![], None); - assert!( - rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); let (current_epoch, inflation) = advance_epoch(&mut shell, &pkh1, &votes, None); @@ -2373,11 +2356,9 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); - assert!( - rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); // Make an account with balance and delegate some tokens let delegator = address::testing::gen_implicit_address(); @@ -2544,11 +2525,9 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); - assert!( - rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap()); // Check that there's 3 unique consensus keys let consensus_keys = @@ -2856,25 +2835,21 @@ mod test_finalize_block { assert_eq!(root_pre.0, root_post.0); // Check transaction's hash in storage - assert!( - shell - .shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper_tx.header_hash()) - .unwrap_or_default() - ); + assert!(shell + .shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper_tx.header_hash()) + .unwrap_or_default()); // Check that the hash is present in the merkle tree - assert!( - !shell - .shell - .wl_storage - .storage - .block - .tree - .has_key(&wrapper_hash_key) - .unwrap() - ); + assert!(!shell + .shell + .wl_storage + .storage + .block + .tree + .has_key(&wrapper_hash_key) + .unwrap()); } /// Test that a decrypted tx that has already been applied in the same block @@ -2979,20 +2954,16 @@ mod test_finalize_block { assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); for (inner, wrapper) in [(inner, wrapper), (new_inner, new_wrapper)] { - assert!( - shell - .wl_storage - .write_log - .has_replay_protection_entry(&inner.raw_header_hash()) - .unwrap_or_default() - ); - assert!( - !shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper.header_hash()) - .unwrap_or_default() - ); + assert!(shell + .wl_storage + .write_log + .has_replay_protection_entry(&inner.raw_header_hash()) + .unwrap_or_default()); + assert!(!shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper.header_hash()) + .unwrap_or_default()); } } @@ -3149,37 +3120,27 @@ mod test_finalize_block { (unsigned_inner, unsigned_wrapper), (wrong_commitment_inner, wrong_commitment_wrapper), ] { - assert!( - !shell - .wl_storage - .write_log - .has_replay_protection_entry( - &invalid_inner.raw_header_hash() - ) - .unwrap_or_default() - ); - assert!( - shell - .wl_storage - .storage - .has_replay_protection_entry(&valid_wrapper.header_hash()) - .unwrap_or_default() - ); - } - assert!( - shell + assert!(!shell .wl_storage .write_log - .has_replay_protection_entry(&failing_inner.raw_header_hash()) - .expect("test failed") - ); - assert!( - !shell + .has_replay_protection_entry(&invalid_inner.raw_header_hash()) + .unwrap_or_default()); + assert!(shell .wl_storage - .write_log - .has_replay_protection_entry(&failing_wrapper.header_hash()) - .unwrap_or_default() - ); + .storage + .has_replay_protection_entry(&valid_wrapper.header_hash()) + .unwrap_or_default()); + } + assert!(shell + .wl_storage + .write_log + .has_replay_protection_entry(&failing_inner.raw_header_hash()) + .expect("test failed")); + assert!(!shell + .wl_storage + .write_log + .has_replay_protection_entry(&failing_wrapper.header_hash()) + .unwrap_or_default()); } #[test] @@ -3245,20 +3206,16 @@ mod test_finalize_block { .as_str(); assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); - assert!( - shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper_hash) - .unwrap_or_default() - ); - assert!( - !shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper.raw_header_hash()) - .unwrap_or_default() - ); + assert!(shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper_hash) + .unwrap_or_default()); + assert!(!shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper.raw_header_hash()) + .unwrap_or_default()); } // Test that if the fee payer doesn't have enough funds for fee payment the @@ -3557,11 +3514,9 @@ mod test_finalize_block { .unwrap(), Some(ValidatorState::Consensus) ); - assert!( - enqueued_slashes_handle() - .at(&Epoch::default()) - .is_empty(&shell.wl_storage)? - ); + assert!(enqueued_slashes_handle() + .at(&Epoch::default()) + .is_empty(&shell.wl_storage)?); assert_eq!( get_num_consensus_validators(&shell.wl_storage, Epoch::default()) .unwrap(), @@ -3580,21 +3535,17 @@ mod test_finalize_block { .unwrap(), Some(ValidatorState::Jailed) ); - assert!( - enqueued_slashes_handle() - .at(&epoch) - .is_empty(&shell.wl_storage)? - ); + assert!(enqueued_slashes_handle() + .at(&epoch) + .is_empty(&shell.wl_storage)?); assert_eq!( get_num_consensus_validators(&shell.wl_storage, epoch).unwrap(), 5_u64 ); } - assert!( - !enqueued_slashes_handle() - .at(&processing_epoch) - .is_empty(&shell.wl_storage)? - ); + assert!(!enqueued_slashes_handle() + .at(&processing_epoch) + .is_empty(&shell.wl_storage)?); // Advance to the processing epoch loop { @@ -3617,11 +3568,9 @@ mod test_finalize_block { // println!("Reached processing epoch"); break; } else { - assert!( - enqueued_slashes_handle() - .at(&shell.wl_storage.storage.block.epoch) - .is_empty(&shell.wl_storage)? - ); + assert!(enqueued_slashes_handle() + .at(&shell.wl_storage.storage.block.epoch) + .is_empty(&shell.wl_storage)?); let stake1 = read_validator_stake( &shell.wl_storage, ¶ms, @@ -4115,13 +4064,11 @@ mod test_finalize_block { ) .unwrap(); assert_eq!(last_slash, Some(misbehavior_epoch)); - assert!( - namada_proof_of_stake::storage::validator_slashes_handle( - &val1.address - ) - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap()); tracing::debug!("Advancing to epoch 7"); @@ -4185,22 +4132,18 @@ mod test_finalize_block { ) .unwrap(); assert_eq!(last_slash, Some(Epoch(4))); - assert!( - namada_proof_of_stake::is_validator_frozen( - &shell.wl_storage, - &val1.address, - current_epoch, - ¶ms - ) - .unwrap() - ); - assert!( - namada_proof_of_stake::storage::validator_slashes_handle( - &val1.address - ) - .is_empty(&shell.wl_storage) - .unwrap() - ); + assert!(namada_proof_of_stake::is_validator_frozen( + &shell.wl_storage, + &val1.address, + current_epoch, + ¶ms + ) + .unwrap()); + assert!(namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap()); let pre_stake_10 = namada_proof_of_stake::storage::read_validator_stake( diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 074fe132ad..1f08287f1e 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -13,14 +13,10 @@ use namada::ledger::storage_api::token::{credit_tokens, write_denom}; use namada::ledger::storage_api::StorageWrite; use namada::ledger::{ibc, pos}; use namada::proof_of_stake::BecomeValidator; -use namada::types::address::{Address, MASP}; +use namada::types::address::Address; use namada::types::hash::Hash as CodeHash; use namada::types::key::*; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; -use namada::types::token::{ - MASP_CONVERT_ANCHOR_KEY, MASP_NOTE_COMMITMENT_ANCHOR_PREFIX, - MASP_NOTE_COMMITMENT_TREE_KEY, -}; use namada::vm::validate_untrusted_wasm; use namada_sdk::eth_bridge::EthBridgeStatus; use namada_sdk::proof_of_stake::PosParams; @@ -145,27 +141,22 @@ where let empty_commitment_tree: CommitmentTree = CommitmentTree::empty(); let anchor = empty_commitment_tree.root(); - let note_commitment_tree_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let note_commitment_tree_key = + namada::core::types::token::masp_commitment_tree_key(); self.wl_storage .write(¬e_commitment_tree_key, empty_commitment_tree) .unwrap(); - let commitment_tree_anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") - .push(&namada::core::types::hash::Hash( - bls12_381::Scalar::from(anchor).to_bytes(), - )) - .expect("Cannot obtain a storage key"); + let commitment_tree_anchor_key = + namada::core::types::token::masp_commitment_anchor_key( + &bls12_381::Scalar::from(anchor), + ); self.wl_storage .write(&commitment_tree_anchor_key, ()) .unwrap(); // Init masp convert anchor - let convert_anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_CONVERT_ANCHOR_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let convert_anchor_key = + namada::core::types::token::masp_convert_anchor_key(); self.wl_storage.write( &convert_anchor_key, namada::core::types::hash::Hash( diff --git a/benches/native_vps.rs b/benches/native_vps.rs index a1cf7e42f1..0e71e37690 100644 --- a/benches/native_vps.rs +++ b/benches/native_vps.rs @@ -46,13 +46,10 @@ use namada::namada_sdk::masp_primitives::transaction::Transaction; use namada::proof_of_stake; use namada::proof_of_stake::KeySeg; use namada::proto::{Code, Section, Tx}; -use namada::types::address::{InternalAddress, MASP}; +use namada::types::address::InternalAddress; use namada::types::eth_bridge_pool::{GasFee, PendingTransfer}; use namada::types::masp::{TransferSource, TransferTarget}; use namada::types::storage::{Epoch, TxIndex}; -use namada::types::token::{ - MASP_NOTE_COMMITMENT_ANCHOR_PREFIX, MASP_NOTE_COMMITMENT_TREE_KEY, -}; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; @@ -511,22 +508,16 @@ fn setup_storage_for_masp_verification( shielded_ctx.shell.wl_storage.commit_tx(); // Update the anchor in storage - let tree_key = namada::core::types::storage::Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let tree_key = namada::core::types::token::masp_commitment_tree_key(); let updated_tree: CommitmentTree = shielded_ctx .shell .wl_storage .read(&tree_key) .unwrap() .unwrap(); - let anchor_key = namada::core::types::storage::Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") - .push(&namada::core::types::hash::Hash( - bls12_381::Scalar::from(updated_tree.root()).to_bytes(), - )) - .expect("Cannot obtain a storage key"); + let anchor_key = namada::core::types::token::masp_commitment_anchor_key( + &bls12_381::Scalar::from(updated_tree.root()), + ); shielded_ctx .shell .wl_storage diff --git a/core/src/ledger/masp_conversions.rs b/core/src/ledger/masp_conversions.rs index fc98c6ff65..fa43ca93b1 100644 --- a/core/src/ledger/masp_conversions.rs +++ b/core/src/ledger/masp_conversions.rs @@ -212,8 +212,8 @@ where }; use rayon::prelude::ParallelSlice; + use crate::types::address; use crate::types::storage::{Key, KeySeg}; - use crate::types::token::MASP_CONVERT_ANCHOR_KEY; // The derived conversions will be placed in MASP address space let masp_addr = MASP; @@ -447,11 +447,8 @@ where wl_storage.storage.conversion_state.tree = FrozenCommitmentTree::merge(&tree_parts); // Update the anchor in storage - let anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_CONVERT_ANCHOR_KEY.to_owned()) - .expect("Cannot obtain a storage key"); wl_storage.write( - &anchor_key, + &crate::types::token::masp_convert_anchor_key(), crate::types::hash::Hash( bls12_381::Scalar::from( wl_storage.storage.conversion_state.tree.root(), diff --git a/core/src/ledger/masp_utils.rs b/core/src/ledger/masp_utils.rs index f37b725531..d2d2b9a65c 100644 --- a/core/src/ledger/masp_utils.rs +++ b/core/src/ledger/masp_utils.rs @@ -6,12 +6,10 @@ use masp_primitives::transaction::Transaction; use super::storage_api::{StorageRead, StorageWrite}; use crate::ledger::storage_api::{Error, Result}; -use crate::types::address::MASP; -use crate::types::hash::Hash; -use crate::types::storage::{BlockHeight, Epoch, Key, KeySeg, TxIndex}; +use crate::types::storage::{BlockHeight, Epoch, TxIndex}; use crate::types::token::{ - Transfer, HEAD_TX_KEY, MASP_NOTE_COMMITMENT_TREE_KEY, MASP_NULLIFIERS_KEY, - PIN_KEY_PREFIX, TX_KEY_PREFIX, + masp_commitment_tree_key, masp_head_tx_key, masp_nullifier_key, + masp_pin_tx_key, masp_tx_key, Transfer, }; // Writes the nullifiers of the provided masp transaction to storage @@ -23,12 +21,7 @@ fn reveal_nullifiers( .sapling_bundle() .map_or(&vec![], |description| &description.shielded_spends) { - let nullifier_key = Key::from(MASP.to_db_key()) - .push(&MASP_NULLIFIERS_KEY.to_owned()) - .expect("Cannot obtain a storage key") - .push(&Hash(description.nullifier.0)) - .expect("Cannot obtain a storage key"); - ctx.write(&nullifier_key, ())?; + ctx.write(&masp_nullifier_key(&description.nullifier), ())?; } Ok(()) @@ -44,9 +37,7 @@ pub fn update_note_commitment_tree( ) -> Result<()> { if let Some(bundle) = transaction.sapling_bundle() { if !bundle.shielded_outputs.is_empty() { - let tree_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let tree_key = masp_commitment_tree_key(); let mut commitment_tree: CommitmentTree = ctx.read(&tree_key)?.ok_or(Error::SimpleMessage( "Missing note commitment tree in storage", @@ -74,15 +65,10 @@ pub fn handle_masp_tx( transfer: &Transfer, shielded: &Transaction, ) -> Result<()> { - let masp_addr = MASP; - let head_tx_key = Key::from(masp_addr.to_db_key()) - .push(&HEAD_TX_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let head_tx_key = masp_head_tx_key(); let current_tx_idx: u64 = ctx.read(&head_tx_key).unwrap_or(None).unwrap_or(0); - let current_tx_key = Key::from(masp_addr.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + ¤t_tx_idx.to_string())) - .expect("Cannot obtain a storage key"); + let current_tx_key = masp_tx_key(current_tx_idx); // Save the Transfer object and its location within the blockchain // so that clients do not have to separately look these // up @@ -103,9 +89,7 @@ pub fn handle_masp_tx( // If storage key has been supplied, then pin this transaction to it if let Some(key) = &transfer.key { - let pin_key = Key::from(masp_addr.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + key)) - .expect("Cannot obtain a storage key"); + let pin_key = masp_pin_tx_key(key); ctx.write(&pin_key, current_tx_idx)?; } diff --git a/core/src/types/token.rs b/core/src/types/token.rs index d738086e7b..8ff08c9d39 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -9,6 +9,8 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use data_encoding::BASE32HEX_NOPAD; use ethabi::ethereum_types::U256; +use masp_primitives::bls12_381::Scalar; +use masp_primitives::sapling::Nullifier; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -1282,6 +1284,59 @@ pub fn masp_last_inflation_key(token_address: &Address) -> Key { ) } +/// Get a key for a masp nullifier +pub fn masp_nullifier_key(nullifier: &Nullifier) -> Key { + Key::from(MASP.to_db_key()) + .push(&MASP_NULLIFIERS_KEY.to_owned()) + .expect("Cannot obtain a storage key") + .push(&Hash(nullifier.0)) + .expect("Cannot obtain a storage key") +} + +/// Get the key for the masp commitment tree +pub fn masp_commitment_tree_key() -> Key { + Key::from(MASP.to_db_key()) + .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get a key for a masp commitment tree anchor +pub fn masp_commitment_anchor_key(anchor: &Scalar) -> Key { + Key::from(MASP.to_db_key()) + .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") + .push(&Hash(anchor.to_bytes())) + .expect("Cannot obtain a storage key") +} + +/// Get the key for the masp convert tree anchor +pub fn masp_convert_anchor_key() -> Key { + Key::from(MASP.to_db_key()) + .push(&MASP_CONVERT_ANCHOR_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get the masp head-tx key +pub fn masp_head_tx_key() -> Key { + Key::from(MASP.to_db_key()) + .push(&HEAD_TX_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get the masp tx key for the provided tx id +pub fn masp_tx_key(tx_id: u64) -> Key { + Key::from(MASP.to_db_key()) + .push(&(TX_KEY_PREFIX.to_owned() + &tx_id.to_string())) + .expect("Cannot obtain a storage key") +} + +/// Get the masp pin tx key for the provided pin key +pub fn masp_pin_tx_key(pin_key: &str) -> Key { + Key::from(MASP.to_db_key()) + .push(&(PIN_KEY_PREFIX.to_owned() + pin_key)) + .expect("Cannot obtain a storage key") +} + /// Check if the given storage key is for a minter of a unspecified token. /// If it is, returns the token. pub fn is_any_minter_key(key: &Key) -> Option<&Address> { diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index e6d244a6c7..ee42c5c8a1 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -14,15 +14,12 @@ use namada_core::ledger::storage; use namada_core::ledger::storage_api::OptionExt; use namada_core::ledger::vp_env::VpEnv; use namada_core::proto::Tx; +use namada_core::types::address::Address; use namada_core::types::address::InternalAddress::Masp; -use namada_core::types::address::{Address, MASP}; -use namada_core::types::storage::{BlockHeight, Epoch, Key, KeySeg, TxIndex}; +use namada_core::types::storage::{BlockHeight, Epoch, Key, TxIndex}; use namada_core::types::token::{ self, is_masp_allowed_key, is_masp_key, is_masp_nullifier_key, - is_masp_tx_pin_key, is_masp_tx_prefix_key, Transfer, HEAD_TX_KEY, - MASP_CONVERT_ANCHOR_KEY, MASP_NOTE_COMMITMENT_ANCHOR_PREFIX, - MASP_NOTE_COMMITMENT_TREE_KEY, MASP_NULLIFIERS_KEY, PIN_KEY_PREFIX, - TX_KEY_PREFIX, + is_masp_tx_pin_key, is_masp_tx_prefix_key, Transfer, }; use namada_sdk::masp::verify_shielded_tx; use ripemd::Digest as RipemdDigest; @@ -134,11 +131,9 @@ where }; for description in shielded_spends { - let nullifier_key = Key::from(MASP.to_db_key()) - .push(&MASP_NULLIFIERS_KEY.to_owned()) - .expect("Cannot obtain a storage key") - .push(&namada_core::types::hash::Hash(description.nullifier.0)) - .expect("Cannot obtain a storage key"); + let nullifier_key = namada_core::types::token::masp_nullifier_key( + &description.nullifier, + ); if self.ctx.has_key_pre(&nullifier_key)? || revealed_nullifiers.contains(&nullifier_key) { @@ -185,9 +180,7 @@ where ) -> Result { // Check that the merkle tree in storage has been correctly updated with // the output descriptions cmu - let tree_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_TREE_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let tree_key = namada_core::types::token::masp_commitment_tree_key(); let mut previous_tree: CommitmentTree = self.ctx.read_pre(&tree_key)?.ok_or(Error::NativeVpError( native_vp::Error::SimpleMessage("Cannot read storage"), @@ -241,13 +234,10 @@ where }; for description in shielded_spends { - let anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") - .push(&namada_core::types::hash::Hash( - description.anchor.to_bytes(), - )) - .expect("Cannot obtain a storage key"); + let anchor_key = + namada_core::types::token::masp_commitment_anchor_key( + &description.anchor, + ); // Check if the provided anchor was published before if !self.ctx.has_key_pre(&anchor_key)? { @@ -268,9 +258,8 @@ where ) -> Result { if let Some(bundle) = transaction.sapling_bundle() { if !bundle.shielded_converts.is_empty() { - let anchor_key = Key::from(MASP.to_db_key()) - .push(&MASP_CONVERT_ANCHOR_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let anchor_key = + namada_core::types::token::masp_convert_anchor_key(); let expected_anchor = self .ctx .read_pre::(&anchor_key)? @@ -328,9 +317,7 @@ where } // Validate head tx - let head_tx_key = Key::from(MASP.to_db_key()) - .push(&HEAD_TX_KEY.to_owned()) - .expect("Cannot obtain a storage key"); + let head_tx_key = namada_core::types::token::masp_head_tx_key(); let pre_head: u64 = self.ctx.read_pre(&head_tx_key)?.unwrap_or(0); let post_head: u64 = self.ctx.read_post(&head_tx_key)?.unwrap_or(0); @@ -339,9 +326,7 @@ where } // Validate tx key - let current_tx_key = Key::from(MASP.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &pre_head.to_string())) - .expect("Cannot obtain a storage key"); + let current_tx_key = namada_core::types::token::masp_tx_key(pre_head); match self .ctx .read_post::<(Epoch, BlockHeight, TxIndex, Transfer, Transaction)>( @@ -363,9 +348,7 @@ where // Validate pin key if let Some(key) = &transfer.key { - let pin_key = Key::from(MASP.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + key)) - .expect("Cannot obtain a storage key"); + let pin_key = namada_core::types::token::masp_pin_tx_key(key); match self.ctx.read_post::(&pin_key)? { Some(tx_idx) if tx_idx == pre_head => (), _ => return Ok(false), From c7c61e9bdf1526c66628fdf5ff4a651edd89b802 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 28 Dec 2023 19:20:03 +0100 Subject: [PATCH 03/26] Refactors `masp_commitment_anchor_key` to be generic --- apps/src/lib/node/ledger/shell/finalize_block.rs | 3 +-- apps/src/lib/node/ledger/shell/init_chain.rs | 4 +--- benches/native_vps.rs | 3 +-- core/src/types/token.rs | 4 ++-- shared/src/ledger/native_vp/masp.rs | 2 +- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 4fff595bad..eb06053adc 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -3,7 +3,6 @@ use data_encoding::HEXUPPER; use masp_primitives::merkle_tree::CommitmentTree; use masp_primitives::sapling::Node; -use masp_proofs::bls12_381; use namada::core::ledger::masp_conversions::update_allowed_conversions; use namada::core::ledger::pgf::inflation as pgf_inflation; use namada::core::types::storage::KeySeg; @@ -571,7 +570,7 @@ where .into_storage_result()?; let anchor_key = namada::core::types::token::masp_commitment_anchor_key( - &bls12_381::Scalar::from(updated_tree.root()), + updated_tree.root(), ); self.wl_storage.write(&anchor_key, ())?; } diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 1f08287f1e..65554dc35f 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -147,9 +147,7 @@ where .write(¬e_commitment_tree_key, empty_commitment_tree) .unwrap(); let commitment_tree_anchor_key = - namada::core::types::token::masp_commitment_anchor_key( - &bls12_381::Scalar::from(anchor), - ); + namada::core::types::token::masp_commitment_anchor_key(anchor); self.wl_storage .write(&commitment_tree_anchor_key, ()) .unwrap(); diff --git a/benches/native_vps.rs b/benches/native_vps.rs index 0e71e37690..ed6da9c22e 100644 --- a/benches/native_vps.rs +++ b/benches/native_vps.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use std::str::FromStr; use criterion::{criterion_group, criterion_main, Criterion}; -use masp_primitives::bls12_381; use masp_primitives::sapling::Node; use namada::core::ledger::governance::storage::proposal::ProposalType; use namada::core::ledger::governance::storage::vote::{ @@ -516,7 +515,7 @@ fn setup_storage_for_masp_verification( .unwrap() .unwrap(); let anchor_key = namada::core::types::token::masp_commitment_anchor_key( - &bls12_381::Scalar::from(updated_tree.root()), + updated_tree.root(), ); shielded_ctx .shell diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 8ff08c9d39..583c4aa6e9 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1301,11 +1301,11 @@ pub fn masp_commitment_tree_key() -> Key { } /// Get a key for a masp commitment tree anchor -pub fn masp_commitment_anchor_key(anchor: &Scalar) -> Key { +pub fn masp_commitment_anchor_key(anchor: impl Into) -> Key { Key::from(MASP.to_db_key()) .push(&MASP_NOTE_COMMITMENT_ANCHOR_PREFIX.to_owned()) .expect("Cannot obtain a storage key") - .push(&Hash(anchor.to_bytes())) + .push(&Hash(anchor.into().to_bytes())) .expect("Cannot obtain a storage key") } diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index ee42c5c8a1..8ff793d517 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -236,7 +236,7 @@ where for description in shielded_spends { let anchor_key = namada_core::types::token::masp_commitment_anchor_key( - &description.anchor, + description.anchor, ); // Check if the provided anchor was published before From b4a5b20a1cef997632be8102218aa13ed79ac65e Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 28 Dec 2023 19:36:08 +0100 Subject: [PATCH 04/26] SDK uses core helper functions for masp keys --- sdk/src/masp.rs | 63 ++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 576f311cb1..bc9d15a6b6 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -54,12 +54,10 @@ use namada_core::types::masp::{ BalanceOwner, ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, }; -use namada_core::types::storage::{BlockHeight, Epoch, Key, KeySeg, TxIndex}; +use namada_core::types::storage::{BlockHeight, Epoch, TxIndex}; use namada_core::types::time::{DateTimeUtc, DurationSecs}; use namada_core::types::token; -use namada_core::types::token::{ - Change, MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, -}; +use namada_core::types::token::{Change, MaspDenom, Transfer}; use namada_core::types::transaction::WrapperTx; use rand_core::{CryptoRng, OsRng, RngCore}; use ripemd::Digest as RipemdDigest; @@ -770,14 +768,8 @@ impl ShieldedContext { BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer, Transaction)>, Error, > { - // The address of the MASP account - let masp_addr = MASP; // Construct the key where last transaction pointer is stored - let head_tx_key = Key::from(masp_addr.to_db_key()) - .push(&HEAD_TX_KEY.to_owned()) - .map_err(|k| { - Error::Other(format!("Cannot obtain a storage key: {}", k)) - })?; + let head_tx_key = namada_core::types::token::masp_head_tx_key(); // Query for the index of the last accepted transaction let head_txidx = query_storage_value::(client, &head_tx_key) .await @@ -786,11 +778,7 @@ impl ShieldedContext { // Fetch all the transactions we do not have yet for i in last_txidx..head_txidx { // Construct the key for where the current transaction is stored - let current_tx_key = Key::from(masp_addr.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &i.to_string())) - .map_err(|e| { - Error::Other(format!("Cannot obtain a storage key {}", e)) - })?; + let current_tx_key = namada_core::types::token::masp_tx_key(i); // Obtain the current transaction let (tx_epoch, tx_height, tx_index, current_tx, current_stx) = query_storage_value::< @@ -1370,14 +1358,8 @@ impl ShieldedContext { return Err(Error::from(PinnedBalanceError::InvalidViewingKey)); } } - // The address of the MASP account - let masp_addr = MASP; // Construct the key for where the transaction ID would be stored - let pin_key = Key::from(masp_addr.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + &owner.hash())) - .map_err(|_| { - Error::Other("Cannot obtain a storage key".to_string()) - })?; + let pin_key = namada_core::types::token::masp_pin_tx_key(&owner.hash()); // Obtain the transaction pointer at the key // If we don't discard the error message then a test fails, // however the error underlying this will go undetected @@ -1385,11 +1367,7 @@ impl ShieldedContext { .await .map_err(|_| PinnedBalanceError::NoTransactionPinned)?; // Construct the key for where the pinned transaction is stored - let tx_key = Key::from(masp_addr.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &txidx.to_string())) - .map_err(|_| { - Error::Other("Cannot obtain a storage key".to_string()) - })?; + let tx_key = namada_core::types::token::masp_tx_key(txidx); // Obtain the pointed to transaction let (tx_epoch, _tx_height, _tx_index, _tx, shielded) = rpc::query_storage_value::< @@ -1792,28 +1770,27 @@ impl ShieldedContext { // To speed up integration tests, we can save and load proofs #[cfg(feature = "testing")] - let load_or_save = if let Ok(masp_proofs) = - env::var(ENV_VAR_MASP_TEST_PROOFS) - { - let parsed = match masp_proofs.to_ascii_lowercase().as_str() { - "load" => LoadOrSaveProofs::Load, - "save" => LoadOrSaveProofs::Save, - env_var => Err(Error::Other(format!( + let load_or_save = + if let Ok(masp_proofs) = env::var(ENV_VAR_MASP_TEST_PROOFS) { + let parsed = match masp_proofs.to_ascii_lowercase().as_str() { + "load" => LoadOrSaveProofs::Load, + "save" => LoadOrSaveProofs::Save, + env_var => Err(Error::Other(format!( "Unexpected value for {ENV_VAR_MASP_TEST_PROOFS} env var. \ Expecting \"save\" or \"load\", but got \"{env_var}\"." )))?, - }; - if env::var(ENV_VAR_MASP_TEST_SEED).is_err() { - Err(Error::Other(format!( + }; + if env::var(ENV_VAR_MASP_TEST_SEED).is_err() { + Err(Error::Other(format!( "Ensure to set a seed with {ENV_VAR_MASP_TEST_SEED} env \ var when using {ENV_VAR_MASP_TEST_PROOFS} for \ deterministic proofs." )))?; - } - parsed - } else { - LoadOrSaveProofs::Neither - }; + } + parsed + } else { + LoadOrSaveProofs::Neither + }; let builder_clone = builder.clone().map_builder(WalletMap); #[cfg(feature = "testing")] From c5952ec96d13c23fcec05a136a832931f00ef51f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 2 Jan 2024 12:42:13 +0100 Subject: [PATCH 05/26] Clippy + fmt --- .../lib/node/ledger/shell/finalize_block.rs | 247 +++++++++++------- apps/src/lib/node/ledger/shell/init_chain.rs | 1 - core/src/ledger/masp_conversions.rs | 3 - sdk/src/masp.rs | 29 +- shared/src/ledger/native_vp/masp.rs | 2 +- 5 files changed, 163 insertions(+), 119 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index eb06053adc..4ab90bbf62 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -5,7 +5,6 @@ use masp_primitives::merkle_tree::CommitmentTree; use masp_primitives::sapling::Node; use namada::core::ledger::masp_conversions::update_allowed_conversions; use namada::core::ledger::pgf::inflation as pgf_inflation; -use namada::core::types::storage::KeySeg; use namada::ledger::events::EventType; use namada::ledger::gas::{GasMetering, TxGasMeter}; use namada::ledger::pos::namada_proof_of_stake; @@ -1864,9 +1863,11 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.to_vec(), vec![], None); - assert!(rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); // FINALIZE BLOCK 2. Tell Namada that val1 is the block proposer. // Include votes that correspond to block 1. Make val2 the next block's @@ -1881,9 +1882,11 @@ mod test_finalize_block { assert!(rewards_prod_2.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_4.is_empty(&shell.wl_storage).unwrap()); - assert!(!rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + !rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); // Val1 was the proposer, so its reward should be larger than all // others, which should themselves all be equal let acc_sum = get_rewards_sum(&shell.wl_storage); @@ -2015,9 +2018,11 @@ mod test_finalize_block { None, ); } - assert!(rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); let rp1 = rewards_prod_1 .get(&shell.wl_storage, &Epoch::default()) .unwrap() @@ -2105,9 +2110,11 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.to_vec(), vec![], None); - assert!(rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); let (current_epoch, inflation) = advance_epoch(&mut shell, &pkh1, &votes, None); @@ -2355,9 +2362,11 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); - assert!(rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); // Make an account with balance and delegate some tokens let delegator = address::testing::gen_implicit_address(); @@ -2524,9 +2533,11 @@ mod test_finalize_block { // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); - assert!(rewards_accumulator_handle() - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + rewards_accumulator_handle() + .is_empty(&shell.wl_storage) + .unwrap() + ); // Check that there's 3 unique consensus keys let consensus_keys = @@ -2834,21 +2845,25 @@ mod test_finalize_block { assert_eq!(root_pre.0, root_post.0); // Check transaction's hash in storage - assert!(shell - .shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper_tx.header_hash()) - .unwrap_or_default()); + assert!( + shell + .shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper_tx.header_hash()) + .unwrap_or_default() + ); // Check that the hash is present in the merkle tree - assert!(!shell - .shell - .wl_storage - .storage - .block - .tree - .has_key(&wrapper_hash_key) - .unwrap()); + assert!( + !shell + .shell + .wl_storage + .storage + .block + .tree + .has_key(&wrapper_hash_key) + .unwrap() + ); } /// Test that a decrypted tx that has already been applied in the same block @@ -2953,16 +2968,20 @@ mod test_finalize_block { assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); for (inner, wrapper) in [(inner, wrapper), (new_inner, new_wrapper)] { - assert!(shell - .wl_storage - .write_log - .has_replay_protection_entry(&inner.raw_header_hash()) - .unwrap_or_default()); - assert!(!shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper.header_hash()) - .unwrap_or_default()); + assert!( + shell + .wl_storage + .write_log + .has_replay_protection_entry(&inner.raw_header_hash()) + .unwrap_or_default() + ); + assert!( + !shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper.header_hash()) + .unwrap_or_default() + ); } } @@ -3119,27 +3138,37 @@ mod test_finalize_block { (unsigned_inner, unsigned_wrapper), (wrong_commitment_inner, wrong_commitment_wrapper), ] { - assert!(!shell + assert!( + !shell + .wl_storage + .write_log + .has_replay_protection_entry( + &invalid_inner.raw_header_hash() + ) + .unwrap_or_default() + ); + assert!( + shell + .wl_storage + .storage + .has_replay_protection_entry(&valid_wrapper.header_hash()) + .unwrap_or_default() + ); + } + assert!( + shell .wl_storage .write_log - .has_replay_protection_entry(&invalid_inner.raw_header_hash()) - .unwrap_or_default()); - assert!(shell + .has_replay_protection_entry(&failing_inner.raw_header_hash()) + .expect("test failed") + ); + assert!( + !shell .wl_storage - .storage - .has_replay_protection_entry(&valid_wrapper.header_hash()) - .unwrap_or_default()); - } - assert!(shell - .wl_storage - .write_log - .has_replay_protection_entry(&failing_inner.raw_header_hash()) - .expect("test failed")); - assert!(!shell - .wl_storage - .write_log - .has_replay_protection_entry(&failing_wrapper.header_hash()) - .unwrap_or_default()); + .write_log + .has_replay_protection_entry(&failing_wrapper.header_hash()) + .unwrap_or_default() + ); } #[test] @@ -3205,16 +3234,20 @@ mod test_finalize_block { .as_str(); assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); - assert!(shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper_hash) - .unwrap_or_default()); - assert!(!shell - .wl_storage - .write_log - .has_replay_protection_entry(&wrapper.raw_header_hash()) - .unwrap_or_default()); + assert!( + shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper_hash) + .unwrap_or_default() + ); + assert!( + !shell + .wl_storage + .write_log + .has_replay_protection_entry(&wrapper.raw_header_hash()) + .unwrap_or_default() + ); } // Test that if the fee payer doesn't have enough funds for fee payment the @@ -3513,9 +3546,11 @@ mod test_finalize_block { .unwrap(), Some(ValidatorState::Consensus) ); - assert!(enqueued_slashes_handle() - .at(&Epoch::default()) - .is_empty(&shell.wl_storage)?); + assert!( + enqueued_slashes_handle() + .at(&Epoch::default()) + .is_empty(&shell.wl_storage)? + ); assert_eq!( get_num_consensus_validators(&shell.wl_storage, Epoch::default()) .unwrap(), @@ -3534,17 +3569,21 @@ mod test_finalize_block { .unwrap(), Some(ValidatorState::Jailed) ); - assert!(enqueued_slashes_handle() - .at(&epoch) - .is_empty(&shell.wl_storage)?); + assert!( + enqueued_slashes_handle() + .at(&epoch) + .is_empty(&shell.wl_storage)? + ); assert_eq!( get_num_consensus_validators(&shell.wl_storage, epoch).unwrap(), 5_u64 ); } - assert!(!enqueued_slashes_handle() - .at(&processing_epoch) - .is_empty(&shell.wl_storage)?); + assert!( + !enqueued_slashes_handle() + .at(&processing_epoch) + .is_empty(&shell.wl_storage)? + ); // Advance to the processing epoch loop { @@ -3567,9 +3606,11 @@ mod test_finalize_block { // println!("Reached processing epoch"); break; } else { - assert!(enqueued_slashes_handle() - .at(&shell.wl_storage.storage.block.epoch) - .is_empty(&shell.wl_storage)?); + assert!( + enqueued_slashes_handle() + .at(&shell.wl_storage.storage.block.epoch) + .is_empty(&shell.wl_storage)? + ); let stake1 = read_validator_stake( &shell.wl_storage, ¶ms, @@ -4063,11 +4104,13 @@ mod test_finalize_block { ) .unwrap(); assert_eq!(last_slash, Some(misbehavior_epoch)); - assert!(namada_proof_of_stake::storage::validator_slashes_handle( - &val1.address - ) - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap() + ); tracing::debug!("Advancing to epoch 7"); @@ -4131,18 +4174,22 @@ mod test_finalize_block { ) .unwrap(); assert_eq!(last_slash, Some(Epoch(4))); - assert!(namada_proof_of_stake::is_validator_frozen( - &shell.wl_storage, - &val1.address, - current_epoch, - ¶ms - ) - .unwrap()); - assert!(namada_proof_of_stake::storage::validator_slashes_handle( - &val1.address - ) - .is_empty(&shell.wl_storage) - .unwrap()); + assert!( + namada_proof_of_stake::is_validator_frozen( + &shell.wl_storage, + &val1.address, + current_epoch, + ¶ms + ) + .unwrap() + ); + assert!( + namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap() + ); let pre_stake_10 = namada_proof_of_stake::storage::read_validator_stake( diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 65554dc35f..706d5c479e 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -5,7 +5,6 @@ use std::ops::ControlFlow; use masp_primitives::merkle_tree::CommitmentTree; use masp_primitives::sapling::Node; use masp_proofs::bls12_381; -use namada::core::types::storage::KeySeg; use namada::ledger::parameters::Parameters; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; diff --git a/core/src/ledger/masp_conversions.rs b/core/src/ledger/masp_conversions.rs index fa43ca93b1..921534b178 100644 --- a/core/src/ledger/masp_conversions.rs +++ b/core/src/ledger/masp_conversions.rs @@ -212,9 +212,6 @@ where }; use rayon::prelude::ParallelSlice; - use crate::types::address; - use crate::types::storage::{Key, KeySeg}; - // The derived conversions will be placed in MASP address space let masp_addr = MASP; diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index bc9d15a6b6..62b4ba1ea2 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -1770,27 +1770,28 @@ impl ShieldedContext { // To speed up integration tests, we can save and load proofs #[cfg(feature = "testing")] - let load_or_save = - if let Ok(masp_proofs) = env::var(ENV_VAR_MASP_TEST_PROOFS) { - let parsed = match masp_proofs.to_ascii_lowercase().as_str() { - "load" => LoadOrSaveProofs::Load, - "save" => LoadOrSaveProofs::Save, - env_var => Err(Error::Other(format!( + let load_or_save = if let Ok(masp_proofs) = + env::var(ENV_VAR_MASP_TEST_PROOFS) + { + let parsed = match masp_proofs.to_ascii_lowercase().as_str() { + "load" => LoadOrSaveProofs::Load, + "save" => LoadOrSaveProofs::Save, + env_var => Err(Error::Other(format!( "Unexpected value for {ENV_VAR_MASP_TEST_PROOFS} env var. \ Expecting \"save\" or \"load\", but got \"{env_var}\"." )))?, - }; - if env::var(ENV_VAR_MASP_TEST_SEED).is_err() { - Err(Error::Other(format!( + }; + if env::var(ENV_VAR_MASP_TEST_SEED).is_err() { + Err(Error::Other(format!( "Ensure to set a seed with {ENV_VAR_MASP_TEST_SEED} env \ var when using {ENV_VAR_MASP_TEST_PROOFS} for \ deterministic proofs." )))?; - } - parsed - } else { - LoadOrSaveProofs::Neither - }; + } + parsed + } else { + LoadOrSaveProofs::Neither + }; let builder_clone = builder.clone().map_builder(WalletMap); #[cfg(feature = "testing")] diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index 8ff793d517..a26fff6185 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -97,7 +97,7 @@ fn convert_amount( // Combine the value and unit into one amount I128Sum::from_nonnegative(asset_type, denom.denominate(&val) as i128) - .map_err(|()| { + .map_err(|()| { Error::NativeVpError(native_vp::Error::SimpleMessage( "Invalid value for amount", )) From ddd6368725255f0c6bd468d7e2d849f8b0a407a6 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 2 Jan 2024 12:21:21 +0100 Subject: [PATCH 06/26] Changelog #2345 --- .changelog/unreleased/SDK/2345-masp-vp-panics.md | 2 ++ .changelog/unreleased/improvements/2345-masp-vp-panics.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 .changelog/unreleased/SDK/2345-masp-vp-panics.md create mode 100644 .changelog/unreleased/improvements/2345-masp-vp-panics.md diff --git a/.changelog/unreleased/SDK/2345-masp-vp-panics.md b/.changelog/unreleased/SDK/2345-masp-vp-panics.md new file mode 100644 index 0000000000..5a88235c6f --- /dev/null +++ b/.changelog/unreleased/SDK/2345-masp-vp-panics.md @@ -0,0 +1,2 @@ +- Refactors MASP keys construction. + ([\#2345](https://github.com/anoma/namada/pull/2345)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/2345-masp-vp-panics.md b/.changelog/unreleased/improvements/2345-masp-vp-panics.md new file mode 100644 index 0000000000..41e041a91f --- /dev/null +++ b/.changelog/unreleased/improvements/2345-masp-vp-panics.md @@ -0,0 +1,2 @@ +- Removes panics from masp vp. Refactors masp storage keys generation. + ([\#2345](https://github.com/anoma/namada/pull/2345)) \ No newline at end of file From dfd8c8e3fcdee63e70ba7b9caeb7126870bf683f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 29 Dec 2023 16:53:04 +0100 Subject: [PATCH 07/26] Removes masp tx writing of lookup data to storage --- core/src/ledger/ibc/context/storage.rs | 4 +-- core/src/ledger/ibc/mod.rs | 2 +- core/src/ledger/masp_utils.rs | 29 +--------------------- shared/src/ledger/native_vp/ibc/context.rs | 17 ++++++------- shared/src/vm/host_env.rs | 14 +++++------ tx_prelude/src/ibc.rs | 10 +++----- wasm/wasm_source/src/tx_transfer.rs | 2 +- 7 files changed, 22 insertions(+), 56 deletions(-) diff --git a/core/src/ledger/ibc/context/storage.rs b/core/src/ledger/ibc/context/storage.rs index facbb2a5c2..a5e79e6e1f 100644 --- a/core/src/ledger/ibc/context/storage.rs +++ b/core/src/ledger/ibc/context/storage.rs @@ -4,7 +4,7 @@ pub use ics23::ProofSpec; use crate::ledger::storage_api::{Error, StorageRead, StorageWrite}; use crate::types::address::Address; -use crate::types::ibc::{IbcEvent, IbcShieldedTransfer}; +use crate::types::ibc::IbcEvent; use crate::types::token::DenominatedAmount; /// IBC context trait to be implemented in integration that can read and write @@ -30,7 +30,7 @@ pub trait IbcStorageContext: StorageRead + StorageWrite { /// Handle masp tx fn handle_masp_tx( &mut self, - shielded: &IbcShieldedTransfer, + shielded: &masp_primitives::transaction::Transaction, ) -> Result<(), Error>; /// Mint token diff --git a/core/src/ledger/ibc/mod.rs b/core/src/ledger/ibc/mod.rs index b695aef259..11afd339e1 100644 --- a/core/src/ledger/ibc/mod.rs +++ b/core/src/ledger/ibc/mod.rs @@ -282,7 +282,7 @@ where self.ctx .inner .borrow_mut() - .handle_masp_tx(&shielded_transfer) + .handle_masp_tx(&shielded_transfer.masp_tx) .map_err(|_| { Error::MaspTx("Writing MASP components failed".to_string()) })?; diff --git a/core/src/ledger/masp_utils.rs b/core/src/ledger/masp_utils.rs index d2d2b9a65c..37631e4855 100644 --- a/core/src/ledger/masp_utils.rs +++ b/core/src/ledger/masp_utils.rs @@ -6,11 +6,7 @@ use masp_primitives::transaction::Transaction; use super::storage_api::{StorageRead, StorageWrite}; use crate::ledger::storage_api::{Error, Result}; -use crate::types::storage::{BlockHeight, Epoch, TxIndex}; -use crate::types::token::{ - masp_commitment_tree_key, masp_head_tx_key, masp_nullifier_key, - masp_pin_tx_key, masp_tx_key, Transfer, -}; +use crate::types::token::{masp_commitment_tree_key, masp_nullifier_key}; // Writes the nullifiers of the provided masp transaction to storage fn reveal_nullifiers( @@ -62,36 +58,13 @@ pub fn update_note_commitment_tree( /// Handle a MASP transaction. pub fn handle_masp_tx( ctx: &mut (impl StorageRead + StorageWrite), - transfer: &Transfer, shielded: &Transaction, ) -> Result<()> { - let head_tx_key = masp_head_tx_key(); - let current_tx_idx: u64 = - ctx.read(&head_tx_key).unwrap_or(None).unwrap_or(0); - let current_tx_key = masp_tx_key(current_tx_idx); - // Save the Transfer object and its location within the blockchain - // so that clients do not have to separately look these - // up - let record: (Epoch, BlockHeight, TxIndex, Transfer, Transaction) = ( - ctx.get_block_epoch()?, - ctx.get_block_height()?, - ctx.get_tx_index()?, - transfer.clone(), - shielded.clone(), - ); - ctx.write(¤t_tx_key, record)?; - ctx.write(&head_tx_key, current_tx_idx + 1)?; // TODO: temporarily disabled because of the node aggregation issue in WASM. // Using the host env tx_update_masp_note_commitment_tree or directly the // update_note_commitment_tree function as a workaround instead // update_note_commitment_tree(ctx, shielded)?; reveal_nullifiers(ctx, shielded)?; - // If storage key has been supplied, then pin this transaction to it - if let Some(key) = &transfer.key { - let pin_key = masp_pin_tx_key(key); - ctx.write(&pin_key, current_tx_idx)?; - } - Ok(()) } diff --git a/shared/src/ledger/native_vp/ibc/context.rs b/shared/src/ledger/native_vp/ibc/context.rs index 27b9ee4324..968ff53329 100644 --- a/shared/src/ledger/native_vp/ibc/context.rs +++ b/shared/src/ledger/native_vp/ibc/context.rs @@ -12,7 +12,7 @@ use crate::ledger::storage::write_log::StorageModification; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::address::{Address, InternalAddress}; -use crate::types::ibc::{IbcEvent, IbcShieldedTransfer}; +use crate::types::ibc::IbcEvent; use crate::types::storage::{ BlockHash, BlockHeight, Epoch, Header, Key, TxIndex, }; @@ -213,13 +213,12 @@ where self.write(&dest_key, dest_bal.serialize_to_vec()) } - fn handle_masp_tx(&mut self, shielded: &IbcShieldedTransfer) -> Result<()> { - masp_utils::handle_masp_tx( - self, - &shielded.transfer, - &shielded.masp_tx, - )?; - masp_utils::update_note_commitment_tree(self, &shielded.masp_tx) + fn handle_masp_tx( + &mut self, + shielded: &masp_primitives::transaction::Transaction, + ) -> Result<()> { + masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::update_note_commitment_tree(self, shielded) } fn mint_token( @@ -418,7 +417,7 @@ where fn handle_masp_tx( &mut self, - _shielded: &IbcShieldedTransfer, + _shielded: &masp_primitives::transaction::Transaction, ) -> Result<()> { unimplemented!("Validation doesn't handle a masp tx") } diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index aa7399bc3a..af2b808892 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -31,7 +31,7 @@ use crate::ledger::vp_host_fns; use crate::proto::Tx; use crate::types::address::{self, Address}; use crate::types::hash::Hash; -use crate::types::ibc::{IbcEvent, IbcShieldedTransfer}; +use crate::types::ibc::IbcEvent; use crate::types::internal::HostEnvResult; use crate::types::storage::{BlockHeight, Epoch, Key, TxIndex}; use crate::types::token::{ @@ -113,6 +113,7 @@ where pub tx: HostRef<'a, &'a Tx>, /// The transaction index is used to identify a shielded transaction's /// parent + // FIXME: still need this? pub tx_index: HostRef<'a, &'a TxIndex>, /// The verifiers whose validity predicates should be triggered. pub verifiers: MutHostRef<'a, &'a BTreeSet
>, @@ -275,6 +276,7 @@ where pub tx: HostRef<'a, &'a Tx>, /// The transaction index is used to identify a shielded transaction's /// parent + // FIXME: still need this? pub tx_index: HostRef<'a, &'a TxIndex>, /// The runner of the [`vp_eval`] function pub eval_runner: HostRef<'a, &'a EVAL>, @@ -2554,14 +2556,10 @@ where fn handle_masp_tx( &mut self, - shielded: &IbcShieldedTransfer, + shielded: &masp_primitives::transaction::Transaction, ) -> Result<(), storage_api::Error> { - masp_utils::handle_masp_tx( - self, - &shielded.transfer, - &shielded.masp_tx, - )?; - masp_utils::update_note_commitment_tree(self, &shielded.masp_tx) + masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::update_note_commitment_tree(self, shielded) } fn mint_token( diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs index ebf97e7859..4a04e2a980 100644 --- a/tx_prelude/src/ibc.rs +++ b/tx_prelude/src/ibc.rs @@ -51,14 +51,10 @@ impl IbcStorageContext for Ctx { fn handle_masp_tx( &mut self, - shielded: &IbcShieldedTransfer, + shielded: &masp_primitives::transaction::Transaction, ) -> Result<(), Error> { - masp_utils::handle_masp_tx( - self, - &shielded.transfer, - &shielded.masp_tx, - )?; - masp_utils::update_note_commitment_tree(self, &shielded.masp_tx) + masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::update_note_commitment_tree(self, shielded) } fn mint_token( diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index e521be4f75..f7874e33ec 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -38,7 +38,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { }) .transpose()?; if let Some(shielded) = shielded { - token::masp_utils::handle_masp_tx(ctx, &transfer, &shielded)?; + token::masp_utils::handle_masp_tx(ctx, &shielded)?; update_masp_note_commitment_tree(&shielded)?; } Ok(()) From 6e471b65027789c79b826757c3c61434118888cc Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 29 Dec 2023 17:01:26 +0100 Subject: [PATCH 08/26] Updates masp storage keys helper functions --- core/src/types/token.rs | 49 +---------------------------------------- 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 583c4aa6e9..da591466d9 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -978,12 +978,6 @@ pub const DENOM_STORAGE_KEY: &str = "denomination"; pub const MINTER_STORAGE_KEY: &str = "minter"; /// Key segment for minted balance pub const MINTED_STORAGE_KEY: &str = "minted"; -/// Key segment for head shielded transaction pointer keys -pub const HEAD_TX_KEY: &str = "head-tx"; -/// Key segment prefix for shielded transaction key -pub const TX_KEY_PREFIX: &str = "tx-"; -/// Key segment prefix for pinned shielded transactions -pub const PIN_KEY_PREFIX: &str = "pin-"; /// Key segment prefix for the nullifiers pub const MASP_NULLIFIERS_KEY: &str = "nullifiers"; /// Key segment prefix for the note commitment merkle tree @@ -1223,11 +1217,7 @@ pub fn is_masp_key(key: &Key) -> bool { pub fn is_masp_allowed_key(key: &Key) -> bool { match &key.segments[..] { [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] - if *addr == MASP - && (key == HEAD_TX_KEY - || key.starts_with(TX_KEY_PREFIX) - || key.starts_with(PIN_KEY_PREFIX) - || key == MASP_NOTE_COMMITMENT_TREE_KEY) => + if *addr == MASP && key == MASP_NOTE_COMMITMENT_TREE_KEY => { true } @@ -1241,22 +1231,6 @@ pub fn is_masp_allowed_key(key: &Key) -> bool { } } -/// Check if the given storage key is a masp tx prefix key -pub fn is_masp_tx_prefix_key(key: &Key) -> bool { - matches!(&key.segments[..], - [DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - ] if *addr == MASP && prefix.starts_with(TX_KEY_PREFIX)) -} - -/// Check if the given storage key is a masp tx pin key -pub fn is_masp_tx_pin_key(key: &Key) -> bool { - matches!(&key.segments[..], - [DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - ] if *addr == MASP && prefix.starts_with(PIN_KEY_PREFIX)) -} - /// Check if the given storage key is a masp nullifier key pub fn is_masp_nullifier_key(key: &Key) -> bool { matches!(&key.segments[..], @@ -1316,27 +1290,6 @@ pub fn masp_convert_anchor_key() -> Key { .expect("Cannot obtain a storage key") } -/// Get the masp head-tx key -pub fn masp_head_tx_key() -> Key { - Key::from(MASP.to_db_key()) - .push(&HEAD_TX_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Get the masp tx key for the provided tx id -pub fn masp_tx_key(tx_id: u64) -> Key { - Key::from(MASP.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &tx_id.to_string())) - .expect("Cannot obtain a storage key") -} - -/// Get the masp pin tx key for the provided pin key -pub fn masp_pin_tx_key(pin_key: &str) -> Key { - Key::from(MASP.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + pin_key)) - .expect("Cannot obtain a storage key") -} - /// Check if the given storage key is for a minter of a unspecified token. /// If it is, returns the token. pub fn is_any_minter_key(key: &Key) -> Option<&Address> { From 3607c39e5932c3eaeca8637214d6223387f21c38 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 29 Dec 2023 17:33:26 +0100 Subject: [PATCH 09/26] Updates keys check in masp vp --- shared/src/ledger/native_vp/masp.rs | 92 +++++------------------------ 1 file changed, 15 insertions(+), 77 deletions(-) diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index a26fff6185..f7811245f8 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -16,10 +16,9 @@ use namada_core::ledger::vp_env::VpEnv; use namada_core::proto::Tx; use namada_core::types::address::Address; use namada_core::types::address::InternalAddress::Masp; -use namada_core::types::storage::{BlockHeight, Epoch, Key, TxIndex}; +use namada_core::types::storage::{Epoch, Key}; use namada_core::types::token::{ self, is_masp_allowed_key, is_masp_key, is_masp_nullifier_key, - is_masp_tx_pin_key, is_masp_tx_prefix_key, Transfer, }; use namada_sdk::masp::verify_shielded_tx; use ripemd::Digest as RipemdDigest; @@ -113,7 +112,7 @@ where // Check that the transaction correctly revealed the nullifiers fn valid_nullifiers_reveal( &self, - keys_changed: &BTreeSet, + keys_changed: &[&Key], transaction: &Transaction, ) -> Result { let mut revealed_nullifiers = HashSet::new(); @@ -285,78 +284,6 @@ where Ok(true) } - - /// Check the correctness of the general storage changes that pertain to all - /// types of masp transfers - fn valid_state( - &self, - keys_changed: &BTreeSet, - transfer: &Transfer, - transaction: &Transaction, - ) -> Result { - // Check that the transaction didn't write unallowed masp keys, nor - // multiple variations of the same key prefixes - let mut found_tx_key = false; - let mut found_pin_key = false; - for key in keys_changed.iter().filter(|key| is_masp_key(key)) { - if !is_masp_allowed_key(key) { - return Ok(false); - } else if is_masp_tx_prefix_key(key) { - if found_tx_key { - return Ok(false); - } else { - found_tx_key = true; - } - } else if is_masp_tx_pin_key(key) { - if found_pin_key { - return Ok(false); - } else { - found_pin_key = true; - } - } - } - - // Validate head tx - let head_tx_key = namada_core::types::token::masp_head_tx_key(); - let pre_head: u64 = self.ctx.read_pre(&head_tx_key)?.unwrap_or(0); - let post_head: u64 = self.ctx.read_post(&head_tx_key)?.unwrap_or(0); - - if post_head != pre_head + 1 { - return Ok(false); - } - - // Validate tx key - let current_tx_key = namada_core::types::token::masp_tx_key(pre_head); - match self - .ctx - .read_post::<(Epoch, BlockHeight, TxIndex, Transfer, Transaction)>( - ¤t_tx_key, - )? { - Some(( - epoch, - height, - tx_index, - storage_transfer, - storage_transaction, - )) if (epoch == self.ctx.get_block_epoch()? - && height == self.ctx.get_block_height()? - && tx_index == self.ctx.get_tx_index()? - && &storage_transfer == transfer - && &storage_transaction == transaction) => {} - _ => return Ok(false), - } - - // Validate pin key - if let Some(key) = &transfer.key { - let pin_key = namada_core::types::token::masp_pin_tx_key(key); - match self.ctx.read_post::(&pin_key)? { - Some(tx_idx) if tx_idx == pre_head => (), - _ => return Ok(false), - } - } - - Ok(true) - } } impl<'a, DB, H, CA> NativeVp for MaspVp<'a, DB, H, CA> @@ -382,7 +309,15 @@ where // The Sapling value balance adds to the transparent tx pool transparent_tx_pool += shielded_tx.sapling_value_balance(); - if !self.valid_state(keys_changed, &transfer, &shielded_tx)? { + // Check that the transaction didn't write unallowed masp keys + let masp_keys_changed: Vec<&Key> = + keys_changed.iter().filter(|key| is_masp_key(key)).collect(); + if masp_keys_changed + .iter() + .filter(|key| !is_masp_allowed_key(key)) + .count() + != 0 + { return Ok(false); } @@ -435,7 +370,10 @@ where if !(self.valid_spend_descriptions_anchor(&shielded_tx)? && self.valid_convert_descriptions_anchor(&shielded_tx)? - && self.valid_nullifiers_reveal(keys_changed, &shielded_tx)?) + && self.valid_nullifiers_reveal( + &masp_keys_changed, + &shielded_tx, + )?) { return Ok(false); } From c2acfec34c220a6e727f8198a86dc4b8c71ee223 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 4 Jan 2024 12:52:43 +0100 Subject: [PATCH 10/26] Adds a new tx event attribute to index masp txs --- .../lib/node/ledger/shell/finalize_block.rs | 19 ++++++++++++++++++- shared/src/ledger/protocol/mod.rs | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 4ab90bbf62..4683de0be0 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -395,6 +395,7 @@ where }, }; + let mut is_committed_fee_unshield = false; match protocol::dispatch_tx( tx, processed_tx.tx.as_ref(), @@ -408,6 +409,7 @@ where &mut self.vp_wasm_cache, &mut self.tx_wasm_cache, Some(&native_block_proposer_address), + &mut is_committed_fee_unshield, ) .map_err(Error::TxApply) { @@ -419,6 +421,9 @@ where "Wrapper transaction {} was accepted", tx_event["hash"] ); + if is_committed_fee_unshield { + tx_event["is_valid_masp_tx"] = String::new(); + } self.wl_storage.storage.tx_queue.push(TxInQueue { tx: wrapper.expect("Missing expected wrapper"), gas: tx_gas_meter.get_available_gas(), @@ -430,6 +435,13 @@ where tx_event["hash"], result ); + if result.vps_result.accepted_vps.contains( + &Address::Internal( + address::InternalAddress::Masp, + ), + ) { + tx_event["is_valid_masp_tx"] = String::new(); + } changed_keys .extend(result.changed_keys.iter().cloned()); stats.increment_successful_txs(); @@ -500,7 +512,7 @@ where msg ); - // If transaction type is Decrypted and didn't failed + // If transaction type is Decrypted and didn't fail // because of out of gas nor invalid // section commitment, commit its hash to prevent replays if let Some(wrapper) = embedding_wrapper { @@ -540,6 +552,11 @@ where if let EventType::Accepted = tx_event.event_type { // If wrapper, invalid tx error code tx_event["code"] = ResultCode::InvalidTx.into(); + // The fee unshield operation could still have been + // committed + if is_committed_fee_unshield { + tx_event["is_valid_masp_tx"] = String::new(); + } } else { tx_event["code"] = ResultCode::WasmRuntimeError.into(); } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index e51d073415..d009b35bea 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -150,7 +150,10 @@ pub fn dispatch_tx<'a, D, H, CA>( wl_storage: &'a mut WlStorage, vp_wasm_cache: &'a mut VpCache, tx_wasm_cache: &'a mut TxCache, + // FIXME: these two params together because they are only needed for + // wrappers block_proposer: Option<&'a Address>, + is_committed_fee_unshield: &mut bool, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -187,6 +190,7 @@ where tx_wasm_cache, }, block_proposer, + is_committed_fee_unshield, )?; Ok(TxResult { gas_used: tx_gas_meter.get_tx_consumed_gas(), @@ -232,6 +236,7 @@ pub(crate) fn apply_wrapper_tx<'a, D, H, CA, WLS>( tx_bytes: &[u8], mut shell_params: ShellParams<'a, CA, WLS>, block_proposer: Option<&Address>, + is_committed_fee_unshield: &mut bool, ) -> Result> where CA: 'static + WasmCacheAccess + Sync, @@ -255,6 +260,7 @@ where block_proposer, &mut changed_keys, )?; + *is_committed_fee_unshield = true; // Account for gas shell_params From 0d0fd11490e848b6082f6e763991ea7b8f28bf20 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 4 Jan 2024 21:29:15 +0100 Subject: [PATCH 11/26] Fetches masp txs from blocks --- apps/src/lib/client/rpc.rs | 8 +- .../lib/node/ledger/shell/finalize_block.rs | 10 + apps/src/lib/node/ledger/shell/governance.rs | 1 + .../src/lib/node/ledger/shell/testing/node.rs | 6 +- core/src/ledger/ibc/context/storage.rs | 2 + core/src/ledger/ibc/mod.rs | 6 +- core/src/ledger/masp_utils.rs | 21 +- core/src/ledger/vp_env.rs | 1 + core/src/types/ibc.rs | 2 + core/src/types/storage.rs | 21 + core/src/types/token.rs | 14 +- sdk/src/masp.rs | 436 ++++++++++++++---- sdk/src/queries/mod.rs | 7 +- shared/src/ledger/mod.rs | 1 + shared/src/ledger/native_vp/ibc/context.rs | 4 +- shared/src/ledger/native_vp/masp.rs | 46 +- shared/src/vm/host_env.rs | 3 +- tx_prelude/src/ibc.rs | 3 +- wasm/wasm_source/src/tx_transfer.rs | 6 +- 19 files changed, 492 insertions(+), 106 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index bc57f58f14..e0178dd33d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,7 +44,9 @@ use namada::types::ibc::{is_ibc_denom, IbcTokenHash}; use namada::types::io::Io; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; -use namada::types::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; +use namada::types::storage::{ + BlockHeight, BlockResults, Epoch, IndexedTx, Key, KeySeg, +}; use namada::types::token::{Change, MaspDenom}; use namada::types::{storage, token}; use namada_sdk::error::{is_pinned_error, Error, PinnedBalanceError}; @@ -145,7 +147,9 @@ pub async fn query_transfers( .map(|fvk| (ExtendedFullViewingKey::from(*fvk).fvk.vk, fvk)) .collect(); // Now display historical shielded and transparent transactions - for ((height, idx), (epoch, tfer_delta, tx_delta)) in transfers { + for (IndexedTx { height, index: idx }, (epoch, tfer_delta, tx_delta)) in + transfers + { // Check if this transfer pertains to the supplied owner let mut relevant = match &query_owner { Either::Left(BalanceOwner::FullViewingKey(fvk)) => tx_delta diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 4683de0be0..8ce68c6ae1 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -470,6 +470,16 @@ where let mut event = Event::from(ibc_event); // Add the height for IBC event query event["height"] = height.to_string(); + if tx_event + .attributes + .contains_key("is_valid_masp_tx") + { + // Add the tx index for masp txs clients + // queries + // FIXME: review this + event["is_valid_masp_tx"] = + tx_index.to_string(); + } event }) // eth bridge events diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 4991310d09..ea64fa0c51 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -287,6 +287,7 @@ where &mut shell.vp_wasm_cache, &mut shell.tx_wasm_cache, None, + &mut false, ); shell .wl_storage diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index b3ef78027e..5bad5fd829 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -761,10 +761,12 @@ impl<'a> Client for &'a MockNode { height: H, ) -> Result where - H: Into + Send, + H: TryInto + Send, { self.drive_mock_services_bg().await; - let height = height.into(); + let height = height.try_into().map_err(|_| { + RpcError::parse("Could not parse block height".to_string()) + })?; let encoded_event = EncodedEvent(height.value()); let locked = self.shell.lock().unwrap(); let events: Vec<_> = locked diff --git a/core/src/ledger/ibc/context/storage.rs b/core/src/ledger/ibc/context/storage.rs index a5e79e6e1f..8ff16cf868 100644 --- a/core/src/ledger/ibc/context/storage.rs +++ b/core/src/ledger/ibc/context/storage.rs @@ -28,9 +28,11 @@ pub trait IbcStorageContext: StorageRead + StorageWrite { ) -> Result<(), Error>; /// Handle masp tx + // FIXME: try again to remove tx_index from some places fn handle_masp_tx( &mut self, shielded: &masp_primitives::transaction::Transaction, + pin_key: Option<&str>, ) -> Result<(), Error>; /// Mint token diff --git a/core/src/ledger/ibc/mod.rs b/core/src/ledger/ibc/mod.rs index 11afd339e1..cd45eb054e 100644 --- a/core/src/ledger/ibc/mod.rs +++ b/core/src/ledger/ibc/mod.rs @@ -102,6 +102,7 @@ where pub fn execute(&mut self, tx_data: &[u8]) -> Result<(), Error> { let message = decode_message(tx_data)?; match &message { + // FIXME: look here for MASP on IBC IbcMessage::Transfer(msg) => { let mut token_transfer_ctx = TokenTransferContext::new(self.ctx.inner.clone()); @@ -282,7 +283,10 @@ where self.ctx .inner .borrow_mut() - .handle_masp_tx(&shielded_transfer.masp_tx) + .handle_masp_tx( + &shielded_transfer.masp_tx, + shielded_transfer.transfer.key.as_deref(), + ) .map_err(|_| { Error::MaspTx("Writing MASP components failed".to_string()) })?; diff --git a/core/src/ledger/masp_utils.rs b/core/src/ledger/masp_utils.rs index 37631e4855..343c7b1c73 100644 --- a/core/src/ledger/masp_utils.rs +++ b/core/src/ledger/masp_utils.rs @@ -6,7 +6,11 @@ use masp_primitives::transaction::Transaction; use super::storage_api::{StorageRead, StorageWrite}; use crate::ledger::storage_api::{Error, Result}; -use crate::types::token::{masp_commitment_tree_key, masp_nullifier_key}; +use crate::types::address::MASP; +use crate::types::storage::{IndexedTx, Key, KeySeg}; +use crate::types::token::{ + masp_commitment_tree_key, masp_nullifier_key, PIN_KEY_PREFIX, +}; // Writes the nullifiers of the provided masp transaction to storage fn reveal_nullifiers( @@ -59,6 +63,7 @@ pub fn update_note_commitment_tree( pub fn handle_masp_tx( ctx: &mut (impl StorageRead + StorageWrite), shielded: &Transaction, + pin_key: Option<&str>, ) -> Result<()> { // TODO: temporarily disabled because of the node aggregation issue in WASM. // Using the host env tx_update_masp_note_commitment_tree or directly the @@ -66,5 +71,19 @@ pub fn handle_masp_tx( // update_note_commitment_tree(ctx, shielded)?; reveal_nullifiers(ctx, shielded)?; + // If storage key has been supplied, then pin this transaction to it + if let Some(key) = pin_key { + let pin_key = Key::from(MASP.to_db_key()) + .push(&(PIN_KEY_PREFIX.to_owned() + key)) + .expect("Cannot obtain a storage key"); + ctx.write( + &pin_key, + IndexedTx { + height: ctx.get_block_height()?, + index: ctx.get_tx_index()?, + }, + )?; + } + Ok(()) } diff --git a/core/src/ledger/vp_env.rs b/core/src/ledger/vp_env.rs index 728e66d9a6..1005e11070 100644 --- a/core/src/ledger/vp_env.rs +++ b/core/src/ledger/vp_env.rs @@ -110,6 +110,7 @@ where /// Get the shielded action including the transfer and the masp tx fn get_shielded_action( + // FIXME: I need this &self, tx_data: &Tx, ) -> Result<(Transfer, Transaction), storage_api::Error> { diff --git a/core/src/types/ibc.rs b/core/src/types/ibc.rs index cfb2357fef..eb060c9f07 100644 --- a/core/src/types/ibc.rs +++ b/core/src/types/ibc.rs @@ -239,6 +239,8 @@ pub fn get_shielded_transfer( return Ok(None); } + // FIXME: I should place the is_masp_tx attribute directly on the ibc event + // not in finalize block FIXME: maybe it's not possible event .attributes .get("memo") diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 87d1b80e50..e85cec7b30 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -1433,6 +1433,27 @@ impl GetEventNonce for InnerEthEventsQueue { } } +/// Represents the pointers of an indexed tx, which are the block height and the +/// index inside that block +#[derive( + Default, + Debug, + Copy, + Clone, + BorshSerialize, + BorshDeserialize, + Eq, + PartialEq, + Ord, + PartialOrd, +)] +pub struct IndexedTx { + /// The block height of the indexed tx + pub height: BlockHeight, + /// The index in the block of the tx + pub index: TxIndex, +} + #[cfg(test)] /// Tests and strategies for storage pub mod tests { diff --git a/core/src/types/token.rs b/core/src/types/token.rs index da591466d9..4429a0caa4 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -978,6 +978,8 @@ pub const DENOM_STORAGE_KEY: &str = "denomination"; pub const MINTER_STORAGE_KEY: &str = "minter"; /// Key segment for minted balance pub const MINTED_STORAGE_KEY: &str = "minted"; +/// Key segment prefix for pinned shielded transactions +pub const PIN_KEY_PREFIX: &str = "pin-"; /// Key segment prefix for the nullifiers pub const MASP_NULLIFIERS_KEY: &str = "nullifiers"; /// Key segment prefix for the note commitment merkle tree @@ -1217,7 +1219,10 @@ pub fn is_masp_key(key: &Key) -> bool { pub fn is_masp_allowed_key(key: &Key) -> bool { match &key.segments[..] { [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] - if *addr == MASP && key == MASP_NOTE_COMMITMENT_TREE_KEY => + if *addr == MASP + //FIXME: place the check back in the masp vp if needed + && (key.starts_with(PIN_KEY_PREFIX) + || key == MASP_NOTE_COMMITMENT_TREE_KEY) => { true } @@ -1258,6 +1263,13 @@ pub fn masp_last_inflation_key(token_address: &Address) -> Key { ) } +/// Get a key for a masp pin +pub fn masp_pin_tx_key(key: &str) -> Key { + Key::from(MASP.to_db_key()) + .push(&(PIN_KEY_PREFIX.to_owned() + key)) + .expect("Cannot obtain a storage key") +} + /// Get a key for a masp nullifier pub fn masp_nullifier_key(nullifier: &Nullifier) -> Key { Key::from(MASP.to_db_key()) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 62b4ba1ea2..a671b93072 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -5,6 +5,7 @@ use std::env; use std::fmt::Debug; use std::ops::Deref; use std::path::PathBuf; +use std::str::FromStr; // use async_std::io::prelude::WriteExt; // use async_std::io::{self}; @@ -49,12 +50,14 @@ use masp_proofs::bellman::groth16::PreparedVerifyingKey; use masp_proofs::bls12_381::Bls12; use masp_proofs::prover::LocalTxProver; use masp_proofs::sapling::SaplingVerificationContext; +use namada_core::ibc::apps::transfer::types::Memo; use namada_core::types::address::{Address, MASP}; +use namada_core::types::ibc::IbcShieldedTransfer; use namada_core::types::masp::{ BalanceOwner, ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, }; -use namada_core::types::storage::{BlockHeight, Epoch, TxIndex}; +use namada_core::types::storage::{BlockHeight, Epoch, IndexedTx, TxIndex}; use namada_core::types::time::{DateTimeUtc, DurationSecs}; use namada_core::types::token; use namada_core::types::token::{Change, MaspDenom, Transfer}; @@ -67,10 +70,11 @@ use thiserror::Error; #[cfg(feature = "testing")] use crate::error::EncodingError; use crate::error::{Error, PinnedBalanceError, QueryError}; +use crate::events::EventType; use crate::io::Io; use crate::proto::Tx; use crate::queries::Client; -use crate::rpc::{query_conversion, query_storage_value}; +use crate::rpc::{query_block, query_conversion, query_epoch_at_height}; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; use crate::tx::decode_component; @@ -597,8 +601,8 @@ pub struct ShieldedContext { /// Location where this shielded context is saved #[borsh(skip)] pub utils: U, - /// The last transaction index to be processed in this context - pub last_txidx: u64, + /// The last indexed transaction to be processed in this context + pub last_indexed: IndexedTx, /// The commitment tree produced by scanning all transactions up to tx_pos pub tree: CommitmentTree, /// Maps viewing keys to applicable note positions @@ -614,10 +618,8 @@ pub struct ShieldedContext { /// Maps note positions to their witness (used to make merkle paths) pub witness_map: HashMap>, /// Tracks what each transaction does to various account balances - pub delta_map: BTreeMap< - (BlockHeight, TxIndex), - (Epoch, TransferDelta, TransactionDelta), - >, + pub delta_map: + BTreeMap, /// The set of note positions that have been spent pub spents: HashSet, /// Maps asset types to their decodings @@ -632,7 +634,7 @@ impl Default for ShieldedContext { fn default() -> ShieldedContext { ShieldedContext:: { utils: U::default(), - last_txidx: u64::default(), + last_indexed: IndexedTx::default(), tree: CommitmentTree::empty(), pos_map: HashMap::default(), nf_map: HashMap::default(), @@ -664,7 +666,7 @@ impl ShieldedContext { /// context. It must be the case that the two shielded contexts share the /// same last transaction ID and share identical commitment trees. pub fn merge(&mut self, new_ctx: ShieldedContext) { - debug_assert_eq!(self.last_txidx, new_ctx.last_txidx); + debug_assert_eq!(self.last_indexed, new_ctx.last_indexed); // Merge by simply extending maps. Identical keys should contain // identical values, so overwriting should not be problematic. self.pos_map.extend(new_ctx.pos_map); @@ -679,10 +681,10 @@ impl ShieldedContext { // The deltas are the exception because different keys can reveal // different parts of the same transaction. Hence each delta needs to be // merged separately. - for ((height, idx), (ep, ntfer_delta, ntx_delta)) in new_ctx.delta_map { + for (height, (ep, ntfer_delta, ntx_delta)) in new_ctx.delta_map { let (_ep, tfer_delta, tx_delta) = self .delta_map - .entry((height, idx)) + .entry(height) .or_insert((ep, TransferDelta::new(), TransactionDelta::new())); tfer_delta.extend(ntfer_delta); tx_delta.extend(ntx_delta); @@ -718,7 +720,14 @@ impl ShieldedContext { let (txs, mut tx_iter); if !unknown_keys.is_empty() { // Load all transactions accepted until this point - txs = Self::fetch_shielded_transfers(client, 0).await?; + txs = Self::fetch_shielded_transfers( + client, + IndexedTx { + height: BlockHeight::first(), + index: TxIndex::default(), + }, + ) + .await?; tx_iter = txs.iter(); // Do this by constructing a shielding context only for unknown keys let mut tx_ctx = Self { @@ -729,11 +738,10 @@ impl ShieldedContext { tx_ctx.pos_map.entry(vk).or_insert_with(BTreeSet::new); } // Update this unknown shielded context until it is level with self - while tx_ctx.last_txidx != self.last_txidx { - if let Some(((height, idx), (epoch, tx, stx))) = tx_iter.next() - { + while tx_ctx.last_indexed != self.last_indexed { + if let Some((indexed_tx, (epoch, tx, stx))) = tx_iter.next() { tx_ctx - .scan_tx(client, *height, *idx, *epoch, tx, stx) + .scan_tx(client, *indexed_tx, *epoch, tx, stx) .await?; } else { break; @@ -744,54 +752,270 @@ impl ShieldedContext { self.merge(tx_ctx); } else { // Load only transactions accepted from last_txid until this point - txs = - Self::fetch_shielded_transfers(client, self.last_txidx).await?; + txs = Self::fetch_shielded_transfers(client, self.last_indexed) + .await?; tx_iter = txs.iter(); } // Now that we possess the unspent notes corresponding to both old and // new keys up until tx_pos, proceed to scan the new transactions. - for ((height, idx), (epoch, tx, stx)) in &mut tx_iter { - self.scan_tx(client, *height, *idx, *epoch, tx, stx).await?; + for (indexed_tx, (epoch, tx, stx)) in &mut tx_iter { + self.scan_tx(client, *indexed_tx, *epoch, tx, stx).await?; } Ok(()) } /// Obtain a chronologically-ordered list of all accepted shielded - /// transactions from the ledger. The ledger conceptually stores - /// transactions as a vector. More concretely, the HEAD_TX_KEY location - /// stores the index of the last accepted transaction and each transaction - /// is stored at a key derived from its index. + /// transactions from a node. pub async fn fetch_shielded_transfers( client: &C, - last_txidx: u64, - ) -> Result< - BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer, Transaction)>, - Error, - > { - // Construct the key where last transaction pointer is stored - let head_tx_key = namada_core::types::token::masp_head_tx_key(); - // Query for the index of the last accepted transaction - let head_txidx = query_storage_value::(client, &head_tx_key) - .await - .unwrap_or(0); + last_indexed_tx: IndexedTx, + ) -> Result, Error> + { + // Query for the last produced block height + let last_block_height = query_block(client) + .await? + .map_or_else(BlockHeight::first, |block| block.height); + let mut shielded_txs = BTreeMap::new(); // Fetch all the transactions we do not have yet - for i in last_txidx..head_txidx { - // Construct the key for where the current transaction is stored - let current_tx_key = namada_core::types::token::masp_tx_key(i); - // Obtain the current transaction - let (tx_epoch, tx_height, tx_index, current_tx, current_stx) = - query_storage_value::< - C, - (Epoch, BlockHeight, TxIndex, Transfer, Transaction), - >(client, ¤t_tx_key) - .await?; - // Collect the current transaction - shielded_txs.insert( - (tx_height, tx_index), - (tx_epoch, current_tx, current_stx), - ); + for height in u64::from(last_indexed_tx.height)..=last_block_height.0 { + // Get the valid masp transactions at the specified height + // FIXME: review if we really need extra key for ibc events + let tx_query = + Query::eq("height", height).and_exists("is_valid_masp_tx"); + + let epoch = query_epoch_at_height(client, height.into()) + .await? + .ok_or_else(|| { + Error::from(QueryError::General(format!( + "Queried height is greater than the last committed \ + block height" + ))) + })?; + + // Paginate the results + for page in 1.. { + let txs_results = client + .tx_search( + tx_query.clone(), + // TODO: currently we are not verifying the merkle + // proof, we should or at least ask a parameter to the + // user ot decide if we want proofs(and their + // verification) or not + false, + page, + 100, + Order::Ascending, + ) + .await + .map_err(|e| { + Error::from(QueryError::General(e.to_string())) + })? + .txs; + + for result in &txs_results { + let tx_index = TxIndex(result.index); + if BlockHeight(height) == last_indexed_tx.height + && tx_index <= last_indexed_tx.index + { + continue; + } + let tx = Tx::try_from(result.tx.as_ref()) + .map_err(|e| Error::Other(e.to_string()))?; + let tx_header = tx.header(); + // NOTE: simply looking for masp sections attached to the tx + // is not safe. We don't validate the sections attached to a + // transaction se we could end up with transactions carrying + // an unnecessary masp section. We must instead look for the + // required masp sections in the signed commitments (hashes) + // of the transactions' headers/data sections + if let Some(wrapper_header) = tx_header.wrapper() { + let hash = wrapper_header + .unshield_section_hash + .ok_or_else(|| { + Error::Other( + "Missing expected fee unshielding section \ + hash" + .to_string(), + ) + })?; + + let masp_transaction = tx + .get_section(&hash) + .ok_or_else(|| { + Error::Other( + "Missing expected masp section".to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other( + "Missing masp transaction".to_string(), + ) + })?; + // FIXME: actually, do I realy need the entire transfer + // object? Probably not mayube we can remove + // FIXME: mroe in general. review everything we are + // storing to see if we actually need it + + // Transfer objects for fee unshielding are absent from + // the tx because they are completely constructed in + // protocol, need to recreate it here + let transfer = Transfer { + source: MASP, + target: wrapper_header.fee_payer(), + token: wrapper_header.fee.token.clone(), + amount: wrapper_header + .get_tx_fee() + .map_err(|e| Error::Other(e.to_string()))?, + key: None, + shielded: Some(hash), + }; + + // Collect the current transaction + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: tx_index, + }, + (epoch, transfer, masp_transaction), + ); + } else if tx_header.decrypted().is_some() { + let (transfer, masp_transaction) = + match Transfer::try_from_slice( + &tx.data().ok_or_else(|| { + Error::Other( + "Missing data section".to_string(), + ) + })?, + ) { + Ok(transfer) => { + let masp_transaction = tx + .get_section( + &transfer.shielded.ok_or_else( + || { + Error::Other( + "Missing masp section \ + hash" + .to_string(), + ) + }, + )?, + ) + .ok_or_else(|| { + Error::Other( + "Missing masp section in \ + transaction" + .to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other( + "Missing masp transaction" + .to_string(), + ) + })?; + + (transfer, masp_transaction) + } + Err(_) => { + // This should be a MASP over IBC transfer, + // continue for now, we'll scan the ibc + // events later + continue; + } + }; + + // Collect the current transaction + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: tx_index, + }, + (epoch, transfer, masp_transaction), + ); + } + } + + // FIXME: are we already storing masp transactions when we + // submit them? In this case I need to make sure we don't + // reqrite them from here An incomplete page + // signifies no more transactions + if txs_results.len() < 100 { + break; + } + } } + + // Fetch all block events to look for MASP over IBC transactions + for height in u64::from(last_indexed_tx.height)..=last_block_height.0 { + let epoch = query_epoch_at_height(client, height.into()) + .await? + .ok_or_else(|| { + Error::from(QueryError::General(format!( + "Queried height is greater than the last committed \ + block height" + ))) + })?; + + let events = client + .block_results(height) + .await + .map_err(|e| Error::from(QueryError::General(e.to_string())))? + .end_block_events + .unwrap_or_default(); + + for (ibc_masp_event, tx_index) in + events.iter().filter_map(|event| { + if event + .kind + .starts_with(&EventType::Ibc(String::new()).to_string()) + { + event + .attributes + .iter() + .find(|attribute| { + &attribute.key == "is_valid_masp_tx" + }) + .map(|tx_index| (event, &tx_index.value)) + } else { + None + } + }) + { + // FIXME: can we parallelize stuff somewhere when constructing + // the internal state of the ShieldeContext? + // Masp transaction, collect it + let shielded_transfer = ibc_masp_event + .attributes + .iter() + .find(|attribute| &attribute.key == "memo") + .map(|memo| { + IbcShieldedTransfer::try_from(Memo::from( + memo.value.clone(), + )) + }); + + if let Some(Ok(shielded_transfer)) = shielded_transfer { + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: TxIndex( + u32::from_str(tx_index) + .map_err(|e| Error::Other(e.to_string()))?, + ), + }, + ( + epoch, + shielded_transfer.transfer, + shielded_transfer.masp_tx, + ), + ); + } + } + } + Ok(shielded_txs) } @@ -806,8 +1030,7 @@ impl ShieldedContext { pub async fn scan_tx( &mut self, client: &C, - height: BlockHeight, - index: TxIndex, + indexed_tx: IndexedTx, epoch: Epoch, tx: &Transfer, shielded: &Transaction, @@ -933,12 +1156,10 @@ impl ShieldedContext { change: -tx.amount.amount().change(), }, ); - self.last_txidx += 1; + self.last_indexed = indexed_tx; - self.delta_map.insert( - (height, index), - (epoch, transfer_delta, transaction_delta), - ); + self.delta_map + .insert(indexed_tx, (epoch, transfer_delta, transaction_delta)); Ok(()) } @@ -946,10 +1167,7 @@ impl ShieldedContext { /// Transfer in this context pub fn get_tx_deltas( &self, - ) -> &BTreeMap< - (BlockHeight, TxIndex), - (Epoch, TransferDelta, TransactionDelta), - > { + ) -> &BTreeMap { &self.delta_map } @@ -1363,21 +1581,73 @@ impl ShieldedContext { // Obtain the transaction pointer at the key // If we don't discard the error message then a test fails, // however the error underlying this will go undetected - let txidx = rpc::query_storage_value::(client, &pin_key) - .await - .map_err(|_| PinnedBalanceError::NoTransactionPinned)?; - // Construct the key for where the pinned transaction is stored - let tx_key = namada_core::types::token::masp_tx_key(txidx); - // Obtain the pointed to transaction - let (tx_epoch, _tx_height, _tx_index, _tx, shielded) = - rpc::query_storage_value::< - C, - (Epoch, BlockHeight, TxIndex, Transfer, Transaction), - >(client, &tx_key) - .await - .map_err(|_| { - Error::Other("Ill-formed epoch, transaction pair".to_string()) + let indexed_tx = + rpc::query_storage_value::(client, &pin_key) + .await + .map_err(|_| PinnedBalanceError::NoTransactionPinned)?; + let tx_epoch = query_epoch_at_height(client, indexed_tx.height) + .await? + .ok_or_else(|| { + Error::from(QueryError::General(format!( + "Queried height is greater than the last committed block \ + height" + ))) })?; + + let tx_query = Query::eq("height", indexed_tx.height.to_string()) + .and_eq("tx.index", indexed_tx.index.to_string()); + let tx_results = client + .tx_search( + tx_query.clone(), + // TODO: currently we are not verifying the merkle + // proof, we should or at least ask a parameter to the + // user ot decide if we want proofs(and their + // verification) or not + false, + 1, + 100, + Order::Ascending, + ) + .await + .map_err(|e| Error::from(QueryError::General(e.to_string())))? + .txs; + + let tx = Tx::try_from( + tx_results + .first() + .ok_or_else(|| { + Error::Other("Missing expected pinned masp tx".to_string()) + })? + .tx + .as_ref(), + ) + .map_err(|e| Error::Other(e.to_string()))?; + + let shielded = + match Transfer::try_from_slice(&tx.data().ok_or_else(|| { + Error::Other("Missing data section".to_string()) + })?) { + Ok(transfer) => tx + .get_section(&transfer.shielded.ok_or_else(|| { + Error::Other("Missing masp section hash".to_string()) + })?) + .ok_or_else(|| { + Error::Other( + "Missing masp section in transaction".to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other("Missing masp transaction".to_string()) + })?, + Err(_) => { + // FIXME: add support for pinned ibc masp txs + // FIXME: probably need to review also how we do it in + // fewtch_shielded_transfer + return Err(Error::Other("IBC Masp pinned tx".to_string())); + } + }; + // Accumulate the combined output note value into this Amount let mut val_acc = I128Sum::zero(); for so in shielded @@ -1516,8 +1786,6 @@ impl ShieldedContext { // No shielded components are needed when neither source nor destination // are shielded - use std::str::FromStr; - use rand::rngs::StdRng; use rand_core::SeedableRng; @@ -1891,10 +2159,7 @@ impl ShieldedContext { query_token: &Option
, viewing_keys: &HashMap, ) -> Result< - BTreeMap< - (BlockHeight, TxIndex), - (Epoch, TransferDelta, TransactionDelta), - >, + BTreeMap, Error, > { const TXS_PER_PAGE: u8 = 100; @@ -1909,6 +2174,7 @@ impl ShieldedContext { let _ = self.save().await; // Required for filtering out rejected transactions from Tendermint // responses + // FIXME: here we query the reulst of only the last block let block_results = rpc::query_results(client).await?; let mut transfers = self.get_tx_deltas().clone(); // Construct the set of addresses relevant to user's query @@ -1925,6 +2191,8 @@ impl ShieldedContext { for addr in relevant_addrs { for prop in ["transfer.source", "transfer.target"] { // Query transactions involving the current address + // FIXME: bnut it seems like here we query all transactions, not + // only those from the last block let mut tx_query = Query::eq(prop, addr.encode()); // Elaborate the query if requested by the user if let Some(token) = &query_token { @@ -1953,7 +2221,7 @@ impl ShieldedContext { // Only process yet unprocessed transactions which have // been accepted by node VPs let should_process = !transfers - .contains_key(&(height, idx)) + .contains_key(&IndexedTx { height, index: idx }) && block_results[u64::from(height) as usize] .is_accepted(idx.0 as usize); if !should_process { @@ -1989,7 +2257,7 @@ impl ShieldedContext { // No shielded accounts are affected by this // Transfer transfers.insert( - (height, idx), + IndexedTx { height, index: idx }, (epoch, delta, TransactionDelta::new()), ); } diff --git a/sdk/src/queries/mod.rs b/sdk/src/queries/mod.rs index 4dbc5173b8..7506b4520d 100644 --- a/sdk/src/queries/mod.rs +++ b/sdk/src/queries/mod.rs @@ -301,10 +301,13 @@ pub trait Client { height: H, ) -> Result where - H: Into + Send, + H: TryInto + Send, { + let height = height.try_into().map_err(|_| { + RpcError::parse("Could not parse to height".to_string()) + })?; self.perform(tendermint_rpc::endpoint::block_results::Request::new( - height.into(), + height, )) .await } diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index abd96064bb..1a28ff12f4 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -71,6 +71,7 @@ mod dry_run_tx { &mut ctx.tx_wasm_cache, ), None, + &mut false, ) .into_storage_result()?; diff --git a/shared/src/ledger/native_vp/ibc/context.rs b/shared/src/ledger/native_vp/ibc/context.rs index 968ff53329..29b3ab901b 100644 --- a/shared/src/ledger/native_vp/ibc/context.rs +++ b/shared/src/ledger/native_vp/ibc/context.rs @@ -216,8 +216,9 @@ where fn handle_masp_tx( &mut self, shielded: &masp_primitives::transaction::Transaction, + pin_key: Option<&str>, ) -> Result<()> { - masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::handle_masp_tx(self, shielded, pin_key)?; masp_utils::update_note_commitment_tree(self, shielded) } @@ -418,6 +419,7 @@ where fn handle_masp_tx( &mut self, _shielded: &masp_primitives::transaction::Transaction, + _pin_key: Option<&str>, ) -> Result<()> { unimplemented!("Validation doesn't handle a masp tx") } diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index f7811245f8..4bdbc94ca9 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -14,11 +14,12 @@ use namada_core::ledger::storage; use namada_core::ledger::storage_api::OptionExt; use namada_core::ledger::vp_env::VpEnv; use namada_core::proto::Tx; -use namada_core::types::address::Address; use namada_core::types::address::InternalAddress::Masp; -use namada_core::types::storage::{Epoch, Key}; +use namada_core::types::address::{Address, MASP}; +use namada_core::types::storage::{Epoch, IndexedTx, Key, KeySeg}; use namada_core::types::token::{ self, is_masp_allowed_key, is_masp_key, is_masp_nullifier_key, + PIN_KEY_PREFIX, }; use namada_sdk::masp::verify_shielded_tx; use ripemd::Digest as RipemdDigest; @@ -284,6 +285,35 @@ where Ok(true) } + + fn valid_state( + &self, + masp_keys_changed: &[&Key], + pin_key: Option<&str>, + ) -> Result { + // Check that the transaction didn't write unallowed masp keys + if masp_keys_changed + .iter() + .any(|key| !is_masp_allowed_key(key)) + { + return Ok(false); + } + + // Validate pin key + if let Some(key) = pin_key { + let pin_key = Key::from(MASP.to_db_key()) + .push(&(PIN_KEY_PREFIX.to_owned() + key)) + .expect("Cannot obtain a storage key"); + match self.ctx.read_post::(&pin_key)? { + Some(IndexedTx { height, index }) + if height == self.ctx.get_block_height()? + && index == self.ctx.get_tx_index()? => {} + _ => return Ok(false), + } + } + + Ok(true) + } } impl<'a, DB, H, CA> NativeVp for MaspVp<'a, DB, H, CA> @@ -309,15 +339,13 @@ where // The Sapling value balance adds to the transparent tx pool transparent_tx_pool += shielded_tx.sapling_value_balance(); - // Check that the transaction didn't write unallowed masp keys + // Check the validity of the keys let masp_keys_changed: Vec<&Key> = keys_changed.iter().filter(|key| is_masp_key(key)).collect(); - if masp_keys_changed - .iter() - .filter(|key| !is_masp_allowed_key(key)) - .count() - != 0 - { + if !self.valid_state( + masp_keys_changed.as_slice(), + transfer.key.as_deref(), + )? { return Ok(false); } diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index af2b808892..85fda430ea 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -2557,8 +2557,9 @@ where fn handle_masp_tx( &mut self, shielded: &masp_primitives::transaction::Transaction, + pin_key: Option<&str>, ) -> Result<(), storage_api::Error> { - masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::handle_masp_tx(self, shielded, pin_key)?; masp_utils::update_note_commitment_tree(self, shielded) } diff --git a/tx_prelude/src/ibc.rs b/tx_prelude/src/ibc.rs index 4a04e2a980..2418e4b8f0 100644 --- a/tx_prelude/src/ibc.rs +++ b/tx_prelude/src/ibc.rs @@ -52,8 +52,9 @@ impl IbcStorageContext for Ctx { fn handle_masp_tx( &mut self, shielded: &masp_primitives::transaction::Transaction, + pin_key: Option<&str>, ) -> Result<(), Error> { - masp_utils::handle_masp_tx(self, shielded)?; + masp_utils::handle_masp_tx(self, shielded, pin_key)?; masp_utils::update_note_commitment_tree(self, shielded) } diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index f7874e33ec..34c3045dd9 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -38,7 +38,11 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { }) .transpose()?; if let Some(shielded) = shielded { - token::masp_utils::handle_masp_tx(ctx, &shielded)?; + token::masp_utils::handle_masp_tx( + ctx, + &shielded, + transfer.key.as_deref(), + )?; update_masp_note_commitment_tree(&shielded)?; } Ok(()) From ed5ab1020b9c48fdfaba850e6a7d03fda0803095 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 5 Jan 2024 18:16:03 +0100 Subject: [PATCH 12/26] Fixes integration tests --- .../src/lib/node/ledger/shell/testing/node.rs | 157 +++++++++++++++++- shared/src/ledger/protocol/mod.rs | 13 +- tests/src/integration/setup.rs | 2 + 3 files changed, 163 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index 5bad5fd829..f306684f02 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::future::poll_fn; use std::mem::ManuallyDrop; use std::path::PathBuf; @@ -33,8 +34,9 @@ use namada::types::control_flow::time::Duration; use namada::types::ethereum_events::EthereumEvent; use namada::types::hash::Hash; use namada::types::key::tm_consensus_key_raw_hash; -use namada::types::storage::{BlockHash, BlockHeight, Epoch, Header}; +use namada::types::storage::{BlockHash, BlockHeight, Epoch, Header, TxIndex}; use namada::types::time::DateTimeUtc; +use namada_sdk::proto::Tx; use namada_sdk::queries::Client; use regex::Regex; use tokio::sync::mpsc; @@ -247,6 +249,9 @@ pub struct MockNode { pub test_dir: ManuallyDrop, pub keep_temp: bool, pub results: Arc>>, + #[allow(clippy::type_complexity)] + pub valid_masp_txs: + Arc>>>>, pub services: Arc, pub auto_drive_services: bool, } @@ -492,6 +497,7 @@ impl MockNode { }, byzantine_validators: vec![], txs: txs + .clone() .into_iter() .zip(tx_results.into_iter()) .map(|(tx, result)| ProcessedTx { @@ -505,6 +511,7 @@ impl MockNode { // process the results let resp = locked.finalize_block(req).unwrap(); + let height = locked.wl_storage.storage.get_block_height().0; let mut error_codes = resp .events .into_iter() @@ -517,6 +524,47 @@ impl MockNode { ) .unwrap(); if code == ResultCode::Ok { + if e.attributes.get("is_valid_masp_tx").is_some() { + // Cache if it was a masp transaction for future queries + // It's either: + // - Wrapper tx with fee unshielding + // - Decrypted masp tx + // NOTE: ignoring masp over ibc for now + let tx_hash = e.attributes.get("hash").unwrap(); + let idx = txs + .iter() + .position(|bytes| { + let mut tx = + Tx::try_from(bytes.as_ref()).unwrap(); + let current_tx_hash = + if tx.header().decrypted().is_some() { + tx.update_header( + namada::types::transaction::TxType::Raw, + ); + tx.header_hash() + } else { + tx.header_hash() + }; + + ¤t_tx_hash.to_string() == tx_hash + }) + .unwrap(); + self.valid_masp_txs + .lock() + .unwrap() + .entry(height) + .and_modify(|list| { + let _ = list.insert( + TxIndex(idx as u32), + txs[idx].clone(), + ); + }) + .or_insert( + vec![(TxIndex(idx as u32), txs[idx].clone())] + .into_iter() + .collect(), + ); + } NodeResults::Ok } else { NodeResults::Failed(code) @@ -810,16 +858,67 @@ impl<'a> Client for &'a MockNode { /// `/tx_search`: search for transactions with their results. async fn tx_search( &self, - _query: namada::tendermint_rpc::query::Query, + query: namada::tendermint_rpc::query::Query, _prove: bool, _page: u32, _per_page: u8, _order: namada::tendermint_rpc::Order, ) -> Result { - // In the past, some cli commands for masp called this. However, these - // commands are not currently supported, so we do not need to fill - // in this function for now. - unreachable!() + // NOTE: atm, this is only needed by the client to query masp txs, so we + // pretend all requests are for masp transactions + self.drive_mock_services_bg().await; + let (block_height, tx_index) = parse_masp_tx_search_query(query); + match self.valid_masp_txs.lock().unwrap().get(&block_height) { + Some(response) => { + if let Some(ref idx) = tx_index { + let tx = response + .get(idx) + .expect("Missing expected indexed transaction") + .to_owned(); + + let txs = + vec![namada::tendermint_rpc::endpoint::tx::Response { + hash: namada::tendermint::Hash::Sha256([0u8; 32]), + height: (block_height.0 as u32).into(), + index: idx.0, + tx_result: Default::default(), + tx, + proof: None, + }]; + + Ok(namada::tendermint_rpc::endpoint::tx_search::Response { + txs, + total_count: 1, + }) + } else { + let total_count = response.len() as u32; + let txs = response + .iter() + .map(|(idx, bytes)| { + namada::tendermint_rpc::endpoint::tx::Response { + hash: namada::tendermint::Hash::Sha256( + [0u8; 32], + ), + height: (block_height.0 as u32).into(), + index: idx.0, + tx_result: Default::default(), + tx: bytes.to_owned(), + proof: None, + } + }) + .collect::>(); + + Ok(namada::tendermint_rpc::endpoint::tx_search::Response { + txs, + total_count, + }) + } + } + None => Ok(namada::tendermint_rpc::endpoint::tx_search::Response { + txs: vec![], + total_count: 0, + }), + } } /// `/health`: get node health. @@ -858,6 +957,52 @@ fn parse_tm_query( } } +// Parse a tx_search query expecting it to require block height (and optionally +// an index) for unfetched masp txs +fn parse_masp_tx_search_query( + query: namada::tendermint_rpc::query::Query, +) -> (BlockHeight, Option) { + // height = 0 AND is_valid_masp_tx EXISTS + let height = query + .conditions + .iter() + .find(|condition| condition.key == "height") + .expect("Block height is required in tx_search"); + let idx = query + .conditions + .iter() + .find(|condition| condition.key == "tx.index"); + + let index = idx.map(|idx| { + if let namada::tendermint_rpc::query::Operation::Eq( + namada::tendermint_rpc::query::Operand::String(ref index), + ) = idx.operation + { + TxIndex(u32::from_str(index).unwrap()) + } else { + unreachable!("Missing expected tx index"); + } + }); + + if let namada::tendermint_rpc::query::Operation::Eq( + namada::tendermint_rpc::query::Operand::Unsigned(block_height), + ) = height.operation + { + return (block_height.into(), index); + } else if let namada::tendermint_rpc::query::Operation::Eq( + namada::tendermint_rpc::query::Operand::String(ref block_height), + ) = height.operation + { + return (BlockHeight(u64::from_str(block_height).unwrap()), index); + } else { + } + + unreachable!( + "We only query txs based on the height of their blocks and possibly \ + an index" + ) +} + /// A Namada event log index and event type encoded as /// a Tendermint block height. #[derive(Copy, Clone, Eq, PartialEq, Debug)] diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index d009b35bea..f2ee274fae 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -259,8 +259,8 @@ where &mut shell_params, block_proposer, &mut changed_keys, + is_committed_fee_unshield, )?; - *is_committed_fee_unshield = true; // Account for gas shell_params @@ -301,6 +301,7 @@ fn charge_fee<'a, D, H, CA, WLS>( shell_params: &mut ShellParams<'a, CA, WLS>, block_proposer: Option<&Address>, changed_keys: &mut BTreeSet, + is_committed_fee_unshield: &mut bool, ) -> Result<()> where CA: 'static + WasmCacheAccess + Sync, @@ -316,7 +317,7 @@ where } = shell_params; // Unshield funds if requested - if let Some(transaction) = masp_transaction { + let requires_fee_unshield = if let Some(transaction) = masp_transaction { // The unshielding tx does not charge gas, instantiate a // custom gas meter for this step let mut tx_gas_meter = @@ -377,7 +378,11 @@ where } Err(e) => tracing::error!("{}", e), } - } + + true + } else { + false + }; // Charge or check fees match block_proposer { @@ -389,6 +394,8 @@ where // Commit tx write log even in case of subsequent errors wl_storage.write_log_mut().commit_tx(); + // Update the flag only after the fee payment has been committed + *is_committed_fee_unshield = requires_fee_unshield; Ok(()) } diff --git a/tests/src/integration/setup.rs b/tests/src/integration/setup.rs index 828f8be112..a5c3c3a5ba 100644 --- a/tests/src/integration/setup.rs +++ b/tests/src/integration/setup.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::mem::ManuallyDrop; use std::path::Path; use std::str::FromStr; @@ -190,6 +191,7 @@ fn create_node( keep_temp, services: Arc::new(services), results: Arc::new(Mutex::new(vec![])), + valid_masp_txs: Arc::new(Mutex::new(HashMap::new())), auto_drive_services, }; let init_req = From 3ba1ac1f4b5f1f3efa65542eb80723068b8bd7e5 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sun, 7 Jan 2024 12:13:24 +0100 Subject: [PATCH 13/26] Updates masp queries to avoid the need for an indexer --- core/src/ledger/ibc/context/storage.rs | 1 - core/src/ledger/ibc/mod.rs | 1 - core/src/ledger/vp_env.rs | 1 - core/src/types/token.rs | 1 - sdk/src/masp.rs | 505 ++++++++++++++----------- 5 files changed, 290 insertions(+), 219 deletions(-) diff --git a/core/src/ledger/ibc/context/storage.rs b/core/src/ledger/ibc/context/storage.rs index 8ff16cf868..ac2b0d362d 100644 --- a/core/src/ledger/ibc/context/storage.rs +++ b/core/src/ledger/ibc/context/storage.rs @@ -28,7 +28,6 @@ pub trait IbcStorageContext: StorageRead + StorageWrite { ) -> Result<(), Error>; /// Handle masp tx - // FIXME: try again to remove tx_index from some places fn handle_masp_tx( &mut self, shielded: &masp_primitives::transaction::Transaction, diff --git a/core/src/ledger/ibc/mod.rs b/core/src/ledger/ibc/mod.rs index cd45eb054e..2c6e054be2 100644 --- a/core/src/ledger/ibc/mod.rs +++ b/core/src/ledger/ibc/mod.rs @@ -102,7 +102,6 @@ where pub fn execute(&mut self, tx_data: &[u8]) -> Result<(), Error> { let message = decode_message(tx_data)?; match &message { - // FIXME: look here for MASP on IBC IbcMessage::Transfer(msg) => { let mut token_transfer_ctx = TokenTransferContext::new(self.ctx.inner.clone()); diff --git a/core/src/ledger/vp_env.rs b/core/src/ledger/vp_env.rs index 1005e11070..728e66d9a6 100644 --- a/core/src/ledger/vp_env.rs +++ b/core/src/ledger/vp_env.rs @@ -110,7 +110,6 @@ where /// Get the shielded action including the transfer and the masp tx fn get_shielded_action( - // FIXME: I need this &self, tx_data: &Tx, ) -> Result<(Transfer, Transaction), storage_api::Error> { diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 4429a0caa4..d769038a3b 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1220,7 +1220,6 @@ pub fn is_masp_allowed_key(key: &Key) -> bool { match &key.segments[..] { [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if *addr == MASP - //FIXME: place the check back in the masp vp if needed && (key.starts_with(PIN_KEY_PREFIX) || key == MASP_NOTE_COMMITMENT_TREE_KEY) => { diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index a671b93072..439b6867a2 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -61,7 +61,7 @@ use namada_core::types::storage::{BlockHeight, Epoch, IndexedTx, TxIndex}; use namada_core::types::time::{DateTimeUtc, DurationSecs}; use namada_core::types::token; use namada_core::types::token::{Change, MaspDenom, Transfer}; -use namada_core::types::transaction::WrapperTx; +use namada_core::types::transaction::{TxResult, WrapperTx}; use rand_core::{CryptoRng, OsRng, RngCore}; use ripemd::Digest as RipemdDigest; use sha2::Digest; @@ -605,6 +605,7 @@ pub struct ShieldedContext { pub last_indexed: IndexedTx, /// The commitment tree produced by scanning all transactions up to tx_pos pub tree: CommitmentTree, + // FIXME: review these positions, what do they refer to? /// Maps viewing keys to applicable note positions pub pos_map: HashMap>, /// Maps a nullifier to the note position to which it applies @@ -634,7 +635,10 @@ impl Default for ShieldedContext { fn default() -> ShieldedContext { ShieldedContext:: { utils: U::default(), - last_indexed: IndexedTx::default(), + last_indexed: IndexedTx { + height: BlockHeight::first(), + index: TxIndex::default(), + }, tree: CommitmentTree::empty(), pos_map: HashMap::default(), nf_map: HashMap::default(), @@ -720,6 +724,7 @@ impl ShieldedContext { let (txs, mut tx_iter); if !unknown_keys.is_empty() { // Load all transactions accepted until this point + eprintln!("FETCHING AGAIN FROM INDEX 0 BECAUSE NEW KEY"); //FIXME: remove txs = Self::fetch_shielded_transfers( client, IndexedTx { @@ -766,8 +771,11 @@ impl ShieldedContext { /// Obtain a chronologically-ordered list of all accepted shielded /// transactions from a node. + // FIXME: remove all the unwraps, we are in the sdk here pub async fn fetch_shielded_transfers( client: &C, + // FIXME: just pass the block heigh here? I always query block anyway + // FIXME: should probably also cached only the last block height? Yes last_indexed_tx: IndexedTx, ) -> Result, Error> { @@ -776,13 +784,22 @@ impl ShieldedContext { .await? .map_or_else(BlockHeight::first, |block| block.height); + eprintln!("ABOUT TO REQUEST PAGINATED RESULT"); //FIXME: remove + eprintln!("LAST BLOCK HEIGHT: {}", last_block_height); //FIXME: remove + eprintln!("LAST INDEXED HEIGHT: {}", last_indexed_tx.height); //FIXME: remove let mut shielded_txs = BTreeMap::new(); // Fetch all the transactions we do not have yet + // FIXME: this will actually query another time the already quiered + // previous last block height, should probably increase by one here. + // Actually, even queryin the same block again shouldn't be a problem + // cause I'll simply overwrite the entry in the map in the context + // FIXME: the index starts from 1 so I need to check the last indexe + // height only in this case, for the other cases I can start from the + // following one for height in u64::from(last_indexed_tx.height)..=last_block_height.0 { + eprintln!("IN HEIGHT {height} LOOP"); //FIXME: remove // Get the valid masp transactions at the specified height // FIXME: review if we really need extra key for ibc events - let tx_query = - Query::eq("height", height).and_exists("is_valid_masp_tx"); let epoch = query_epoch_at_height(client, height.into()) .await? @@ -793,229 +810,279 @@ impl ShieldedContext { ))) })?; + eprintln!("REQUESTING BLOCK AT HEIGHT: {}", height); //FIXME: remove // Paginate the results - for page in 1.. { - let txs_results = client - .tx_search( - tx_query.clone(), - // TODO: currently we are not verifying the merkle - // proof, we should or at least ask a parameter to the - // user ot decide if we want proofs(and their - // verification) or not - false, - page, - 100, - Order::Ascending, - ) - .await - .map_err(|e| { - Error::from(QueryError::General(e.to_string())) - })? - .txs; - - for result in &txs_results { - let tx_index = TxIndex(result.index); - if BlockHeight(height) == last_indexed_tx.height - && tx_index <= last_indexed_tx.index - { - continue; - } - let tx = Tx::try_from(result.tx.as_ref()) - .map_err(|e| Error::Other(e.to_string()))?; - let tx_header = tx.header(); - // NOTE: simply looking for masp sections attached to the tx - // is not safe. We don't validate the sections attached to a - // transaction se we could end up with transactions carrying - // an unnecessary masp section. We must instead look for the - // required masp sections in the signed commitments (hashes) - // of the transactions' headers/data sections - if let Some(wrapper_header) = tx_header.wrapper() { - let hash = wrapper_header - .unshield_section_hash - .ok_or_else(|| { - Error::Other( - "Missing expected fee unshielding section \ - hash" - .to_string(), - ) - })?; - - let masp_transaction = tx - .get_section(&hash) - .ok_or_else(|| { - Error::Other( - "Missing expected masp section".to_string(), - ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other( - "Missing masp transaction".to_string(), - ) - })?; - // FIXME: actually, do I realy need the entire transfer - // object? Probably not mayube we can remove - // FIXME: mroe in general. review everything we are - // storing to see if we actually need it - - // Transfer objects for fee unshielding are absent from - // the tx because they are completely constructed in - // protocol, need to recreate it here - let transfer = Transfer { - source: MASP, - target: wrapper_header.fee_payer(), - token: wrapper_header.fee.token.clone(), - amount: wrapper_header - .get_tx_fee() - .map_err(|e| Error::Other(e.to_string()))?, - key: None, - shielded: Some(hash), - }; - - // Collect the current transaction - shielded_txs.insert( - IndexedTx { - height: height.into(), - index: tx_index, - }, - (epoch, transfer, masp_transaction), - ); - } else if tx_header.decrypted().is_some() { - let (transfer, masp_transaction) = - match Transfer::try_from_slice( - &tx.data().ok_or_else(|| { - Error::Other( - "Missing data section".to_string(), - ) - })?, - ) { - Ok(transfer) => { - let masp_transaction = tx - .get_section( - &transfer.shielded.ok_or_else( - || { - Error::Other( - "Missing masp section \ - hash" - .to_string(), - ) - }, - )?, - ) - .ok_or_else(|| { - Error::Other( - "Missing masp section in \ - transaction" - .to_string(), - ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other( - "Missing masp transaction" - .to_string(), - ) - })?; - - (transfer, masp_transaction) - } - Err(_) => { - // This should be a MASP over IBC transfer, - // continue for now, we'll scan the ibc - // events later - continue; - } - }; - - // Collect the current transaction - shielded_txs.insert( - IndexedTx { - height: height.into(), - index: tx_index, - }, - (epoch, transfer, masp_transaction), - ); - } - } - - // FIXME: are we already storing masp transactions when we - // submit them? In this case I need to make sure we don't - // reqrite them from here An incomplete page - // signifies no more transactions - if txs_results.len() < 100 { - break; - } - } - } - - // Fetch all block events to look for MASP over IBC transactions - for height in u64::from(last_indexed_tx.height)..=last_block_height.0 { - let epoch = query_epoch_at_height(client, height.into()) - .await? - .ok_or_else(|| { - Error::from(QueryError::General(format!( - "Queried height is greater than the last committed \ - block height" - ))) - })?; - - let events = client + // FIXME: I think I'm braking here even before doing the first + // transaction, or better I'm livelocking, the ledger runs but the + // client never submits transactions FIXME: I get to + // here + let txs_results = match client .block_results(height) .await .map_err(|e| Error::from(QueryError::General(e.to_string())))? .end_block_events - .unwrap_or_default(); - - for (ibc_masp_event, tx_index) in - events.iter().filter_map(|event| { - if event - .kind - .starts_with(&EventType::Ibc(String::new()).to_string()) - { - event - .attributes - .iter() - .find(|attribute| { + { + // FIXME: imrpove this match + Some(events) => events + .into_iter() + .enumerate() + .filter(|(_idx, event)| { + eprintln!("EVENT: {:#?}", event); //FIXME: remove + // FIXME: I also need to save the index in the vec to + // retrieve the tx here + // Filter only the tx events which are valid masp txs + (event.kind == EventType::Accepted.to_string() + || event.kind == EventType::Applied.to_string()) + && event.attributes.iter().any(|attribute| { &attribute.key == "is_valid_masp_tx" }) - .map(|tx_index| (event, &tx_index.value)) - } else { - None - } - }) - { - // FIXME: can we parallelize stuff somewhere when constructing - // the internal state of the ShieldeContext? - // Masp transaction, collect it - let shielded_transfer = ibc_masp_event + }) + .collect::>(), + None => { + eprintln!("NO EVENTS IN END BLOCK, CONITNUING"); //FIXME: remove + continue; + } + }; + + eprintln!("BEFORE BLOCK"); //FIXME: remove + let block = client + .block(height as u32) + .await + .map_err(|e| Error::from(QueryError::General(e.to_string())))? + .block + .data; + + // FIXME: but I don't get to here, I must break in between + eprintln!("SIZE OF RESPONSE: {}", txs_results.len()); //FIXME: remove + // FIXME: seems like we can't find the succesful previous masp + // transaction even though it has been flagged + // FIXME: I never get here becasuse it seems the result is always + // empty + for (idx, tx_event) in &txs_results { + eprintln!("FOUND TRANSACTION"); //FIXME: remove + // FIXME: could I also receive a block height smaller than + // the cached one? + // FIXME: I think this condition is useless because in case I + // just overwrite the entry in the hashmap + // if BlockHeight(height) == last_indexed_tx.height + // && tx_index <= last_indexed_tx.index + // { + // continue; + // } + + let tx = Tx::try_from(block[*idx].as_ref()) + .map_err(|e| Error::Other(e.to_string()))?; + + let tx_header = tx.header(); + // NOTE: simply looking for masp sections attached to the tx + // is not safe. We don't validate the sections attached to a + // transaction se we could end up with transactions carrying + // an unnecessary masp section. We must instead look for the + // required masp sections in the signed commitments (hashes) + // of the transactions' headers/data sections + let (transfer, masp_transaction) = if let Some(wrapper_header) = + tx_header.wrapper() + { + eprintln!("FOUND WRAPPER MASP TX"); //FIXME: remove + let hash = wrapper_header + .unshield_section_hash + .ok_or_else(|| { + // FIXME: error here + // FIXME: probably in MockNode I'm getting the + // wrong index + Error::Other( + "Missing expected fee unshielding section hash" + .to_string(), + ) + })?; + + let masp_transaction = tx + .get_section(&hash) + .ok_or_else(|| { + Error::Other( + "Missing expected masp section".to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other("Missing masp transaction".to_string()) + })?; + // FIXME: actually, do I realy need the entire transfer + // object? Probably not mayube we can remove + + // Transfer objects for fee unshielding are absent from + // the tx because they are completely constructed in + // protocol, need to recreate it here + let transfer = Transfer { + source: MASP, + target: wrapper_header.fee_payer(), + token: wrapper_header.fee.token.clone(), + amount: wrapper_header + .get_tx_fee() + .map_err(|e| Error::Other(e.to_string()))?, + key: None, + shielded: Some(hash), + }; + + (transfer, masp_transaction) + } else { + // Expect decrypted transaction + eprintln!("FOUND DECRYPTED MASP TX"); //FIXME: remove + match Transfer::try_from_slice(&tx.data().ok_or_else( + || Error::Other("Missing data section".to_string()), + )?) { + Ok(transfer) => { + let masp_transaction = tx + .get_section(&transfer.shielded.ok_or_else( + || { + Error::Other( + "Missing masp section hash" + .to_string(), + ) + }, + )?) + .ok_or_else(|| { + Error::Other( + "Missing masp section in transaction" + .to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other( + "Missing masp transaction".to_string(), + ) + })?; + + (transfer, masp_transaction) + } + Err(_) => { + // This should be a MASP over IBC transaction + let shielded_transfer = tx_event .attributes .iter() - .find(|attribute| &attribute.key == "memo") - .map(|memo| { - IbcShieldedTransfer::try_from(Memo::from( - memo.value.clone(), - )) - }); - - if let Some(Ok(shielded_transfer)) = shielded_transfer { - shielded_txs.insert( - IndexedTx { - height: height.into(), - index: TxIndex( - u32::from_str(tx_index) - .map_err(|e| Error::Other(e.to_string()))?, - ), - }, - ( - epoch, - shielded_transfer.transfer, - shielded_transfer.masp_tx, - ), - ); - } + .find_map(|attribute| { + if attribute.key == "inner_tx" { + //FIXME: manage unwrap here + let tx_result = TxResult::from_str(&attribute.value).unwrap(); + + for ibc_event in tx_result.ibc_events { + for (key, value) in ibc_event.attributes { + if key == "memo" { + return Some(IbcShieldedTransfer::try_from(Memo::from(value))); + } + } + } + None + } else {None } + }).unwrap().unwrap(); //FIXME:remove these unwraps + + eprintln!("FOUND IBC MASP EVENT"); //FIXME:remove + ( + shielded_transfer.transfer, + shielded_transfer.masp_tx, + ) + } + } + }; + + // Collect the current transaction + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: TxIndex(*idx as u32), + }, + (epoch, transfer, masp_transaction), + ); } + + // FIXME: are we already storing masp transactions when we + // submit them? In this case I need to make sure we don't + // reqrite them from here } + // FIXME: we get to here even though we haven't scanned all the blocks! + eprintln!("DONE REQUESTING HEIGHT"); //FIXME: remove + + // FIXME: need this thing? I think I'm already marking the tx correctly + // FIXME: ah but the issue is that this is not a Transfer object but an + // IBC PacketMsg, so I need to derialize diferently FIXME: yes + // but the ibc event is already associated with the tx so I can do this + // in the previous loop , I don0t need this! Fetch all block + // events to look for MASP over IBC transactions for height in + // u64::from(last_indexed_tx.height)..=last_block_height.0 { + // let epoch = query_epoch_at_height(client, height.into()) + // .await? + // .ok_or_else(|| { + // Error::from(QueryError::General(format!( + // "Queried height is greater than the last committed \ + // block height" + // ))) + // })?; + + // //FIXME: this has alreaady been queried before, can I reuse? + // let events = client + // .block_results(height) + // .await + // .map_err(|e| + // Error::from(QueryError::General(e.to_string())))? + // .end_block_events + // .unwrap_or_default(); + + // for (ibc_masp_event, tx_index) in + // events.iter().filter_map(|event| { + // if event + // .kind + // + // .starts_with(&EventType::Ibc(String::new()).to_string()) + // { + // event + // .attributes + // .iter() + // .find(|attribute| { + // &attribute.key == "is_valid_masp_tx" + // }) + // .map(|tx_index| (event, &tx_index.value)) + // } else { + // None + // } + // }) + // { + // // FIXME: can we parallelize stuff somewhere when + // constructing // the internal state of the + // ShieldeContext? // Masp transaction, collect it + // let shielded_transfer = ibc_masp_event + // .attributes + // .iter() + // .find(|attribute| &attribute.key == "memo") + // .map(|memo| { + // IbcShieldedTransfer::try_from(Memo::from( + // memo.value.clone(), + // )) + // }); + + // if let Some(Ok(shielded_transfer)) = shielded_transfer { + // eprintln!("FOUND IBC MASP EVENT"); //FIXME:remove + // shielded_txs.insert( + // IndexedTx { + // height: height.into(), + // index: TxIndex( + // u32::from_str(tx_index) + // .map_err(|e| + // Error::Other(e.to_string()))?, ), + // }, + // ( + // epoch, + // shielded_transfer.transfer, + // shielded_transfer.masp_tx, + // ), + // ); + // } + // } + // } + + // eprintln!("DONE REQUESTING IBC EVENTS"); //FIXME: remove + Ok(shielded_txs) } @@ -1181,8 +1248,10 @@ impl ShieldedContext { ) -> Result, Error> { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { + eprintln!("KEY NOT IN MAP"); //FIXME: remove return Ok(None); } + eprintln!("KEY IN MAP"); //FIXME: remove let mut val_acc = I128Sum::zero(); // Retrieve the notes that can be spent by this key if let Some(avail_notes) = self.pos_map.get(vk) { @@ -1595,7 +1664,13 @@ impl ShieldedContext { })?; let tx_query = Query::eq("height", indexed_tx.height.to_string()) + // FIXME: actually this condition seems to be accepted, so I guess I + // can query the attribute of the events or the field of the + // response of a transaction + // FIXME: well it works but in the integration test where the + // responses are mocked .and_eq("tx.index", indexed_tx.index.to_string()); + eprintln!("TX SEARCH IN COMPUTE PINNED BALANCE"); //FIXME: remove let tx_results = client .tx_search( tx_query.clone(), From 0682e15274a47e4262db8bd236682fcd9916ac01 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Sun, 7 Jan 2024 22:32:32 +0100 Subject: [PATCH 14/26] Fixes integration tests --- .../src/lib/node/ledger/shell/testing/node.rs | 372 +++++++++--------- sdk/src/masp.rs | 65 ++- ...FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin | Bin 9941 -> 9941 bytes ...C733C557AA7C18902CB851BB4DB93A834ED187.bin | Bin 7448 -> 7448 bytes ...F6B0C21274416797B6508AE6C759537F947AC8.bin | Bin 7448 -> 7448 bytes ...3F779253B53A9A0BD62FD3DF8F6FCF4AF1E145.bin | Bin 17018 -> 0 bytes ...BF9BC175A70082068C8785BDDBF49DCCA4BE66.bin | Bin 7448 -> 0 bytes ...BAE37AFDAA28B0701ED5D313EA859FC8153343.bin | Bin 7448 -> 7448 bytes ...C89879834677F926130D56EB5E4067728EB5CF.bin | Bin 10382 -> 0 bytes ...BE50202AF004DFD895A721EDA284D96B253ACC.bin | Bin 7448 -> 0 bytes ...3CEA99A9BC66B2BAB03D4ABA1AE57B12596061.bin | Bin 7448 -> 0 bytes ...7C1886347E49C18C8E0F35D25956CA06390B17.bin | Bin 7448 -> 7448 bytes ...F136D4522BAFC99E0E81FD54A28A38E8419CFC.bin | Bin 20518 -> 0 bytes ...99BB39683C7B8AAB924B267369B3143A3FBF89.bin | Bin 10382 -> 10382 bytes ...ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin | Bin 7448 -> 7448 bytes ...958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin | Bin 6669 -> 6669 bytes ...C010E6D95775165E4FC619A11DCFDB59E21D30.bin | Bin 6669 -> 0 bytes ...6D2CF14C7F9E90A8588BA75DA0D2860F36E2CB.bin | Bin 9649 -> 0 bytes ...1E860D9B5DF041F4645BFC329BE9A03AABFE47.bin | Bin 9941 -> 9941 bytes ...396732D2EBF77728D2772EB251123DF2CEF6A1.bin | Bin 9941 -> 0 bytes ...5CF776D5DE7C9153D67B7FDF22ED6F35558738.bin | Bin 24494 -> 0 bytes ...8F934075535EBBEC567084302C3D9B469B83FA.bin | Bin 9649 -> 9649 bytes ...FF5FEC42C100CEB0B19DC6C47DBFE88D42FFFC.bin | Bin 17018 -> 17018 bytes ...45FC957F905749C47816C307B4B8D580DAE5D9.bin | Bin 24494 -> 24494 bytes ...F288D9729C50CA4AE4D1635C6D82007461517F.bin | Bin 7448 -> 7448 bytes ...0F74AF27826AB075ECA82AE64F488CC7D7B99D.bin | Bin 7448 -> 0 bytes ...47FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin | Bin 15597 -> 15597 bytes ...51842690C36EA86369DC2145AED8B139748042.bin | Bin 15257 -> 15257 bytes ...6C8B75587507CA52981F48FD1FC9C5BE0BEE43.bin | Bin 15257 -> 0 bytes tests/src/integration/setup.rs | 2 +- 30 files changed, 210 insertions(+), 229 deletions(-) delete mode 100644 test_fixtures/masp_proofs/51EF94B13138B91DB3081F95193F779253B53A9A0BD62FD3DF8F6FCF4AF1E145.bin delete mode 100644 test_fixtures/masp_proofs/544F7432B95E26E312105FB804BF9BC175A70082068C8785BDDBF49DCCA4BE66.bin delete mode 100644 test_fixtures/masp_proofs/6DD5A788D36258E9D5FEF454DBC89879834677F926130D56EB5E4067728EB5CF.bin delete mode 100644 test_fixtures/masp_proofs/6E92D0B97A65FB5ADFA6371A6CBE50202AF004DFD895A721EDA284D96B253ACC.bin delete mode 100644 test_fixtures/masp_proofs/8032CA7B951C625E43F48AEBD53CEA99A9BC66B2BAB03D4ABA1AE57B12596061.bin delete mode 100644 test_fixtures/masp_proofs/8A79BF5E0292339E70287DB626F136D4522BAFC99E0E81FD54A28A38E8419CFC.bin delete mode 100644 test_fixtures/masp_proofs/A312CDD49C05B7C768F5DAF708C010E6D95775165E4FC619A11DCFDB59E21D30.bin delete mode 100644 test_fixtures/masp_proofs/A8A9963AC2983B576BAE4DE9BA6D2CF14C7F9E90A8588BA75DA0D2860F36E2CB.bin delete mode 100644 test_fixtures/masp_proofs/AEA19C9B07742FF5F6D759B171396732D2EBF77728D2772EB251123DF2CEF6A1.bin delete mode 100644 test_fixtures/masp_proofs/C788F9057C615CCE7B260BB8BF5CF776D5DE7C9153D67B7FDF22ED6F35558738.bin delete mode 100644 test_fixtures/masp_proofs/E7F3B43D776427F6570F4EF1600F74AF27826AB075ECA82AE64F488CC7D7B99D.bin delete mode 100644 test_fixtures/masp_proofs/EDE8DC791D02098C5C199CAF2F6C8B75587507CA52981F48FD1FC9C5BE0BEE43.bin diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index f306684f02..02c8d6bf4f 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -34,11 +34,12 @@ use namada::types::control_flow::time::Duration; use namada::types::ethereum_events::EthereumEvent; use namada::types::hash::Hash; use namada::types::key::tm_consensus_key_raw_hash; -use namada::types::storage::{BlockHash, BlockHeight, Epoch, Header, TxIndex}; +use namada::types::storage::{BlockHash, BlockHeight, Epoch, Header}; use namada::types::time::DateTimeUtc; -use namada_sdk::proto::Tx; use namada_sdk::queries::Client; use regex::Regex; +use tendermint_rpc::endpoint::block; +use tendermint_rpc::Response; use tokio::sync::mpsc; use crate::facade::tendermint_proto::v0_37::abci::{ @@ -249,9 +250,7 @@ pub struct MockNode { pub test_dir: ManuallyDrop, pub keep_temp: bool, pub results: Arc>>, - #[allow(clippy::type_complexity)] - pub valid_masp_txs: - Arc>>>>, + pub blocks: Arc>>, pub services: Arc, pub auto_drive_services: bool, } @@ -402,44 +401,90 @@ impl MockNode { let (proposer_address, votes) = self.prepare_request(); let mut locked = self.shell.lock().unwrap(); - - // build finalize block abci request - let req = { - // check if we have protocol txs to be included - // in the finalize block request - let txs = { - let req = RequestPrepareProposal { - proposer_address: proposer_address.clone().into(), - ..Default::default() - }; - let txs = locked.prepare_proposal(req).txs; - - txs.into_iter() - .map(|tx| ProcessedTx { - tx, - result: TxResult { - code: 0, - info: String::new(), - }, - }) - .collect() + let height = locked + .wl_storage + .storage + .get_last_block_height() + .next_height(); + + // check if we have protocol txs to be included + // in the finalize block request + let txs: Vec = { + let req = RequestPrepareProposal { + proposer_address: proposer_address.clone().into(), + ..Default::default() }; - FinalizeBlock { - hash: BlockHash([0u8; 32]), - header: Header { - hash: Hash([0; 32]), - time: DateTimeUtc::now(), - next_validators_hash: Hash([0; 32]), - }, - byzantine_validators: vec![], - txs, - proposer_address, - votes, - } + let txs = locked.prepare_proposal(req).txs; + + txs.into_iter() + .map(|tx| ProcessedTx { + tx, + result: TxResult { + code: 0, + info: String::new(), + }, + }) + .collect() + }; + // build finalize block abci request + let req = FinalizeBlock { + hash: BlockHash([0u8; 32]), + header: Header { + hash: Hash([0; 32]), + time: DateTimeUtc::now(), + next_validators_hash: Hash([0; 32]), + }, + byzantine_validators: vec![], + txs: txs.clone(), + proposer_address, + votes, }; locked.finalize_block(req).expect("Test failed"); locked.commit(); + + // Cache the block + self.blocks.lock().unwrap().insert( + height, + block::Response { + block_id: tendermint::block::Id { + hash: tendermint::Hash::None, + part_set_header: tendermint::block::parts::Header::default( + ), + }, + block: tendermint::block::Block::new( + tendermint::block::Header { + version: tendermint::block::header::Version { + block: 0, + app: 0, + }, + chain_id: locked + .chain_id + .to_string() + .try_into() + .unwrap(), + height: 1u32.into(), + time: tendermint::Time::now(), + last_block_id: None, + last_commit_hash: None, + data_hash: None, + validators_hash: tendermint::Hash::None, + next_validators_hash: tendermint::Hash::None, + consensus_hash: tendermint::Hash::None, + app_hash: tendermint::AppHash::default(), + last_results_hash: None, + evidence_hash: None, + proposer_address: tendermint::account::Id::new( + [0u8; 20], + ), + }, + txs.into_iter().map(|tx| tx.tx.to_vec()).collect(), + tendermint::evidence::List::default(), + None, + ) + .unwrap(), + }, + ); } /// Advance to a block height that allows @@ -470,6 +515,11 @@ impl MockNode { ..Default::default() }; let mut locked = self.shell.lock().unwrap(); + let height = locked + .wl_storage + .storage + .get_last_block_height() + .next_height(); let (result, tx_results) = locked.process_proposal(req); let mut errors: Vec<_> = tx_results @@ -511,7 +561,6 @@ impl MockNode { // process the results let resp = locked.finalize_block(req).unwrap(); - let height = locked.wl_storage.storage.get_block_height().0; let mut error_codes = resp .events .into_iter() @@ -524,47 +573,6 @@ impl MockNode { ) .unwrap(); if code == ResultCode::Ok { - if e.attributes.get("is_valid_masp_tx").is_some() { - // Cache if it was a masp transaction for future queries - // It's either: - // - Wrapper tx with fee unshielding - // - Decrypted masp tx - // NOTE: ignoring masp over ibc for now - let tx_hash = e.attributes.get("hash").unwrap(); - let idx = txs - .iter() - .position(|bytes| { - let mut tx = - Tx::try_from(bytes.as_ref()).unwrap(); - let current_tx_hash = - if tx.header().decrypted().is_some() { - tx.update_header( - namada::types::transaction::TxType::Raw, - ); - tx.header_hash() - } else { - tx.header_hash() - }; - - ¤t_tx_hash.to_string() == tx_hash - }) - .unwrap(); - self.valid_masp_txs - .lock() - .unwrap() - .entry(height) - .and_modify(|list| { - let _ = list.insert( - TxIndex(idx as u32), - txs[idx].clone(), - ); - }) - .or_insert( - vec![(TxIndex(idx as u32), txs[idx].clone())] - .into_iter() - .collect(), - ); - } NodeResults::Ok } else { NodeResults::Failed(code) @@ -572,6 +580,47 @@ impl MockNode { }) .collect::>(); self.results.lock().unwrap().append(&mut error_codes); + self.blocks.lock().unwrap().insert( + height, + block::Response { + block_id: tendermint::block::Id { + hash: tendermint::Hash::None, + part_set_header: tendermint::block::parts::Header::default( + ), + }, + block: tendermint::block::Block::new( + tendermint::block::Header { + version: tendermint::block::header::Version { + block: 0, + app: 0, + }, + chain_id: locked + .chain_id + .to_string() + .try_into() + .unwrap(), + height: 1u32.into(), + time: tendermint::Time::now(), + last_block_id: None, + last_commit_hash: None, + data_hash: None, + validators_hash: tendermint::Hash::None, + next_validators_hash: tendermint::Hash::None, + consensus_hash: tendermint::Hash::None, + app_hash: tendermint::AppHash::default(), + last_results_hash: None, + evidence_hash: None, + proposer_address: tendermint::account::Id::new( + [0u8; 20], + ), + }, + txs, + tendermint::evidence::List::default(), + None, + ) + .unwrap(), + }, + ); locked.commit(); } @@ -654,12 +703,55 @@ impl<'a> Client for &'a MockNode { async fn perform( &self, - _request: R, + request: R, ) -> std::result::Result where R: SimpleRequest, { - unreachable!() + self.drive_mock_services_bg().await; + // NOTE: atm this is only needed to query blocks at a specific height + match request.method() { + tendermint_rpc::Method::Block => { + let request_str = request.into_json(); + const QUERY_PARSING_REGEX_STR: &str = r#".*"height": "(.)+".*"#; + + lazy_static! { + /// Compiled regular expression used to parse queries. + static ref QUERY_PARSING_REGEX: Regex = Regex::new(QUERY_PARSING_REGEX_STR).unwrap(); + } + + let captures = + QUERY_PARSING_REGEX.captures(&request_str).unwrap(); + let mut height_str = captures + .get(0) + .unwrap() + .as_str() + .rsplit(':') + .next() + .unwrap() + .to_owned(); + height_str.retain(|c| !(c.is_whitespace() || c == '"')); + let height = BlockHeight::from_str(&height_str).unwrap(); + + match self.blocks.lock().unwrap().get(&height) { + Some(block) => { + let wrapper = + tendermint_rpc::response::Wrapper::new_with_id( + tendermint_rpc::Id::None, + Some(block), + None, + ); + + let response = serde_json::to_string(&wrapper).unwrap(); + R::Response::from_string(response).map(Into::into) + } + None => Err(RpcError::invalid_params(format!( + "Could not find block height {height}" + ))), + } + } + _ => unimplemented!(), + } } /// `/abci_info`: get information about the ABCI application. @@ -820,9 +912,11 @@ impl<'a> Client for &'a MockNode { let events: Vec<_> = locked .event_log() .iter() - .enumerate() - .flat_map(|(index, event)| { - if index == encoded_event.log_index() { + .flat_map(|event| { + if usize::from_str(event.attributes.get("height").unwrap()) + .unwrap() + == encoded_event.log_index() + { Some(event) } else { None @@ -842,7 +936,6 @@ impl<'a> Client for &'a MockNode { }) .collect(); let has_events = !events.is_empty(); - Ok(tendermint_rpc::endpoint::block_results::Response { height, txs_results: None, @@ -858,67 +951,16 @@ impl<'a> Client for &'a MockNode { /// `/tx_search`: search for transactions with their results. async fn tx_search( &self, - query: namada::tendermint_rpc::query::Query, + _query: namada::tendermint_rpc::query::Query, _prove: bool, _page: u32, _per_page: u8, _order: namada::tendermint_rpc::Order, ) -> Result { - // NOTE: atm, this is only needed by the client to query masp txs, so we - // pretend all requests are for masp transactions - self.drive_mock_services_bg().await; - let (block_height, tx_index) = parse_masp_tx_search_query(query); - match self.valid_masp_txs.lock().unwrap().get(&block_height) { - Some(response) => { - if let Some(ref idx) = tx_index { - let tx = response - .get(idx) - .expect("Missing expected indexed transaction") - .to_owned(); - - let txs = - vec![namada::tendermint_rpc::endpoint::tx::Response { - hash: namada::tendermint::Hash::Sha256([0u8; 32]), - height: (block_height.0 as u32).into(), - index: idx.0, - tx_result: Default::default(), - tx, - proof: None, - }]; - - Ok(namada::tendermint_rpc::endpoint::tx_search::Response { - txs, - total_count: 1, - }) - } else { - let total_count = response.len() as u32; - let txs = response - .iter() - .map(|(idx, bytes)| { - namada::tendermint_rpc::endpoint::tx::Response { - hash: namada::tendermint::Hash::Sha256( - [0u8; 32], - ), - height: (block_height.0 as u32).into(), - index: idx.0, - tx_result: Default::default(), - tx: bytes.to_owned(), - proof: None, - } - }) - .collect::>(); - - Ok(namada::tendermint_rpc::endpoint::tx_search::Response { - txs, - total_count, - }) - } - } - None => Ok(namada::tendermint_rpc::endpoint::tx_search::Response { - txs: vec![], - total_count: 0, - }), - } + // In the past, some cli commands for masp called this. However, these + // commands are not currently supported, so we do not need to fill + // in this function for now. + unreachable!() } /// `/health`: get node health. @@ -957,52 +999,6 @@ fn parse_tm_query( } } -// Parse a tx_search query expecting it to require block height (and optionally -// an index) for unfetched masp txs -fn parse_masp_tx_search_query( - query: namada::tendermint_rpc::query::Query, -) -> (BlockHeight, Option) { - // height = 0 AND is_valid_masp_tx EXISTS - let height = query - .conditions - .iter() - .find(|condition| condition.key == "height") - .expect("Block height is required in tx_search"); - let idx = query - .conditions - .iter() - .find(|condition| condition.key == "tx.index"); - - let index = idx.map(|idx| { - if let namada::tendermint_rpc::query::Operation::Eq( - namada::tendermint_rpc::query::Operand::String(ref index), - ) = idx.operation - { - TxIndex(u32::from_str(index).unwrap()) - } else { - unreachable!("Missing expected tx index"); - } - }); - - if let namada::tendermint_rpc::query::Operation::Eq( - namada::tendermint_rpc::query::Operand::Unsigned(block_height), - ) = height.operation - { - return (block_height.into(), index); - } else if let namada::tendermint_rpc::query::Operation::Eq( - namada::tendermint_rpc::query::Operand::String(ref block_height), - ) = height.operation - { - return (BlockHeight(u64::from_str(block_height).unwrap()), index); - } else { - } - - unreachable!( - "We only query txs based on the height of their blocks and possibly \ - an index" - ) -} - /// A Namada event log index and event type encoded as /// a Tendermint block height. #[derive(Copy, Clone, Eq, PartialEq, Debug)] diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 439b6867a2..d05674ae00 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -793,10 +793,17 @@ impl ShieldedContext { // previous last block height, should probably increase by one here. // Actually, even queryin the same block again shouldn't be a problem // cause I'll simply overwrite the entry in the map in the context - // FIXME: the index starts from 1 so I need to check the last indexe - // height only in this case, for the other cases I can start from the - // following one - for height in u64::from(last_indexed_tx.height)..=last_block_height.0 { + // FIXME: instead it's apparently a problem, the integratio ntests were + // failing becasue of this FIXME: the index starts from 1 so I + // need to check the last indexe height only in this case, for + // the other cases I can start from the following one + // FIXME: refator this if with methods in IndexedTx + let first_height_to_query = if last_indexed_tx.height.0 <= 1 { + 1 + } else { + last_indexed_tx.height.next_height().0 + }; + for height in first_height_to_query..=last_block_height.0 { eprintln!("IN HEIGHT {height} LOOP"); //FIXME: remove // Get the valid masp transactions at the specified height // FIXME: review if we really need extra key for ibc events @@ -827,10 +834,10 @@ impl ShieldedContext { .into_iter() .enumerate() .filter(|(_idx, event)| { - eprintln!("EVENT: {:#?}", event); //FIXME: remove - // FIXME: I also need to save the index in the vec to - // retrieve the tx here + // eprintln!("EVENT: {:#?}", event); //FIXME: remove // Filter only the tx events which are valid masp txs + // FIXME: probably no need the condition on the event + // type, it's redundant (event.kind == EventType::Accepted.to_string() || event.kind == EventType::Applied.to_string()) && event.attributes.iter().any(|attribute| { @@ -927,11 +934,12 @@ impl ShieldedContext { (transfer, masp_transaction) } else { // Expect decrypted transaction - eprintln!("FOUND DECRYPTED MASP TX"); //FIXME: remove + eprintln!("FOUND DECRYPTED MASP TX AT HEIGHT: {}", height); //FIXME: remove match Transfer::try_from_slice(&tx.data().ok_or_else( || Error::Other("Missing data section".to_string()), )?) { Ok(transfer) => { + eprintln!("DECRYPTED TX: {:#?}", transfer); //FIXME: remove let masp_transaction = tx .get_section(&transfer.shielded.ok_or_else( || { @@ -1646,6 +1654,8 @@ impl ShieldedContext { } } // Construct the key for where the transaction ID would be stored + // FIXME: should index the tx hash, not the block and height? Yes it's + // faster to query, less data let pin_key = namada_core::types::token::masp_pin_tx_key(&owner.hash()); // Obtain the transaction pointer at the key // If we don't discard the error message then a test fails, @@ -1663,40 +1673,15 @@ impl ShieldedContext { ))) })?; - let tx_query = Query::eq("height", indexed_tx.height.to_string()) - // FIXME: actually this condition seems to be accepted, so I guess I - // can query the attribute of the events or the field of the - // response of a transaction - // FIXME: well it works but in the integration test where the - // responses are mocked - .and_eq("tx.index", indexed_tx.index.to_string()); - eprintln!("TX SEARCH IN COMPUTE PINNED BALANCE"); //FIXME: remove - let tx_results = client - .tx_search( - tx_query.clone(), - // TODO: currently we are not verifying the merkle - // proof, we should or at least ask a parameter to the - // user ot decide if we want proofs(and their - // verification) or not - false, - 1, - 100, - Order::Ascending, - ) + let block = client + .block(indexed_tx.height.0 as u32) .await .map_err(|e| Error::from(QueryError::General(e.to_string())))? - .txs; + .block + .data; - let tx = Tx::try_from( - tx_results - .first() - .ok_or_else(|| { - Error::Other("Missing expected pinned masp tx".to_string()) - })? - .tx - .as_ref(), - ) - .map_err(|e| Error::Other(e.to_string()))?; + let tx = Tx::try_from(block[indexed_tx.index.0 as usize].as_ref()) + .map_err(|e| Error::Other(e.to_string()))?; let shielded = match Transfer::try_from_slice(&tx.data().ok_or_else(|| { @@ -1716,7 +1701,7 @@ impl ShieldedContext { Error::Other("Missing masp transaction".to_string()) })?, Err(_) => { - // FIXME: add support for pinned ibc masp txs + // FIXME: add support for pinned ibc masp txs? // FIXME: probably need to review also how we do it in // fewtch_shielded_transfer return Err(Error::Other("IBC Masp pinned tx".to_string())); diff --git a/test_fixtures/masp_proofs/28A7EA5FE79BA929443DE88963FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin b/test_fixtures/masp_proofs/28A7EA5FE79BA929443DE88963FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin index dab21288854c57fc1864647231264f0898463c1a..0c1c12704ed5cee1546ff20313ed698b07654f28 100644 GIT binary patch delta 1742 zcmV;<1~K{7P1Q}XJ{cf>s8^+^3K;x9EIjPcE#%d&)F}aV+s6Jxx$GId@=ixnZvX^cnS@UYfR+lzLZQERUD{E5KS{`<_S#7E=WH@_b=9t?AI;f$P!0WUaxfY!8RB_Vb^o z`)9e1sNM_-Lo`3U=uEmD-&;Ir|NX3grPGY-t;x--cbtQP>@~%R5T*gC zV;wDGXSg_Fvww{mjlZzf0~86*LxUy>aYUIal1Dd=oa7GZSkr;_j7$=I7Ej`20jNqC znO}QVFkg)w(X>uYqR`$9j_4NBiSW7bOc>*;5W*q;1&Lw#Xj8nUptBo9l2n&Yu++Gp7!cF3T_3W-r>+4iQiY{+HLE&~6&VGL?mdi7Dzox5}7%SOO?I4^j( zaf{Hx@_%XHe>RO1ifXvoJ8>Dm1DrnWCNha>?HH<1+LL~41@K`m!G{AWB4~(i2(i0t zOrqzVBV3P%zVXfUEl#KdPlTYshNlrS_cJRWthD|dbpE6f=+YvXb@;Uwg%*X54BR6X z9g5%-3CT8MT5EFyphKU#_ju0i`S8j#?C2|qAb*~|Wk3R`dk#Z>(rVVEQf04%tAXOz ze-L@VDp{vni9m13ZD^!!=9|x8PWkI#a4y# zq~hf>wrK}LE^~th#3^Stq@mUYauVZ{Lh4;nNps{cx?`jB+xi5SWmW+DUE&}WVkIJz z$bYPu6F`#$g5T|$nud-=K3*%j@upHO9ZO~0G^x4ds&A7dGq)O@f?*6W%q?1D2nu}( zGO56BsC^5~R_~8}k-w;s~KtuK{R)6LF%#Wo`V0gFFQwQny7icsH+r(3KUalQS zY8rdl6!o(3cR~gX_dOTOMn9-D`;gmY8r_>Hvwu)euh_f1g7a8#&mk7|6w!)P=m6~lboXyuy`6trvXrGm}*m$ z03sOv&SiYlxRA0!o=4tNY$po`M1PyZ?~QnCqK{vQUn&&#K5U2_4n@|?%Xa45`K5ik z++^Sdb5UfGWwC7HxI3MVl!XAiq`i}VQ)s}I^5A=lwy8sTwN%MIZ_`8a>$0$S9N{1; zythVzqxTfdC~Et!UewO3G?_)nr#&hdzv|dhQe*UZs7b`;2cx3}K-#v~ZGZO2b){YE zDSTE^7_mC^4U-EUa<4$oPj3|%k1xuF#cR7Pkt^)z**Hs*thHnW+e!5d`CU0sbe5YS z%i_?R13jY4!HO-LUVsICeOM!;EIM-7&*?V?H3re~V}2dV#R`?wov#lGW6qdK$e3IF k<9+0nqZ-mZLmm^>Sj0Fw2@6rUl1DbFn-L}m0F#*}JfUJ-i~s-t delta 1742 zcmV;<1~K{7P1Q}XJ{chAz?mq=_a$!*fJ5x7SN{=xb-H0#JX~8mETk2D@PBZmESaL5im2Uv2H( zoHYW?rXG1B0&eBo<+g*hMluc6U5~afpp+TWcn+OGhS`a;pc?c7An0>xjufxG#7j?L zO|dVh(h`F4d@0M?h8?~L9yb^2ShKVpqykWPFz`a3pBB|tV6Ijdt>=^hQBTZn^8^~a z;)=7I@529f&#M!_B}W@v)b9lK-`uK14b=R%8VjfaxGJDt)y^hiCJr*J5YEhqbk^im zIO?&I87YyS2$R_$K!2)3CjacU%~zU+I<|rY6^eYL-pHd<9IXxY9gfK!42@nX6&3B0 ztu=4?grOkHZyl-bZg3MW8g|ElX<~lw*ANp5%-P3^txKZYKP9ht+lO8q(z-3#umq)U znAYC(raBd}9US+RnrsnO#sL$94ik=Mzyz+jc_iKB3Pzt81b+jvK3?3$Q~GyfH7)#x@?rH zr`>Dru*=OoDom=(BCzyLGDZ^aR@``L-`*_QL#f@&Ws}$ifMN{Bxs#eobfZP76BBL} zeN~HQzzoINuYa-jDxw>dD6j}s<^4g=NTy(?UMz7<18P>19>S;9pEy*dVqYgigE>XVGO(Q!i zJ*=PobjOdX8d~!%_=c-hoB02@ueLJWAe8p406=Ubqkn$YL#;C-B&&dBScrqd8qpJz zoJ*+mHoIawT8(4EIpU|a-fgrU*#l&^FcD>(I?0O2VXf;{#ekg}D3TR!nK*x@YzC6u zz!Cw0YRBLFXyc|!FZ3=!*Qz~e3g2=j0Uq+h4-jMdIKnuha_f3!xFM)K9~0SNEF4?~ zwaXiU-hTwkkpXVp_-Pkx@LqJAp=J#OPSvvy^uq{-$%0*Fhw?s*3p=@1riLX_rba~J zGoE`;8`$&KV>Q#P}O3k|3*T%p!cyLQc=w0B033|M$dw z#=M^%n-%$YU7VoCBJIGjM7k9$f`4>;xF|aA0*$_^HUCl(p8U*a%=YG3nQJC|bvssX z+d2#xYM~*B-D<2}XGzPO)oZMowSP%-z)Fnf zP|&@BDlmLq)n2jiVKCtnyxe~Nxwv?%$5$cW@X%KsrTu`2F;cpF(HK=hn&Vw96Ye9w z7+8=s0DB^qb6q9i9292^0Id4#!~tk*TXquWHBDdS4_3iLTJh<=T#0*Dd$Gr3;so225*3MZ-e#1CyC1JOXi8wg3PC diff --git a/test_fixtures/masp_proofs/2E68959DFE3412D892C1EB6A83C733C557AA7C18902CB851BB4DB93A834ED187.bin b/test_fixtures/masp_proofs/2E68959DFE3412D892C1EB6A83C733C557AA7C18902CB851BB4DB93A834ED187.bin index d7589ef1fad70758a5502b5f6664b9c858ee3258..1208f6c0728a860e4393030e39acf503f7a70cdd 100644 GIT binary patch delta 1002 zcmV|0Y4RLk;u- zAYxMRWSqy7n;g_qZiZcV;|Px^%(@y#>c(><^YOEG?6X%8^a3ESAwr?%S%7hdwCz&| z52hQ>edd)i)pg&6x#@K}vY9`#YZLSWAl(-33($v`TJ|~yce>@ZBAG%5E%%q&<{z>6 zP3`$DMYOa89`$vd~62_!#0T+i+P7Xnzs=SO*ApM z;^*{|8v+WFo_J*!%=Ko=9my)=!^~yJZ_nto*%`vCeXED`hI6I{mX!(B;DXe=lRKbU z+TisWe<9`!`IJ`RZYkK~5V83T$#>460(3;H zvGrlqc4S}Vpe+{9-njZVJnGgg%C&Gq{X5cl{!~ln+h^LE{^RIp1-Mu7W2*>9k%519 z?}W)vAExrD9;5jsdSc`2^^L^@iRJ-IM?@3LylRmrxXw&AsFT4763vZL zQsa17w0VC`kWNkJAN!1ZF{syJv5N>*nX0xXN+!^rwkZmO%+XwDXb^L{eju<3s%;SP zuh*}3Y`le{e!LzDR9Rv@oG!GC2|OtG`}anU&hds}0cIqSmXxek-F0=?bDFdon0Wi2 z^#O{9gtV}X_-(QFq{?WU zCdPjpQpOzP_TVcI)9X;iON3jTrAY^=Dh7UxBGj2O6~KOd2tP|>v=L5nJv4yn_ zb5G_}wMeP~L~yxjYh{tOq%C=qYEZ%~<5z#CdX3uB)rA=H$NJwoGi&{=Oenpyu@I?6 z{AsoNgiR^fx?TiF6!jAtg_!-9xX8Obp$a)ISj<%=Kyms|RboLwjb)&C;nb*xchLsb z-#S$i=90MonMP-!C6^VkNkWvhXR4|9K3~8`g|o1`qo*!2ARyIh9-SlNwQyfPRVzkp ziF1O2xgPpeA<*~BtLq{lkwXS@XA;&o;SrfXtS3D}D4jIEQ|t{m496+N->TXiT$I8F Ya2hU~f5kL6S7P&ryKN|%0h8n$G&hp;SO5S3 delta 1002 zcmVEXcI5t5CC$8+METQ|G#pmhpLF$jYBAgq<-^n8I}Iadx@kqPUjcW*4#R**nwTX>KV zs?xmyKr=~?!U^bQA!B859!U!LUWcJZVB#czk4m88NV7nEkSW!NHn)`fuThe z=BxFMfDiQS)kHkg>AJzLUf@hu)KbsSeJ9kMO@4#bH?=k$jCU;&+cTzvi}H_=(ksow zLP(2TL@j?w$#3ntX$pI{ss~%oMd!Bf=)DT@#&Q6%inhhUL`dO4TmyB9=07C(Bm*JV zQ2EXbw5=LTs2eD^yv}4_xY_h5GecpJl_cmSCe-ASJO$1F?zY|+O$KGbcIv?rHV)e~DDbhYLy@smzNDS-tE4zx^4EldUy$q8rPm&Ow3K3wqQbBx3l@XNT zVM)>s4Ufm}q}=tX+*`Ezuk1tw^9LOL*y6PtfYYZ@*F1u^poU(XqIz$H^VhAh8+C+W zZu~aaSIN!Ek%XsVDFdQf^-4o0zfVO?wPFcaCwOoN?_OX2W-mj2kMv2DUW)B`B^XM5SF1c%!na1QeLiknZD+r%c;%g60Z z^>Ke@@C03kMi=W;od*|!A$1pBzg5HplcRqt5L_`?Rzwc@TPofKagN18Z<|K(y`x7J zuSYniWPgCaH${q;4jJ=S38+F%>F Y(R2$XVpG!dIYjNEkxiDC1C!(&G_Q;DBLDyZ diff --git a/test_fixtures/masp_proofs/375F008787D3797A051D288892F6B0C21274416797B6508AE6C759537F947AC8.bin b/test_fixtures/masp_proofs/375F008787D3797A051D288892F6B0C21274416797B6508AE6C759537F947AC8.bin index 506df0e8deac5b7960cabbb0cb80d17ca19ae77c..e4ad8cebcb0a0bc577d0c7d0ae2551c7405ae37a 100644 GIT binary patch delta 1002 zcmV250+I_ zDEBjpL$iMtB?Etyu@F<_x~p4>n4O%#peM3VA%t-gev}b(Z>ew#L)d1(LzJ9P!yJfg zDc+JqHHJ5^5jcSRl4Me_!u%#fN3kT~87D&=9N9quM!BMdE5Mc1?6m)y&rnJo{4@2A^`>g%ClV^E8VKIUNrsfyQHg@Ba} zDn8#xQ&)@ug^`Sl1>r3c7#4JcnjbH>N=n4L^Z4h}-o8L;mM_9PAr^XSuB6YeDc{4S zvRKUouLyr5Y~FH~n@}OgD$co_;_I1QFLRn^c8?5_f-PX>7JkT{CL?2cR(#6G{0Iw;t zj8T8Gj4wRB0}mXW)7tIEx-8g|tM380DhrOIjp_8D@yPc}jqnS4pR7&nkHd0V3W~Ya zC^2M&l|8G{YH5jVs;M^44z-uKg%)FE7TPxF=OrkM!&)9vxvs?i5~6xTzw4O6c2*^l zxxU_zl7ov}0lW9}`QfIc-IDD^FPi{z$LN2t4(5up+upJl} zV~47|fC`(-FHvLf6^DXoS9)lhp$1tF7<1{4vcjPaMa9H;&hLx`om1_FG+EORP;zAA zdzo$4rM0(efpP|a$@pJX!6q5qY&d*EFgaB5XKpzKoKu<{j3d!;;zlt>Wb10_I6g-C zamPuLz6;>uS1xHkQo+{ZB%KW)#XEbh($ delta 1002 zcmV~VrP;% zxjG;~C*ef^d-??m%uptEQF0^E)U$sUB?EtwU`uw*vk%xubz(YQfc>z3vqU&v-Eb|l<2gaCuRi(~Wz=_Rg?IP1 zo&V9*V1-GUEhzP2HaNi+{knR$owNW>^-8=PB3sOmJMmg@A_az;AyQ|7)DQJKS7tf_ zf&KmjJKL=J1K1Zk1z3&`gxO^dDo{8Wr-YKt{X_n~Kt6wjDb)xfDPc62>bbjaY*gPx zsIMex-^fCwWl=_iOF_r}$+C{tF;;vA)-?MxdygWwjbfW~*msH>sOv$du5=zSwHF>w z`1DWjRO;|$K;gb^iqeF@>Q` zVgyXfu4Ku*i|qhcw)wYMN5_q<((-z})hBY&c!H$RpLUAuz=kxg3+TK}?R}SPfWW`a zdZ5!q>Yk-jHAm3o^f*jfE1)7SmW@KR$*2rHHR0eb?x0KfRpd`2jX;*NF6cKOs4Sm; z!_t3FuS)DwAl=twfg0H%Cvrjd-+R!VOxG7E0> zk-PB#SE})I{L;@yL#NlUtX90AOr~z2UEY6P2yWn89Ek;HnQJbLxqGFJOV47tzJfL}u;SmPD?cPl@Bo5dicT zt|KcU;&I*QrLpe8i6GHrGk7pF$Hk}`FnEvOK#(bQwM-_7x@gZO;#v(3+LGVc2~78grFVvUF%uIeN3}LHfp_JG Yii69Ahh60Pc5Je~H43c&1C!(&G!KOIl>h($ diff --git a/test_fixtures/masp_proofs/51EF94B13138B91DB3081F95193F779253B53A9A0BD62FD3DF8F6FCF4AF1E145.bin b/test_fixtures/masp_proofs/51EF94B13138B91DB3081F95193F779253B53A9A0BD62FD3DF8F6FCF4AF1E145.bin deleted file mode 100644 index b142e6796d50a608ea20b778d7b3facb2c9cff06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17018 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 diff --git a/test_fixtures/masp_proofs/68DE980FCC7CC858B090D50340BAE37AFDAA28B0701ED5D313EA859FC8153343.bin b/test_fixtures/masp_proofs/68DE980FCC7CC858B090D50340BAE37AFDAA28B0701ED5D313EA859FC8153343.bin index 6c150f6d2c46817cc5f68e97330ed1b1df2956f1..73f8d2584d77a8b1221f5a93e05edcdf48063f64 100644 GIT binary patch delta 1001 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�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 u$r5jzbZV`!+~6NK*HsPx diff --git a/test_fixtures/masp_proofs/8032CA7B951C625E43F48AEBD53CEA99A9BC66B2BAB03D4ABA1AE57B12596061.bin b/test_fixtures/masp_proofs/8032CA7B951C625E43F48AEBD53CEA99A9BC66B2BAB03D4ABA1AE57B12596061.bin deleted file mode 100644 index cce1836fd8c7b690adbc526badc0accadbe85162..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/852AFF2FF8758999DA709605017C1886347E49C18C8E0F35D25956CA06390B17.bin b/test_fixtures/masp_proofs/852AFF2FF8758999DA709605017C1886347E49C18C8E0F35D25956CA06390B17.bin index 80ce28042819b42c15626580fccc5c9e335fe2fc..c0a9e3ab9e3a7f90dd08bacf45e12de012829a58 100644 GIT binary patch delta 1002 zcmVxo1;9E0e8wW>^^|Y3Lk;u- zAj1P2jKw8r_O4F-jRQ@86>yXON>Y4NITyWeaUoVvPqSAL^a3E!pU97|&dq$4O4yX_ z+Sdk&LNHVe!;kx0Vjf36iO;XIYZLSWAmSCCBa=v1Pc7gei^@b@bgrPUZ1W-p8ywG4 zFQ~Ujkh6alB?EtAKCVtwFhu!sT+Yxuc7Q7=+xoOR|*BkSY7v>=B;xOEmlBS-!H~)QJxu%O&!_A}a zxt?HdlPaoz;+>Sb$~nDEapMg-)Bw+)``gX^KJ|arM1PnW`0ER8)0RnC{7x3w#~m^L z%Vl;3RZ$lfd%X%#FnN}Vvj#JCkibmVDQ`ZMEHk4R>yU#;#e+TsCkLZy_t&Y4HYxrdj8S_M1jx|xg4xny<5VAv8fA>gbzmOsEb-s*3K?4MrwLGj#MFYdSF`J_hyv% z&y_oBPNdkysp(7F0@iV>;=Z0~JC|DQZz6w=vV{|7%W!0VqC~!4_b~Ke%)9h*rlFHo#RmyVA?Hr zKMrcj4q`g9h}tgh31mCf+XFf6X)?h2WF1wTz`KRj)Sx=|5SzQj32lLNX$7uNGfN^9}FLDbk&tqpSibZBg>?*Xrv>OT{jAs{+<<;&Ow4`F^y9 z&mI3Y9<32ajW3OO(bCutzw6V_cPB{b?%UYc3eHZ1NaaM5@vMJ}20l>m;RQ3y7R+`L zVR+EZvY|!`Ud{8QJ-sM^20a2GLL`3X&Zgli_mF=$vc9+=mVxu1f&3SXX}MLv+#%2J zaZ!x1BSh*b`^)vV?KQ7n%^JfQmx0%?XWSGqdzOmaJLFSQT-V3$vTaZY6Ejz3&`^Pg zM42fSOM#gLlMeh>f?m1$YLB}7MtJsC0GJX8C(1F1_otY&0%!PB{F)f9T~dC{(fjK0 z=JbE9w#si32h?}dKTF!}PuB1VW%hA`G)UBG4RZ3|CncebL+I2$7zvxeI9OEZkAuHJ zq#g;e1MRnH{FWWgXs(?WRs^=Pv3%*^g%>)`a%&1w<|#7&PkElVr<-!M&t2nc4{&TX zCkaxh-n<5|KTX`!*v2=mB(uYpreOAVMZ$l+7J-QF=EFV6^ou@`XR>gy>)kPKkuttF z-URojeD6OA94rn#4dGg?H5?h+%WGL3MzhEYaSgMuE%Sj4g%2q#H+dYKLf8Hf6M{Os zq6@Zb2Ozh{rx!~sKXl<+1Tk$=8?c?(T%0^NMn(mOjnD(d@iDLdYhtzR>7DTFukl8h z7vX-=lXA^Tl2cv~x2eH&UTokGE3!6ywaNe@E@lBf>-WD<>y@B&3p>4)D*D*(CKcd; Y!}U}Mz6Y$`@KW^2y=Kp44wK{@G{k-Cga7~l diff --git a/test_fixtures/masp_proofs/8A79BF5E0292339E70287DB626F136D4522BAFC99E0E81FD54A28A38E8419CFC.bin b/test_fixtures/masp_proofs/8A79BF5E0292339E70287DB626F136D4522BAFC99E0E81FD54A28A38E8419CFC.bin deleted file mode 100644 index c74348c48c549d9cb35d9f7b00f273e92b6b05b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20518 zcmeI4Wo%sA(x%N2GbZLXGcz;B%n&m(#*UepnPO(U%^Wi`Gcz-@J#*%sNHe36&X4>3 zm`^ubYSnJ_UTdkUUp=Z_rL{moKtM45=jZnY>T21m9~(q}a;x}7{Wy+fFt3>bOZMPQ zNUxXxH0^+t1oZDGK~ur{3U>4BI(0L!N&0+XKJb4?$GipK3Virk?5XUD+d%xD@-GYY z;QxdeIZ;GRz`iGiOst0`5%C87aA+7;MX`?M`E#mG4FeS}S_Tf6s}8;^Z#==l+@ZUE zBv;d11QEWYhzK>YK#1}9;cOOBqN}XYQ{LtY@MY>A1TLhH42Qi>&Pzq$KD%N&K`$#E zDK;x9x`9P~RR&zTfe$yThJ%m5BmiR^WbB!L$}--(#Ia@YW**pi>qg}W+W;*oAh_#z z%bI?)8T%RgDBU$tc`OpE-iof5?69FfOiCni%Gvxd{mArdQeKMC>WT≫%n=y53yO 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 diff --git a/test_fixtures/masp_proofs/9267939E4DFBF958BF98337A9099BB39683C7B8AAB924B267369B3143A3FBF89.bin b/test_fixtures/masp_proofs/9267939E4DFBF958BF98337A9099BB39683C7B8AAB924B267369B3143A3FBF89.bin index 57a0d2c3906146fe035a19023a1c16e387347c9a..04819e3f37fe77b9dcc79b968264e3b6d482f61d 100644 GIT binary patch delta 1781 zcmVFbkbzLBS~f%-gLCSkSl;342h9i3diPy!S|A~lj~hTB zP*%A5ld1hbhVuv!~qwiJ@UKY@@-ThFV1Jv9|It5IGu!^h&HZ3Tha)&%(@II;k1h^jP;# zp7`Zr%lMP;41jx5(sdJu*AO^J~(M1?l|E2@WehqP7N@APsEvs(2lu328(P8$V3IN8@=LRdmMy*fUsJmBrq?0ilDUqEBlgc7M ze~EHh5r}mAo+XDwtr^dpi-Ay*MvsUQ)yk#4>lx~EudCsyL(d-r_H{bUTT1~?B9tuL zLUF5@3WcGp!&wmSwHo3kDcY%Nv`phB=vB!5m%es+pO|Xo>so!;KOvDj#}KZnUiA^4 zWytvsNhUxz0io!qE6=%;9H33ksa*oif3pthXV7l01LHariqM}&r#G}m6&~BM&|``E zBoRzd&sEFWxknWra*Jl#(9DVYzDIXa|M9&?n8Sol?;=~)e_mZn zR3>2ER6B;eb@I^XkhkZQ*g6qFOLxwSY)^pa@pwxCFqDU9<$lyZ34nmXEP4+o)uG-O z#@kVRm+{CwowB1}f1GwFx*cE32yGY9R&K<->28u?Uc9$W=lX26DRhqX z2yFca>nl%IUJf$qRv(-KZ_FkJo}k>szut7UY!L()lwb@2ne~0RtR#kpbXNcegA=nY zvsSU4Fb$ePM<(Xs$IKbGcT})Q?y-I2KKsTI3&rj&Mp@GBO7c{H#GHK1e{TEa0*Nhq z)u?z}2<*h36N#$iz||!zxn~V9`MnT%RjR;B_IN3f* z-U%d{g!5_+k%mI%0QvC8f8W1M*WJJXF3q3w@uEOp-O4p!fjWotS`aOBAa3wubd^<{ zvNCPv&_Xl}(%S?P_fP8okjmMeb>u!6L-e~PB(NJ=(AO$No@ zzU)U2qGxVFYq8;`8A?sA zS(z6g>*q`PS`;;ae|lMaC`A{3h0F0DJ}rg{TVw;4uJsDa-pysI`t{@)ldp3wa5-)w znxA6H6)GHfyb_t|+g(Koxmu${mBKvr?^{M~I1!z@IzRZK^FGBPn@R&W;M#~cy0}_~ zT)h=K({sZ~&80Nb0oM62@4!#Ns3b7U={ychuM?lAQxAW)f2~#MuKpuHeKQ=9Tgn5~ z{YfE|t4)iEj-{ap-@9t-rMvsZ@zAUgDa`2afB3*YkT+4&41=>3c#mhGWl;DV^X3nA$vC%*zNc zUGS-_-fs6IAW&aSUNC=1QYe<6p8(EyR(E!?8i|F3IORr1zkn=C7Q@LJ4c(8>Yw{_- z`h<#pC_Ud3u0o$?C8%zE@~v#whiNJonWTd8!H(s-O7nIGwqr|IzDU0Y;*H#$TL3@o zCQ2n~Xp&+R0A$EVJT}1&K_E3btZlW0=ItGo_-L=g=$iA2(p*N~t2OmH9lxGUal2S` XM1l>IUMMJ&UMLg@1^@s600000VwZJ- delta 1781 zcmVUpCsxO(FSdNV1v3NCZ98rszF$>t4)o^zAU#xAlkXcrVXqg9>mc*Ep*heK*|@dv zVti~Ez6&=NxEo)(6u4f;RB@BF+j>5S7Rj7ck~~}x?a=zn`2S>`#E#GTY&4N_P67$_ zipql#+ zbb?Z-c8C=}X$b3bW<87v+8&7ov8h|Xv;j%}R8`s8N@4bXhHSQVm`IG2I;k1h^jP;# zp7`Zr%lMP*@V(KRDxHEmX9Aoq^yYPsO7IXl`VBtXAPZdCmn`uR3`)ojl1Q z3aBnBJ;kjml^7MBrr-TEW^DDM;co}k^+GzFi^*W>-p3NOSOICA$dfS}DUqEBlgc7M zf2(Fsfqq&pwFJH4Fc2=KTIRguC&^=5N=E`(cA!93vtaTC(p47mkV-W$*{wGZ^Bo zzhAEdvYOo=c~0|GHXjHrxSuWTfAtn`c{tiM&q%77nY`cz?e*2h&DW zQq_s->8R7U7q#*$hI;&1mUnL{u_NVbd03QOcNnUjB~Z$rjIsY6B_Ub%e>8a@0)PQ2 zQNwz^`NoTFE0tG^)OT4>^Gt#WXJ~p95Vo8qc2erBk3RGSMq1ifHAt!Ji-Xsf7e~2* z{^Uih_xC&%&Wv&YzKoa(s?4A}(8}W^wI0Po_>|!%^B=omN(zI0>F*T zcK^AWGKLiG)kV;Jck2fVe+T<<6sQ#F`woo|@Q1RkP(e!GPkpc6RRaDYe=vaMQs~hV zoyo2;fxeLN5)^#7@8@JxyeSQaS}fc~+>3bsxR?X}XoZK0$VRky7MRa%m`qihd#``5 zd;qVR6n>#m7Z@2zkmz*$b@}8Wpm7;0_51Sp;ZUb*=uNPP;Rz$df1q-5d@iZ`13kAR zhG*U`X-dAf>FPLh*^jWDOa4X%3>jgKcqAbVBw+DL$?i{|UH*V@$*t846 z?se#7`y=tFUB)>nf0AJRDC*S8fWSoSY|530pNf3|R?&iH`J08p{Ox26hUbB?Ip zs?Gw)dbIQoc)Kt->_Hyhdv!Su6nAy)YGMqgb+WtUPNI$^iDEW4WOg6WNIA+FWg-f# zd8FTe$k}EZe^Z|X246ITYkoe|``pT4_J@?ch1WKef6|Su`^+!ofzwcKYP8;->-qxi zJ4y;@9->@gROec@b^j$s0gO$|9{b3j*{05|j*B`%5#A??1rY3yIYV-hLo0y&@nO#n zuSZ%JyRWC3yZX&PTEz8kOH4y|izBOI;8|>v;;iVyN^pY{;fdZumi-2D!<8U6Dy&LR z_)H|Yzktz;zR_77QU3@XF7uC~L6{m0$HFBC1Y3gIg>I}4X2q+wpUe-m(ip>^un)Sl Xh7k^vUMMJ&UMLg@1^@s600000pFwng diff --git a/test_fixtures/masp_proofs/94DF56F3CCC7E6F588F0CB82C2ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin b/test_fixtures/masp_proofs/94DF56F3CCC7E6F588F0CB82C2ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin index a139159fa4829a846a3893c7314147abd2ae8117..80a445cbbc84668564e90944cf69385858b57bc9 100644 GIT binary patch delta 1002 zcmV0DU1%YLk;u- zAZl_6x-sAQ1`_){dsHYKC)g7EQG{N!T*Q^+`YZLSWAhsne;{*3gR(**!j-IxyAcV7Ed^~PqqBJ<$ z=ef|*fwO-WB?Et|Eo|mBwWv`GFG(gwuW=oNmsuPoiL&$MChFa1k&tPlJ=<5;QWSgN z3J)jD@}bb6SX-|Z5Mi=ITX|Y(#w<8)A0?CEnpEqTCf46WVzn6x4l~xoF2aexhR98e zbkU3n7X*Y+q7jl2j6kwrKXXvzFnp|k1Fd*_b>~as{Uv|(M#hpcRW}qNF3Ii{42=^c zvOq}dBOnzRNmy^t1m<&z)XWvDhfDmpsc;Tc*zxTg-5oPF-Rymfl2sM)gLBV=iGT9Y z?Ux3z-=Ujx1MbvVmo5VBZL->Se{83a1_z=m2%P$~zrkz)bJ9c{+cOnd#w|%2FIeVssbk#>iWxN7dupAy%r95IpdE^>STuK?4ac3l#dHAyT1p zCRK+OwGk?GcDMW8o3q)+MpLSjT6Qp|@?5LNfahx8T}Rd-%ZI(3Ojl2TaF^1ODmZfU z*{V~Xz5M#96xFee`pF)^I#c=(xf|qWQTWhm$47q(z8#@6Jn~0G;Q8$5;Nn1PpI`S3 zK$j^VXR0xg%s?&w?3Op%)uK-Yy`S29duuF-Gn9}SQ8raQt65L+Aci2`w2z0B>6^V8 zP@fAVmtO6n1B8P;?qjV)k^_DA><<#>NB7(J&2+H(0_B9;b6* z603+Uo{w}=*`v16Rhx8GdgZu=C7#~cT2GQd3jPB|h>9KaV<8R#h|1O=Wcg$yr%g9! zB@tw8<FtaFnD+ts0;A$p%R(YNu53&Ol+Sp}0*kJD7V>JnTgjeSg?v7o&8K*KZ)vGS zws^ryXXVOpvf>{b6h(@z`Q*VDnR9b)FV?}5+Ut@@z6#p_uCtMoT?5$RDg#IX=DbGY zgf}wO*5PcKk+UH$;CXecYfQMxM*fu4t9~Lawkb8r;0njgX0@i`1TsgttK|`l2HZw* YLDnMj&V0!=Bx}W?UZHF+1C!(&G!2;RAOHXW delta 1002 zcmV1RgXx!}zV453OqXzONzOz>l^a3D;0t~<)mV%q;zU7{A z6K_Nb8O1jS_LiXAvI?Xq^g1Q8YZLSWAOCH&`N5B2dG>q7^cy=`jM-ZQC?8ElYem-B6r)bLzL(KU-mroq3 zDv{nsM|4L9~XyzF&X&)PAG*2$ltE@hF{}X`0G; zgfjxW2N188j=rVDdxZmzck3H$owGlE{x*J@5RHxXS-XVgIP0B>F``!XaE*MnffS!u zDBd)Phu>xYmpJ53sgdyJWd}4QG`CBK@Yi~oqmtTYbHer2BZZgta?}9kps#Xw>BA=) zndqQ<2dIB?vOCv&nwF0#(OXIFKC|032$0W@m)-JzQ8`c~c@l~72Ly5+p^bo(M(Q)n z34lbid-bB2;&5HUZce5I&AJv19yFl>`A7n~Zs3on`x;8YlD4qy03I;(6NZ$;+QSP` z%$ZEi&6A(GPx^+K!IzUrWXEjg5Z_944$CzcxsJ3n3vFO{z^Syp~Bh zSy9uWwC1F@ao-BoV^O_uKrpLtC`_7-N4d{XovQ9ZvGd~yH@m}VYqfSR>b~*lc)NCdsPc@45vwF9V zCWn7WnV>AAjVS;Evg*HEPlzIIXD;`pAe_uddjZ|`EJd1dn(D;kBEaYpY4KFoVB8qr zV#Ydjm~!sRfKx;xn9H9%$z4p7q6@YmGK$|m>@T0tFs=S^EUNH^I?)cjpnu4P%UeZa z3_XnDB%+iL9B9j8u@Op#gUkS|r2t=j)6##=%jOxhjxB}9GP(Gk31Yv^)xRpvur6E# z_jWuhWOx`%)4IgZ)wk3lZJtz&|he1c`(SfAF&$8-0GoHV2syjwg z`bbw)Wr)Hq{jP2t+>^}C772+80&T9W=c$)bD4F?_Pvt?|iSY4t3k-P@yS!ptxv|Hu Y{$)2Xj0)Yf-4jd~_VHE81(W0)G$Z2gaR2}S diff --git a/test_fixtures/masp_proofs/A2385FC511BDA00B7E8605BF05958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin b/test_fixtures/masp_proofs/A2385FC511BDA00B7E8605BF05958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin index dba4c4a2423899792f0475c38a2aa33cf3bce582..0be8a6ac33a32da71a379595777d94e7f2718788 100644 GIT binary patch delta 1214 zcmV;v1VQ_aG>tT{<_;jmG^SeHxlf-6kLLeA8YxbF+4?u{u)RHmROu<6cUWeV1`j|W zjpe|>YPV&(fi)~aLqTvw^g>E*DwF&-R-gAq5eAa~H}nJKERr?Us)VB{v3 zhAJmKNYw9EbYk;OGgu-CS+mFw^a3Czq%R|PVr3_^!_g88`FNYS7^J8s_s0N&hlbBR z2hXIl+!CY$P!M799$O2Btqr}`KTv{*k%uW7EOh()DJ19IdYWoWL$#jxG(hDfu?Hpq zHq~Wy+C?-xGd#yy8bx}$_3J^OESGNE(r<1D<~ywyo6o1BXp_(tQGbm|?{ift~J;k;*J98wE3s>=2PNd%d8 zqt21;P5p~5h?0xUW=mw)TL5lH7$?5(3Cq`q-Ip$ya$qyB$ChcA3UF`6?Q^a9y79@E z8=08yTDUzx-7oE|sV~nen}zG;jPz)&dEh)D*(_Z`V1C&3J%9dBoSfPDPYwjlF+mQ< z_int3Ea7VzCz<^~f^tw8dS01N7aGUwg`n-kfysk(b`jR2;3mIX(L-sk0+DZr^${leqketxW9>El~um51%%D-DrNdkIlmWv9=Zj`q1$yo7oQPqey>@@ z^*+*m83snZ^7Ya80^O!ue|@`NdQev9jRAwBlyx}^{C{hW(*TQw`8Ze9I1z1x)>zra zfG(8d=f1&2`>A6qs_IpL10Rdt9^8b$mu2RV|75+QqP4EcRn}Ta5wpaspg~AHqx_vKQex%*9OTb@UNYF(3-Vcn>6hw^4e87nVxb1Qd3Z^`IMW zB(6E`ZGSrD2#X$LNL0-Ss>i`87UMd2u!YHjqV0h9ncXyOreYUEh3TZSDFdJ5e6p0W zH=br7S4`xUJ8JS7_$xe_wcsa9Q9h%KECzbDtTP9xAH)4mM2mO*5*yl&@#EubYE_mI z&CWn;TEhGDM)!IADr~K4$+(!f9X)eoq9u1Voqq$}sL5yxD^C3(I2oa3G(s2?1)l3m zVq-SpNt(c0gSkj#u>JJt84cP6EdG{VdFKbnT1CB!0A`FE7y9Vcf>T=~Wtc3*BV~uG z-MJgOXvw%o$D00As$5t)EziZ0X+Up*WMff;k{Uvq4&t@UJbq_XH-R@TQSQw&(D5HL z=0}t9-poLS=_X)mp)Vx>Wi~=wrX{H|nxxS~;E+kDK6CnMG!^-PW@@qS!vg#1QXAiN cw5mnYOvLIw(@A{hNF1gQ&A&N<1C!wyERhyYdH?_b delta 1214 zcmV;v1VQ_aG>tT{<_;jarGT?VUj+erW}k9Bh76l;waIO6URL&FZr!#-j8g=Y1`j|W zMik~w-~Gyb6QU3IA*$DyqVDMyRSg(8q{Vidr}XE^lV%S>Ah1o%eQ(>8jv8}ogCk%Zh+h~jTaO7h&fxsN?;uczBKT{0t^_gHt=KA}@ zdmeXTpbGZp=>NWJvVZLkOlST?j74csCwFVM&?S(7HhJ$+%x70O&eg^2er@^42Q-a~ zuu?l@5ov*`ydENeI~=G#V^2NoNt-cH-~S%Bg;Dvio9vCvWG5^_{ebcJ7fj`05VSq{ zP14<-OF!ATJ~^c?U#RO?B`#Zf(SBX6kA32g>59DFbTQmjW`9Iui}V+{CN6#Mktrqu zLS*OeY(PN><%6z{9%xR|hnnHe4$FGFe+s$X;!}>`C;J<|fDWA6;!Hd*Qw9gEobYX2{MNfTlrh`SUq+LG+Q4OXgI z4U&in2H$A7VQeX~OfdD=(8y*MU+AW@Lo>u-NP*eYNzN%rDKnB$uqj0J#LaHb0e zKIRZ6bbq*9h8smCL;nHRzB;c@in9vSs2?4Gz3tk}52zLAqw$&XlgAaInpITj#mJYG z&4nG~rid7wsF4c>&>o(TIGr!D<|YNN6;xoev-GsX3cfGU@pjU;r(wM>D72Z0{qwt| z4=H)~i*1As5g{0oU$ywMtlSB??oBX1>8icNL4Qeb93^ZP@6m>E?+#cg-`VVG5L~>4D83#UKxaZ8dV>h?e?BE$wm;=V7 zam~0&NJ%G%){hGJeX+XlE@V<_SrHwF z*++O=!s6j|T~K`HQgn@0?)TA+fThuNYYY-~P@F|`frtz`C5+o+DgZF@H~d&d7Y&o0 cE}o(-HV~ifg-+}$4Guw(vZm+j1(V?!EVqzO>Hq)$ diff --git a/test_fixtures/masp_proofs/A312CDD49C05B7C768F5DAF708C010E6D95775165E4FC619A11DCFDB59E21D30.bin b/test_fixtures/masp_proofs/A312CDD49C05B7C768F5DAF708C010E6D95775165E4FC619A11DCFDB59E21D30.bin deleted file mode 100644 index 67decdde40742831de0381eb95bbcd660c44f7a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6669 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 diff --git a/test_fixtures/masp_proofs/A8A9963AC2983B576BAE4DE9BA6D2CF14C7F9E90A8588BA75DA0D2860F36E2CB.bin b/test_fixtures/masp_proofs/A8A9963AC2983B576BAE4DE9BA6D2CF14C7F9E90A8588BA75DA0D2860F36E2CB.bin deleted file mode 100644 index 9e942f81f5005e2591a70ab551c8bb02e5eeeaa0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9649 zcmeI2bx>VTmd9}j4<|T*3l9qrEV#S#Ah;&DI|R4j?rwqLE)Vxdf&~J>LU1QRg1heI zHifs1PoLB9aBy%0e``Mt{%=j1pnU;Q+|Tx+@JfHC z5WumyRrf5>wU;WQ`4;47e&`*QMyQhdya~-9Tgr`Qvn_4xl z0)qIO1p?-TICk9LyA!b^{N3;GeWi~GI#$eB37C>+QyvFwNFp13hkEhdB(auZiNrU1 zyimc}=<=uuRZdiF!{wAKYkMVuh;%#4Ge!MV9nLfN#d$0K4B$h|!ek2% z$`vHqU{lj8GbX^%*=^?0uVumR+%5C(tlOMIO5|JP`te3}(deoQ6~~SguN4kIN$A0G zqRD(&!Nk+pJhzWe+IqfBjWcNL+z3~fdx^=N<6eP)>%F?9p7TV4`hycC?m=E3z9%A{ za>y;ufYHMc=_uPU#w$v%hD;D--EU7)&+_>}61Uf0hFJWCh(sWtJqrj40G1@KhMo8H z;D`7hNZu3=yPH8*`msJu9WKI-7T>8pU;$MGSIx+NlI*7-qRA0A5~U!%y2aVAAb<-% zFp)MfWdS1YOa;34&l*BE)^f=+W|*HQ%Ot#K?WN_j(P0mL)ASsOJ%=#2BTm=oa7fmx zRlMI;g0>PMr>?pnCSyYCr#(o4BIzfPs7X~v4y3LodOwwSy?*YV7ZvGuQlqu)MX*+Q zKtS~dmqQL0BxN4%>se+cD6o~{T+|A(LPbEaPdgyc2NY1-WQXNF1>u)>O%h4;Op{JV zf2cN&mMlAaQe&n1$pB(9XHzL(B)5B|9|_DCktRQ$E2 z{Bp^xHCNpB)632)^vE|hPLjtF3D<0HlImo@*Qkz3Nk{1eLdA9N&ekszZzU`yF3WxS zOd(-Iu)UASz%zHWhRn!jsopb6lTf+-LXBWX2M7{+wNiSBx(N@B4iMoAwq&x4)4ctr zclDv<0-I~Ogx6jJ;-g7wT*~#nz#Ne4{d#b+-Ba`q-gnchVGYKz!+dDWfag!Ajjdc( zqX`D=XDqt1&F6d_{Mud+JI&4+Ng2{LpWv*%e6CX|wwGuKEMlQXu~iXzpu|^Hvy-jh zV=!hSVq%&N9x?85nEnV603tYb^eT+duIjh0Ur()0gzS2HlD~Ar;fnlZY^^|qKn1k& zdJeV7*xB%@CJ6@RdG%NC!k>4fky(h&xjioQEz<`0t^2ISBM`-Hp;<6=s3P<|Mh6dmn%So7Gj(cscMS-&k zieg6ZR`VyU8K51V+S{u4d`trbSl1QwL`XphweEJX+wggzhvKhNyf{rMIR~L$x3?Me zmf-(Is((KJA5oZR(DZz?gTSGXRDI(N@fg8EbqWwUOq{0v5%}VTShd!#J@5ZaVSfka zPxT^*tUBH+z6qTVw0KeqR`d_d z0N3-p_dJZDgZo3_KNS8$;s0O?=XvX3*tPk_UUkDT243Te*!2;1ed8{p2ik3ZtK)HHoZ{JZNP z%Kf3-AIkkZ$~oC-W5@YY8nt@drf49Uy)w%DNJ5_MnY1Wv=b6^-;j2!We#U9Z;3ylj0!I zk{{9)tL$N3m&j20OqUo>U>i~?^Nw6doG)`mkRC95W`wmp<77~KeCyNuRQZHAWpgLZ zD0yF{B8C7}*GZr6A4owUCVK6_Av*LdXdq%)wPWiPA--ACtp#zzB!Q=i%z{E$jq;!H z`D^eyViDi|D-RzYQnl95JI7~o%pM+pxi?0AEiT=iKX>CvRJ@QImC8mL@r!VOi1mk9 ze~9((h;;<_F|gSuXP7!IFV)&*R^f>Ba$!0w;BL>j*ywHkofWXMD!C={-H(GcRiu{p zLu{>gLstv`Q9&(Vz`dyXmH10Q9n@flT`Y$t=oUqQ&_v8)So*_JKCazfwi9s(Cb}K^5&}v7bS?|{X}cHy|SMk!Ixu&ndHaFEDm@yzpL0No$FE?#d-zW?@ znRHPdN>6qg3AuVr-e+=-(;--5wRPuBAeEp^! z;`yc&884WAEw~S1M4(KK%Zal_xwg3j;lfCcgHtu$eK@$~X|UNm?nqsb4dY8h<$W^@ zWy2(NuX<$#^THocm%t|5DDFElO=taUcCqBIU(-^zxI?yX+!VSA&Od2?s3z#SoNFg! z!=IFVFPgqEWR3q|8ap)~u+2V=OT-=tj$BN2oIM9ES|It(dl7;MUT zqDO#Q5C+E2<9d|jO3h0!kIujkRH|0C(2}dPi3>jbRQH z*%iMLeuvTJ7>#J`F3P2b(`Lcw&SnZ&O?x|{n{A| zVaRg^bS+Cc2p30obq6vh;3m=+^a^1HOP<`c?YO-@+jt84t<#l033hW38k6|)arO3^ zNA&CiTiT2rgBMha=Uyr_5YZ2Zltjh{o23yHzDkOlPOqAQjkk#*JgDmyr9@4IN#y1$ z;3uu*va@q^T<3IaBstE>WMDNnEb_ymcdY|TJ)&o0@Lt_KBsR*eEb0#M(`a;^sq0C$ z%a%JBv*ML=qDRrRPqh1G{XezMP^Rr9j`Ih$;I7|2nP)H($*oGaf+k*5<|K3-RTyg` z4_OYds^L|-m&ZnEFV2@{5y;FQe8z$L5!iRwoRTVi4F2L{vrrn@)AL>&UToTxZb^#E zfiYOhjxg(kj$kBsU8f~|ev=Gx&Wf{}u}0k26=HB4@|MmQ^%_B*#7ZDFLV~+e+yaLR zU39dn*1I`_?sisp=HYD8zub3XnOH6vqjCbR{$tp%O1+8xrhL9u;3uYR+jmT~ z@H?F+gJ{>JEjvj><^=RBH;B5M(d-YUO5Mcd-%1#p$hvN#=8{uDKG+LwW2|ln7mHw8 zJ)WIx)3&v@@h?15Qj}D|Cc2{2#yPFs5wSu$794nEcJJ!wyalj|wz-xYF9?4$xC+bPS>>UeGUd+w zCP*(^7tvzwuPVnt8+qH1oBKk5k%pQ3`Am$^-dE8#$JlArq~h993>&l zkD|AU3;ECr#e9@93`pn*_MYRp_BZ<~m~x?2eObE6w^U~0Am1g_pQ7laL^{sjwz2 zCaNbYQx(ixLjNM*#><&S*CMW{c#`O5i?~pc#zVpk&m$f#@U%+8CSa53{zm4L_X`S$ zsvq@O(3ZfX*x?D+I3#!x@pM7#ba@HR6`nZuv1dkcmd zFuDlquSRpj?r{vU}H1ovf-ducFaBqn0U5dH6S>mFX<4nPZznm zo=1QYIjP{H-a8+Zj>kMmNpvd4WT&s)%0}7p-P=({uJGd;8oH(lCW@ZHjcp5`IF>4f zUFg&!UnPFWQ`#JiJBG@q>_V4R)T}rq+*YzX7EW3-sBq+F*E?b;Ti&Q`Mp35uWSdkW zNykr3nqjeqN7-XjBZT+d2Co$i!{WNSjd4o2FcJB9&@3(;&0slh5n0b@KV!Zgd~m>~ zsZEjV%$Sf%S9DUa&2qhUa(+D7jcT1$ld_B}f67_Pwg0^R*Z}AC9_P~%A5h65b=U|4 znq2K=&nVl?5iTfS_3X&svs%ZB*F^t7gEste7McgiP*q#lO3vYBv=X$`w~xsZiBwS= z^8)jX(f`tiFKO!$??6jrykKRrCEEm1I(d$}a951x0-V;@!cz<~u@%%OZ9GpwCJb)p z(6-mc>JF3B@Hrvh7nkz*EzS{ya0~D-+!-b{s^7s1V{}V0l9sY__ZVKAq^B$NFqJmd z$L3J+;DyW{uiqQx-b7{|Hi7K}G+GCRaS9b)@@9pm%8P9V#R+}vZ}wlwr$5|+(_q_J zb(eD0eHdZ1(yb=E3S>$deL5$>HR*E}t^Z)D@gRHK&;{W25XIDHf97lgN(kNuSy zl(-D-#r8||K{L=($pSg1h7dgTy3w%kdX0%uoks-9>jyFwL^9B%4?|#(%G?^>PYbUy z-(FVH?XWzz)X9qTbfrU5w#0&BiJlPns5oZZqgfIE$0(eCcA}-YW@aRV4}G zStx6`x(GEWw0coQGMerJignDM-QyypQTndrs?%t7shi?%6#W97bF{;(JzSM6Lfa%o zC+5iU$Ra3y7I^X?<7_ZRs_i?kZnlZnI7DPqA1vhzBPbOsxfUut9(2X(8XG5=`-07v7kMB+ia3N3zh03?oC5H%JQ zY=a_QM{-onQI_CBkr$HFi2<@c>!1yxI`KQ0ro8U;LkdN16Kxy}i>jdzXPf+n^z>v; zIYQc7&}t@AHZ|_pZ|W*7a6}Vtz*;7W@5+U{Cxp9M!SgbEI+N=?%w)N&Tc76MIAGWP z74Jh$w*<6)s#Vd)!_504|CI*AQLC1`QY*ZI?uj0k@UVz4o8hc!zN<=40@A-;SNc^` S`T6eG_!p=Db^hxv(0>6>UgRzS diff --git a/test_fixtures/masp_proofs/AA4BBAF45B9610AD4D2BCBDDF61E860D9B5DF041F4645BFC329BE9A03AABFE47.bin b/test_fixtures/masp_proofs/AA4BBAF45B9610AD4D2BCBDDF61E860D9B5DF041F4645BFC329BE9A03AABFE47.bin index c368a61e3e9aebd4e2e7e35529b8a333aacbdc23..c07abaa0c29da906a5032b7fb742a1720534ff20 100644 GIT binary patch delta 1742 zcmV;<1~K{7P1Q}XJ{chJ<6zJ%`nf#QGJg2P(Ef}m=EOJ#PEA_&z0g3>(WDHMUKv0j z>Z!m5B>dF8A6e&5X%gX_MTh(y1~_XyVcqao)^8dOlfD^1Aj2%r&Hy59qMoC_*>LJ) zp~ejJN+McbXZPp974(1%&XXP*K|%8NN0T;VU^(INmwENB9)LwMUP(3D$MaFvmNx4; ze6<1&?aW?gaimrOG_yr|`bR&EidV+N?O8A%o}1pjWKYtwpc?c7ARwX@dYb2(dg2l& zDnJU!z$mih_CYgG)H6iSkh|bJ)w8r6qykVI(D(+QYhbhH<2*ys`?DY~?xXTpA}RgQ zi4gDB;cLdAB@27CVF$q#9wfA(lc=#1Dq)aBs49OX#0dp5vZCL%PQJ#L_^bHv?Uk1l z1u1Wm87YyS2$R_$K!1^>Wy4n=6|}EY!^LxGs~moj%pi=rkMCxc@%-Zkw;Te?G>@VT z2C=@1Z>zmQRurtHOnd`3U};=NW}njM-#HZfCr=Dl6YpG#$B1Alqm=-U!4o)z^Nnja zOEEA5l$HcjDNL3y5u+uQJbZ?_eqOU6a)FoQV~e|(3)TGh{C~c9(FuA|Zoqd00847| zr@!E;jxA%r6&7%ofwEP0GbF$Wu`cBRiqA#7Rzx#~vLkhq>=Y8GwWVzx=H@`z`(U=0 z62wD^r73ct*?J|7CdPf>7OPE0ZjVAe^QujC$sy7UzNCR^(MlTKLVeN*abbL<++e$| zc;%1t_yEL1Pk$)th@$%&a_Ftbs@b>}Z8}*~0l^!fNbAEkBbuXP9>{HU0}Bdt#AyBq z&tEFV1?z#Ct6Ni4f-er{^gTgE)B*Dk%^zDck54=@7xAy19mhe$v_c^NPkFY@`Iq-mTR7EFd!<957C?)GSE=C z(*89%zkl-SYpy4){WqHHwrBy7c<3cegy6A1NG@3vZ#n2z139FOZ5}#g_B;J z52amf*XSW+Jn`!A1Nx^L-%Z0B(?HsO=H91!mal+yv4iH*VoUJO!xe-+h7$|15AT8s zDcpS-9=~dbIZYRzZ9e@ccGxM!?z4OxxY40W5dhDLF=e}m!_eW~v=MSUzmP*A&mc zJb8rHh4a2TE3qpWwsGz!NHLHd`~|Co5qu_5yT1eeB>OYY&{!0t;{h(}oYNS8a@fbF z0hcoeD%g-B-=Jwjq1%+PWr0KDe>TK5eSb~DHE3QW=xUvS^o>ZZFUFc;f?k1XwUnEa zLxIc7O^q$58%UqU?mEGfL(3WP80BIt$p;D1S|`kQ?J=E4ge|=KN?@qoL!vJK!4zyK zCf&V8z6en@G9-$cbJ%I0m6Nf&qRMXJ7Muv~Mpe#B()80UI7(%)&pb0hku0LaAXvYTyE~MBvyESG*|KnM^6yf@<-A-^F&1? z8qC}8=J!+v?LUUr1~yhD%)>^zDMrMwVOkW1^tZO3r4|~6GGbOOqwZeOKDcx{`_B<| z1i-6=K8s+MU0)}7+6rrewz_;3x{a*?&?DH(@$s(|`(zDUC^ojGT0Jwi#DA{mZx^cc zX0f`&?-hn~VYDUH6BAKEBm6c^r>ZL!qCWRhMuED#^>ibXdv9OB)ucmz%1=HtW@VDM zenr+V?oEn$4yb*X2SU@goU&=PcY4=F(>O-@U6gV~7#=beNqLW88vTFFECzuf^KzqZ kDH#5_E;lNkk5rvVGo-0TX$#g#;gKYHFaDz%0F#*}Jg@Os+yDRo delta 1742 zcmV;<1~K{7P1Q}XJ{cgV{?E}TPZ=<`mv~Qb*IZR*dP6_jux6_+KLn&3TRRDpUKv0j z)Eqd~D%eg4koxpAI~-1E^iLpbi@9ooe%FpK9_@WYlfD^1AYwtL44G9N`)Te*g|^uf zG{g5U>O;Ob2fJiZ7W_0D0h1mYK|ukyBTo-VucuR)UM~IJ@ED3-PyjW!zpjUg%ReVn zmgxcu6q-v!J4aPRdYRut5A_n`pcuPb5a_U(3uOiKy=!T+pc?c7APY2#J+8trYd6Uk zkL6eeyt+pD$F!?|h>!sV+&iHKinFvGqykVv4=y|%vUq!q*SUET`~clDD1PEL+)g8a zTH;IK7!d+>wRWoqwx9Z!(v=Z;U1`I56}b19NTAh!?~XHk16b0iyZ9=#i{4(( z#)vwS87YyS2$R_$K!3KkXR4Dlf!fR2W`WJ$1OWG)6Y};E*2F1KuNsV~lw{kgtq2*E zkGFAo{l@O_#E-RQp0u?SAxt0~%GHK|zzJBPj?C4NChO+Wb1eky|8845#C|edHX9q@tKV-Biei1&Gi_Mac{EI1wN#>P|DdBlTGjkX(mw*D zo+GqZ5Iz{mP&Ey`2#KTY&_r%{Znjhf%Wasg3oRS0AC%rW7hQzzNCGd9trLx*crT>a zar=lVSFk!(a({xW8@%y9pD6YzbuBcznBZocbRD))8=-;E-h|Ir69joA872~UyJgHF zg|neoyU)_6;?$}6P50SQ?zVZd{82@@I3jFHsyBW!>yZ`M5kS}VujQcm9*xkp>vw?{ z`%4(D$Dc+NXDLosjbKHZaRPsp5;Qa?okU(*uQ7 zjl#I0tw!b8l*|x&BHoVB3*#q%a+HU?j;(nw%=YTVEUh@H0Em>OWk0W%hVDK)q2BS( z-4J1D0MqGJeq_avK<+B)`%Swr3cA#l&N==fOaiC&%0(k0p~;BBXM}^XOTcxY^@YT^ z>G(w3(0|b517sUj2A}m$@Kz55-1zaP z1eGpg3vX#n2=l3#bBB`>2Sb@IyAvWkg)BfPa9gA3Y^Ai8>B*~9sjPp21F2KtQ)3CB z-meivpvZ%PHx~U(CFvBGtNJ*&eeI43RH)ddn}3Ge`N;!d+*~96W7{vnQomA}ZyRcv zXw?P1e*elXq_D=OItVML#mj4^LJ8!hA8j>%`%N*>FVxg{t4hM?tKM(S43@Sn>ixiI9cL+uTVI z5q~rc&8H;+mG~NyXAmw)))NyQxor(G7Vx{0+z1gGUVnRxf1W!iEm7a(j|@8(a#4L8fx;ElE{o|H za*uM4bK`e8){)ClQkHURVaJ`K79|eNSgcBA2a0w;EzZ@@=d7c6>^$1<^{@9m0TK#? zXPz4d^fB10pGY&JPYDdBzwd*fE#)h@Tn7*aMXqh|R1=Aqam#qH)`2I{0IvnzkAD>^ zMSgl`(I)l2)oB_X<@W+1MYb8vV3<@ja?7ZJppXM9s{;N`HS;<{N6m2|=l!qT&^@LuYzI$jyE+iO9Am&)&;`tB;A{-1Nt1;+K~WqRs-4SFY)a kR`73a_Wen3LMO(p3%>ch*9%0Yr|VsKyxVNe29ud4JUc^SH2?qr diff --git a/test_fixtures/masp_proofs/AEA19C9B07742FF5F6D759B171396732D2EBF77728D2772EB251123DF2CEF6A1.bin b/test_fixtures/masp_proofs/AEA19C9B07742FF5F6D759B171396732D2EBF77728D2772EB251123DF2CEF6A1.bin deleted file mode 100644 index 6cba86af9a258beb76ffd299c1665469b94a2947..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9941 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 k-+gtz4f5{}*;fBwGpWsAoD0HkYa&j0`b diff --git a/test_fixtures/masp_proofs/D32DFDE8713AB8AAD01125856B8F934075535EBBEC567084302C3D9B469B83FA.bin b/test_fixtures/masp_proofs/D32DFDE8713AB8AAD01125856B8F934075535EBBEC567084302C3D9B469B83FA.bin index a5ec743370f01e7e5cc8dd093b7d35b6063bd9a8..bf78b74267f10719a7fc26e972b2ea9e753d8a37 100644 GIT binary patch delta 1675 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 U)Jo4_okR9c9{6(!0FyFbe9fd5Pyhe` delta 4170 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 UyXSx-Df=|NUUHEI0+TXde1l5+S^xk5 diff --git a/test_fixtures/masp_proofs/E3409A9853B0ECDBE4147AD52CF288D9729C50CA4AE4D1635C6D82007461517F.bin b/test_fixtures/masp_proofs/E3409A9853B0ECDBE4147AD52CF288D9729C50CA4AE4D1635C6D82007461517F.bin index d0fc8f71716cec7b9b2d9be5241cd6d81a328a7b..cc841c9c269233397daead24acb1793162fb88f0 100644 GIT binary patch delta 1002 zcmVdzTbf6TAMkG`6=RL-H zTP;r&q7~fhffIrxJ7ZD6i4{VO7L0ROF};^dR!??z6)S%aE+oZxy@7~$1d3@GC~cor2AMYKX->XTXufzg<;jwVy2s@qc}*-@6< z{)<(;rF(ztrsDa%^hHt?d-@$NBYpd4Ofj%S;ucEaJ-KjGSQY%r-VLn14d}fo#)c9a z5QO)Utq~9O73B;Qf)mweRgoeCrrx!<=gqW4|1Jk!j`(~s`>MsXK*26KuE-tTd^>TA zVs+}Z}-sSCiWFT4wV67ou`6!vX4mgt2IQIJ6@?Eh~cADsEm)a#Y>*DP!{_6t;mfhywej4WiCwYW zp-@4MEkk`er#o(B2e}py{AlYZ_!l8jWYHHX5Zru<^pETo9O+;4z0%42lEdKs#3STTIZCO3AcH{HgTLf4Cep=xGE Yjc(#}Yowp*FfVEXucpis4wK{@G!wV$D*ylh delta 1002 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 diff --git a/test_fixtures/masp_proofs/E93FF3062E6FCF83381BEE364347FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin b/test_fixtures/masp_proofs/E93FF3062E6FCF83381BEE364347FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin index 93c04891d76e7a6b714a20d57107b197b75cab89..85368bee0f91fa5319a4f9b5214cf784a68a4b20 100644 GIT binary patch delta 2810 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 uc(?>PyYC|$ Date: Mon, 8 Jan 2024 11:03:09 +0100 Subject: [PATCH 15/26] Refactors tx caching in `ShieldedContext` --- .../lib/node/ledger/shell/finalize_block.rs | 20 +- core/src/types/ibc.rs | 2 - sdk/src/masp.rs | 212 ++++-------------- ...FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin | Bin 9941 -> 9941 bytes ...C733C557AA7C18902CB851BB4DB93A834ED187.bin | Bin 7448 -> 7448 bytes ...F6B0C21274416797B6508AE6C759537F947AC8.bin | Bin 7448 -> 7448 bytes ...BAE37AFDAA28B0701ED5D313EA859FC8153343.bin | Bin 7448 -> 7448 bytes ...7C1886347E49C18C8E0F35D25956CA06390B17.bin | Bin 7448 -> 7448 bytes ...99BB39683C7B8AAB924B267369B3143A3FBF89.bin | Bin 10382 -> 10382 bytes ...ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin | Bin 7448 -> 7448 bytes ...958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin | Bin 6669 -> 6669 bytes ...1E860D9B5DF041F4645BFC329BE9A03AABFE47.bin | Bin 9941 -> 9941 bytes ...8F934075535EBBEC567084302C3D9B469B83FA.bin | Bin 9649 -> 9649 bytes ...FF5FEC42C100CEB0B19DC6C47DBFE88D42FFFC.bin | Bin 17018 -> 17018 bytes ...45FC957F905749C47816C307B4B8D580DAE5D9.bin | Bin 24494 -> 24494 bytes ...F288D9729C50CA4AE4D1635C6D82007461517F.bin | Bin 7448 -> 7448 bytes ...47FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin | Bin 15597 -> 15597 bytes ...51842690C36EA86369DC2145AED8B139748042.bin | Bin 15257 -> 15257 bytes 18 files changed, 54 insertions(+), 180 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8ce68c6ae1..6f67b1651b 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -422,7 +422,8 @@ where tx_event["hash"] ); if is_committed_fee_unshield { - tx_event["is_valid_masp_tx"] = String::new(); + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); } self.wl_storage.storage.tx_queue.push(TxInQueue { tx: wrapper.expect("Missing expected wrapper"), @@ -440,7 +441,8 @@ where address::InternalAddress::Masp, ), ) { - tx_event["is_valid_masp_tx"] = String::new(); + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); } changed_keys .extend(result.changed_keys.iter().cloned()); @@ -468,18 +470,7 @@ where .map(|ibc_event| { // Add the IBC event besides the tx_event let mut event = Event::from(ibc_event); - // Add the height for IBC event query event["height"] = height.to_string(); - if tx_event - .attributes - .contains_key("is_valid_masp_tx") - { - // Add the tx index for masp txs clients - // queries - // FIXME: review this - event["is_valid_masp_tx"] = - tx_index.to_string(); - } event }) // eth bridge events @@ -565,7 +556,8 @@ where // The fee unshield operation could still have been // committed if is_committed_fee_unshield { - tx_event["is_valid_masp_tx"] = String::new(); + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); } } else { tx_event["code"] = ResultCode::WasmRuntimeError.into(); diff --git a/core/src/types/ibc.rs b/core/src/types/ibc.rs index eb060c9f07..cfb2357fef 100644 --- a/core/src/types/ibc.rs +++ b/core/src/types/ibc.rs @@ -239,8 +239,6 @@ pub fn get_shielded_transfer( return Ok(None); } - // FIXME: I should place the is_masp_tx attribute directly on the ibc event - // not in finalize block FIXME: maybe it's not possible event .attributes .get("memo") diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index d05674ae00..092d21a69e 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -70,7 +70,6 @@ use thiserror::Error; #[cfg(feature = "testing")] use crate::error::EncodingError; use crate::error::{Error, PinnedBalanceError, QueryError}; -use crate::events::EventType; use crate::io::Io; use crate::proto::Tx; use crate::queries::Client; @@ -602,10 +601,9 @@ pub struct ShieldedContext { #[borsh(skip)] pub utils: U, /// The last indexed transaction to be processed in this context - pub last_indexed: IndexedTx, + pub last_indexed: Option, /// The commitment tree produced by scanning all transactions up to tx_pos pub tree: CommitmentTree, - // FIXME: review these positions, what do they refer to? /// Maps viewing keys to applicable note positions pub pos_map: HashMap>, /// Maps a nullifier to the note position to which it applies @@ -635,10 +633,7 @@ impl Default for ShieldedContext { fn default() -> ShieldedContext { ShieldedContext:: { utils: U::default(), - last_indexed: IndexedTx { - height: BlockHeight::first(), - index: TxIndex::default(), - }, + last_indexed: None, tree: CommitmentTree::empty(), pos_map: HashMap::default(), nf_map: HashMap::default(), @@ -725,14 +720,7 @@ impl ShieldedContext { if !unknown_keys.is_empty() { // Load all transactions accepted until this point eprintln!("FETCHING AGAIN FROM INDEX 0 BECAUSE NEW KEY"); //FIXME: remove - txs = Self::fetch_shielded_transfers( - client, - IndexedTx { - height: BlockHeight::first(), - index: TxIndex::default(), - }, - ) - .await?; + txs = Self::fetch_shielded_transfers(client, None).await?; tx_iter = txs.iter(); // Do this by constructing a shielding context only for unknown keys let mut tx_ctx = Self { @@ -774,9 +762,7 @@ impl ShieldedContext { // FIXME: remove all the unwraps, we are in the sdk here pub async fn fetch_shielded_transfers( client: &C, - // FIXME: just pass the block heigh here? I always query block anyway - // FIXME: should probably also cached only the last block height? Yes - last_indexed_tx: IndexedTx, + last_indexed_tx: Option, ) -> Result, Error> { // Query for the last produced block height @@ -786,27 +772,16 @@ impl ShieldedContext { eprintln!("ABOUT TO REQUEST PAGINATED RESULT"); //FIXME: remove eprintln!("LAST BLOCK HEIGHT: {}", last_block_height); //FIXME: remove - eprintln!("LAST INDEXED HEIGHT: {}", last_indexed_tx.height); //FIXME: remove + eprintln!("LAST INDEXED HEIGHT: {:#?}", last_indexed_tx); //FIXME: remove let mut shielded_txs = BTreeMap::new(); // Fetch all the transactions we do not have yet - // FIXME: this will actually query another time the already quiered - // previous last block height, should probably increase by one here. - // Actually, even queryin the same block again shouldn't be a problem - // cause I'll simply overwrite the entry in the map in the context - // FIXME: instead it's apparently a problem, the integratio ntests were - // failing becasue of this FIXME: the index starts from 1 so I - // need to check the last indexe height only in this case, for - // the other cases I can start from the following one - // FIXME: refator this if with methods in IndexedTx - let first_height_to_query = if last_indexed_tx.height.0 <= 1 { - 1 - } else { - last_indexed_tx.height.next_height().0 - }; + let first_height_to_query = + last_indexed_tx.map_or_else(|| 1, |last| last.height.0); + let first_idx_to_query = + last_indexed_tx.map_or_else(|| 0, |last| last.index.0 + 1); for height in first_height_to_query..=last_block_height.0 { eprintln!("IN HEIGHT {height} LOOP"); //FIXME: remove // Get the valid masp transactions at the specified height - // FIXME: review if we really need extra key for ibc events let epoch = query_epoch_at_height(client, height.into()) .await? @@ -818,11 +793,6 @@ impl ShieldedContext { })?; eprintln!("REQUESTING BLOCK AT HEIGHT: {}", height); //FIXME: remove - // Paginate the results - // FIXME: I think I'm braking here even before doing the first - // transaction, or better I'm livelocking, the ledger runs but the - // client never submits transactions FIXME: I get to - // here let txs_results = match client .block_results(height) .await @@ -832,17 +802,36 @@ impl ShieldedContext { // FIXME: imrpove this match Some(events) => events .into_iter() - .enumerate() - .filter(|(_idx, event)| { - // eprintln!("EVENT: {:#?}", event); //FIXME: remove + .filter_map(|event| { // Filter only the tx events which are valid masp txs - // FIXME: probably no need the condition on the event - // type, it's redundant - (event.kind == EventType::Accepted.to_string() - || event.kind == EventType::Applied.to_string()) - && event.attributes.iter().any(|attribute| { - &attribute.key == "is_valid_masp_tx" - }) + // and that we haven't fetched yet + let tx_index = + event.attributes.iter().find_map(|attribute| { + if attribute.key == "is_valid_masp_tx" { + Some(TxIndex( + // FIXME: ok to unwrap here? + u32::from_str(&attribute.value) + .unwrap(), + )) + } else { + None + } + }); + + match tx_index { + Some(idx) => { + if height == first_height_to_query { + if idx.0 >= first_idx_to_query { + Some((idx, event)) + } else { + None + } + } else { + Some((idx, event)) + } + } + None => None, + } }) .collect::>(), None => { @@ -852,6 +841,11 @@ impl ShieldedContext { }; eprintln!("BEFORE BLOCK"); //FIXME: remove + // Query the actual block to get the txs bytes. If we only need one + // tx it might be slightly better to query the /tx endpoint to + // reduce the amount of data sent over the network, but this is a + // minimal improvement and it's even hard to tell how many times + // we'd need a single masp tx to make this worth it let block = client .block(height as u32) .await @@ -859,25 +853,9 @@ impl ShieldedContext { .block .data; - // FIXME: but I don't get to here, I must break in between eprintln!("SIZE OF RESPONSE: {}", txs_results.len()); //FIXME: remove - // FIXME: seems like we can't find the succesful previous masp - // transaction even though it has been flagged - // FIXME: I never get here becasuse it seems the result is always - // empty - for (idx, tx_event) in &txs_results { - eprintln!("FOUND TRANSACTION"); //FIXME: remove - // FIXME: could I also receive a block height smaller than - // the cached one? - // FIXME: I think this condition is useless because in case I - // just overwrite the entry in the hashmap - // if BlockHeight(height) == last_indexed_tx.height - // && tx_index <= last_indexed_tx.index - // { - // continue; - // } - - let tx = Tx::try_from(block[*idx].as_ref()) + for (idx, tx_event) in txs_results { + let tx = Tx::try_from(block[idx.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; let tx_header = tx.header(); @@ -894,9 +872,6 @@ impl ShieldedContext { let hash = wrapper_header .unshield_section_hash .ok_or_else(|| { - // FIXME: error here - // FIXME: probably in MockNode I'm getting the - // wrong index Error::Other( "Missing expected fee unshielding section hash" .to_string(), @@ -914,8 +889,6 @@ impl ShieldedContext { .ok_or_else(|| { Error::Other("Missing masp transaction".to_string()) })?; - // FIXME: actually, do I realy need the entire transfer - // object? Probably not mayube we can remove // Transfer objects for fee unshielding are absent from // the tx because they are completely constructed in @@ -998,99 +971,15 @@ impl ShieldedContext { shielded_txs.insert( IndexedTx { height: height.into(), - index: TxIndex(*idx as u32), + index: idx, }, (epoch, transfer, masp_transaction), ); } - - // FIXME: are we already storing masp transactions when we - // submit them? In this case I need to make sure we don't - // reqrite them from here } - // FIXME: we get to here even though we haven't scanned all the blocks! eprintln!("DONE REQUESTING HEIGHT"); //FIXME: remove - // FIXME: need this thing? I think I'm already marking the tx correctly - // FIXME: ah but the issue is that this is not a Transfer object but an - // IBC PacketMsg, so I need to derialize diferently FIXME: yes - // but the ibc event is already associated with the tx so I can do this - // in the previous loop , I don0t need this! Fetch all block - // events to look for MASP over IBC transactions for height in - // u64::from(last_indexed_tx.height)..=last_block_height.0 { - // let epoch = query_epoch_at_height(client, height.into()) - // .await? - // .ok_or_else(|| { - // Error::from(QueryError::General(format!( - // "Queried height is greater than the last committed \ - // block height" - // ))) - // })?; - - // //FIXME: this has alreaady been queried before, can I reuse? - // let events = client - // .block_results(height) - // .await - // .map_err(|e| - // Error::from(QueryError::General(e.to_string())))? - // .end_block_events - // .unwrap_or_default(); - - // for (ibc_masp_event, tx_index) in - // events.iter().filter_map(|event| { - // if event - // .kind - // - // .starts_with(&EventType::Ibc(String::new()).to_string()) - // { - // event - // .attributes - // .iter() - // .find(|attribute| { - // &attribute.key == "is_valid_masp_tx" - // }) - // .map(|tx_index| (event, &tx_index.value)) - // } else { - // None - // } - // }) - // { - // // FIXME: can we parallelize stuff somewhere when - // constructing // the internal state of the - // ShieldeContext? // Masp transaction, collect it - // let shielded_transfer = ibc_masp_event - // .attributes - // .iter() - // .find(|attribute| &attribute.key == "memo") - // .map(|memo| { - // IbcShieldedTransfer::try_from(Memo::from( - // memo.value.clone(), - // )) - // }); - - // if let Some(Ok(shielded_transfer)) = shielded_transfer { - // eprintln!("FOUND IBC MASP EVENT"); //FIXME:remove - // shielded_txs.insert( - // IndexedTx { - // height: height.into(), - // index: TxIndex( - // u32::from_str(tx_index) - // .map_err(|e| - // Error::Other(e.to_string()))?, ), - // }, - // ( - // epoch, - // shielded_transfer.transfer, - // shielded_transfer.masp_tx, - // ), - // ); - // } - // } - // } - - // eprintln!("DONE REQUESTING IBC EVENTS"); //FIXME: remove - Ok(shielded_txs) } @@ -1231,7 +1120,7 @@ impl ShieldedContext { change: -tx.amount.amount().change(), }, ); - self.last_indexed = indexed_tx; + self.last_indexed = Some(indexed_tx); self.delta_map .insert(indexed_tx, (epoch, transfer_delta, transaction_delta)); @@ -1701,9 +1590,7 @@ impl ShieldedContext { Error::Other("Missing masp transaction".to_string()) })?, Err(_) => { - // FIXME: add support for pinned ibc masp txs? - // FIXME: probably need to review also how we do it in - // fewtch_shielded_transfer + // FIXME: add support for pinned ibc masp txs? Yes return Err(Error::Other("IBC Masp pinned tx".to_string())); } }; @@ -2234,7 +2121,6 @@ impl ShieldedContext { let _ = self.save().await; // Required for filtering out rejected transactions from Tendermint // responses - // FIXME: here we query the reulst of only the last block let block_results = rpc::query_results(client).await?; let mut transfers = self.get_tx_deltas().clone(); // Construct the set of addresses relevant to user's query @@ -2251,8 +2137,6 @@ impl ShieldedContext { for addr in relevant_addrs { for prop in ["transfer.source", "transfer.target"] { // Query transactions involving the current address - // FIXME: bnut it seems like here we query all transactions, not - // only those from the last block let mut tx_query = Query::eq(prop, addr.encode()); // Elaborate the query if requested by the user if let Some(token) = &query_token { diff --git a/test_fixtures/masp_proofs/28A7EA5FE79BA929443DE88963FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin b/test_fixtures/masp_proofs/28A7EA5FE79BA929443DE88963FFC7D83CE95D11AC98C8B9343E5EED170119A9.bin index 0c1c12704ed5cee1546ff20313ed698b07654f28..20238922fb3b6b7a739b7e63362f91588a2462b3 100644 GIT binary patch delta 1742 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 diff --git a/test_fixtures/masp_proofs/2E68959DFE3412D892C1EB6A83C733C557AA7C18902CB851BB4DB93A834ED187.bin b/test_fixtures/masp_proofs/2E68959DFE3412D892C1EB6A83C733C557AA7C18902CB851BB4DB93A834ED187.bin index 1208f6c0728a860e4393030e39acf503f7a70cdd..cc29bfb364aa126f802c67a6971820040cdaee7e 100644 GIT binary patch delta 1002 zcmVI0`SQGAYebR0$JFSMj*n#PX-YpmL-r;@itgUCLk;u- zAlDHi?X;}Q4KS%NgX}dv#yYV8iAEVyFZ{gRfd?jCjUB(?jaS1uVd z@;`A9P-%v=KEhIwAzVy>+A1cqYZLSWAPE00ZtdYRYty#MqBu zo3ACF@3VgvB?Eto{Ik@|5pSg`#)-)c%;5Uq8C9%N`0GKd zP0iTFsf419_)Ke#3MmC)s~&#==mU5!Eh9zL#sK;8{%86_8}ax{7v0G@_OBmy{NBf_OqpGF1b?TmX$)4-XERwG5N3MxAtV`Q)GdD_l65&n4a--|)#J8E{0Mbh|ut2^$eENY#h-b>};SY~&vojtJhIcT!80?RRKWa6VmoE_%@#g=xncH8IhH!; znWxT!uRfA9NUJfAY5q?Rl{%EKj0MB(fn3=60IG>!CSA=&clWL>T0H zt~|=uigaattKy*mBbgxU6M>ay$Dz5R^aDr_)Z%|QH3znbWNQ(UUeGsD;1|&9b(<}M zc2Ib(8ATo$YH_fxzSw_9rY1M~sm9h9;~Hr<(@aDw%x41vW5o{#CfPm_D%MW_@0BCT*KS?q`^MqYJi(R z{4IZEGWJ^mehwneQKVmbF914csL83Y0W?uiHR&Z5GA>^I+kTA)RQ7nGj#YhzElGw0 z1>wjUceBTwYQ}stHSX?SMHgR+vhs)jzrhj;->|aQm67NC$?034bDA$ZJjYY66s-$x z3__lT;AT3N8Z*!HfVf|0Y4RLk;u- zAYxMRWSqy7n;g_qZiZcV;|Px^%(@y#>c(><^YOEG?6X%8^a3ESAwr?%S%7hdwCz&| z52hQ>edd)i)pg&6x#@K}vY9`#YZLSWAl(-33($v`TJ|~yce>@ZBAG%5E%%q&<{z>6 zP3`$DMYOa89`$vd~62_!#0T+i+P7Xnzs=SO*ApM z;^*{|8v+WFo_J*!%=Ko=9my)=!^~yJZ_nto*%`vCeXED`hI6I{mX!(B;DXe=lRKbU z+TisWe<9`!`IJ`RZYkK~5V83T$#>460(3;H zvGrlqc4S}Vpe+{9-njZVJnGgg%C&Gq{X5cl{!~ln+h^LE{^RIp1-Mu7W2*>9k%519 z?}W)vAExrD9;5jsdSc`2^^L^@iRJ-IM?@3LylRmrxXw&AsFT4763vZL zQsa17w0VC`kWNkJAN!1ZF{syJv5N>*nX0xXN+!^rwkZmO%+XwDXb^L{eju<3s%;SP zuh*}3Y`le{e!LzDR9Rv@oG!GC2|OtG`}anU&hds}0cIqSmXxek-F0=?bDFdon0Wi2 z^#O{9gtV}X_-(QFq{?WU zCdPjpQpOzP_TVcI)9X;iON3jTrAY^=Dh7UxBGj2O6~KOd2tP|>v=L5nJv4yn_ zb5G_}wMeP~L~yxjYh{tOq%C=qYEZ%~<5z#CdX3uB)rA=H$NJwoGi&{=Oenpyu@I?6 z{AsoNgiR^fx?TiF6!jAtg_!-9xX8Obp$a)ISj<%=Kyms|RboLwjb)&C;nb*xchLsb z-#S$i=90MonMP-!C6^VkNkWvhXR4|9K3~8`g|o1`qo*!2ARyIh9-SlNwQyfPRVzkp ziF1O2xgPpeA<*~BtLq{lkwXS@XA;&o;SrfXtS3D}D4jIEQ|t{m496+N->TXiT$I8F Ya2hU~f5kL6S7P&ryKN|%0h8n$G&hp;SO5S3 diff --git a/test_fixtures/masp_proofs/375F008787D3797A051D288892F6B0C21274416797B6508AE6C759537F947AC8.bin b/test_fixtures/masp_proofs/375F008787D3797A051D288892F6B0C21274416797B6508AE6C759537F947AC8.bin index e4ad8cebcb0a0bc577d0c7d0ae2551c7405ae37a..ae93490ee2e9047a788655cd25a3cc18ab753594 100644 GIT binary patch delta 1002 zcmVwqEZh3Kb7{UuAK zuTw7)Poe9k5^pjV-_s5Blm>@xmRYV(A6UN#a$ZjFT#tq+BaN(JcVnY#kpg?ruOPw2W(|zAr!o5~Z;ij?Hl)=H> zOY_tW;6t86jl`pAZ_yRze5_|1KS$~!xK6!$qlnA09i!Edw{Pd+fU}Rau#)s^B@?@A zQG@!}e7S#$vo~oHNR%ZuAOWp-Bw?9bhKmskh8CVZN&|>Mw`F46DJo+lIvvc29CVPk%6k=+S6W>r0K=JpyS*+rYe7ehXf0;KTnlEY&zy4jb`z5Gphe%;NAJ(c7o?#P2w7Qan#}+&(wxz=TBotX9Y7wLOZM zk$+cP1M`5Rt^9L5Y5OfppQ(yG>GX<~Z7SI*7LYu2VW7It5F0F&@EBLx$=}E5DJ({| zfog9Af21KBu)&o)=4N@`qM5rY8#XUiI^B%0AFctTznPR!QfI1Ux8uHeH_l}8AO*CP YAZ{lsUatYwR5#JRd#!z#2$SR-G250+I_ zDEBjpL$iMtB?Etyu@F<_x~p4>n4O%#peM3VA%t-gev}b(Z>ew#L)d1(LzJ9P!yJfg zDc+JqHHJ5^5jcSRl4Me_!u%#fN3kT~87D&=9N9quM!BMdE5Mc1?6m)y&rnJo{4@2A^`>g%ClV^E8VKIUNrsfyQHg@Ba} zDn8#xQ&)@ug^`Sl1>r3c7#4JcnjbH>N=n4L^Z4h}-o8L;mM_9PAr^XSuB6YeDc{4S zvRKUouLyr5Y~FH~n@}OgD$co_;_I1QFLRn^c8?5_f-PX>7JkT{CL?2cR(#6G{0Iw;t zj8T8Gj4wRB0}mXW)7tIEx-8g|tM380DhrOIjp_8D@yPc}jqnS4pR7&nkHd0V3W~Ya zC^2M&l|8G{YH5jVs;M^44z-uKg%)FE7TPxF=OrkM!&)9vxvs?i5~6xTzw4O6c2*^l zxxU_zl7ov}0lW9}`QfIc-IDD^FPi{z$LN2t4(5up+upJl} zV~47|fC`(-FHvLf6^DXoS9)lhp$1tF7<1{4vcjPaMa9H;&hLx`om1_FG+EORP;zAA zdzo$4rM0(efpP|a$@pJX!6q5qY&d*EFgaB5XKpzKoKu<{j3d!;;zlt>Wb10_I6g-C zamPuLz6;>uS1xHkQo+{ZB%KW)#XEbh($ diff --git a/test_fixtures/masp_proofs/68DE980FCC7CC858B090D50340BAE37AFDAA28B0701ED5D313EA859FC8153343.bin b/test_fixtures/masp_proofs/68DE980FCC7CC858B090D50340BAE37AFDAA28B0701ED5D313EA859FC8153343.bin index 73f8d2584d77a8b1221f5a93e05edcdf48063f64..f195a75b9b4a3e9c1d0a6956eef21b9c7247990d 100644 GIT binary patch delta 1002 zcmV1jopCv$6+C}Lk;u- zAU|faRHip1c|d<9|~k<*Hq9DHY+e+u zU%}Jagz22T=;RnvZNxQA^}^G$YZLSWAYxVTZ4aBW5zEK-%-NAXBn5KNQL$%d#l!Z@ zLqeYfEVF+WB?EtRJ$D4={0r$Ut}JH31p`=uk)^i+mI>Xy zsr;-kO&ovIy<3^Df^5@H0a97S?3E)dan@43&X(hXSv&680D+9t00D4G?E2pD>qlAvPEMW3iG~9Nv#3MB-j% zkW`akFe2F^)lt1bB2%rjDIzj)iiIRgX`z2e;(03!>pMCq3Mphfl|U2LLB->f3Mqok zYHTm0M0^wyw#5Tzv2c%cvsuJpkx4xaduA4V1=Or_8$H^#LWUdEY#&XB$ z$nhafRFs2lnRZE1Akl-_EDG6cp^K*HC-id*q7_^TdH3#LMwT0q2BuRg@rP{uig`u^ z2%UmTUIHky*N%GI$a#EV01&f%#Upi%0V3onh`NmAr?N=@0;T+8k$^Phj~&zrok3^I Y*m_4zJMIURByfdueZSTw1e4?(G)7zCQvd(} delta 1002 zcmVCMLk;u- zAo-Fer!9|;;*;OY+o$VLDxwJEiy8g*Ed=cF5l9TY-LqE^^a3CxR@m?8TmJSuA^~Om zHa8pA$1LB~NX}ADWJZ!S#;aMgYZLSWAX`V<4x~Z%n4qy15u6Vbz(92`y%thRM^zn% z#gQ#eoU?xxB?EtfPEqf{i=Ic7vKzcDWz+)rdc^q^{7agN((*hU#TOCyf6UMaEj~VV z@pI5nWBt#z!y(JRjpuPsPY={PK|Pj^F)?d}n~ddQ+vsx4%u^`LGTk^c&r@{V(S#a} zJT)N-I_%;U90E5?(DsTuNl{(|1E}$o5sQG$TH^z!_W6I}Ig$P81|Sd|dbI*M3S2mR zkg{^De{0`RS)o?B1g7qS=z!0GvPzlE}&5@K#x%9rM8J>rDX(vRc+v6cMC z%zCW~+1EN-wCi+DsFo3$c5a2?DGah_<3a;oS-l!xp~8gD-L;)OLcQ0LG*MwZWtC&= z#3p||w};xBVyhJZZqmMWkKa9fjdYB%7*HMzD)O%kXj8ifMz*^T3qTL*i856@I8suH zBPWq(9V%!+ESAu;YLC878a`32n-`CMn=GwG9n;bg_FctG!sgb7x~+Nw_45{&@ogEe z4?33J8hOxo2QelA=z00G1L1GYUVDbrpOSy7ZlJV+K2+t%+{>ZXzB9v~Lf}0^Hhm_X z(c3&C$)&~mJQmT_G+C^V4ImFBWc0t8G}N)%*Tdn z@OK&J?_gWysX(bJ=-T{SWtx!xUVpMfw3f%LH2A`4UvpjbmbD}NQj{j`lqD}QUfD)W zBZlf?iXwtMdLqT;Z&!d7S;gYIJ_fderRhFCh4g4|ajn-RwdjXg?)rN;8;>Ukvb?`dJOgopT|HhGt+a~i> zhkrK0DmZ5MV|YAG1k^M3VtcV5K;I|%&8&#wfb~mC1L@2WtUM}OYgWwwWWN2PfedB* z`yV)Eige;lUQdkI33_{}CF=v7$Rxa3$nXxQhh005a<(GmuN%NT! zqzSKJ-JgG94T5vEwgzQ=bN2D;!dGZgqhoWSz>$C(#Mfku_<8mS+z+#i(Ma!RK4Wl; zVJ+$j>cf-c^o>>%L&Jgss8;WWuXXs2`1*^7w7x z=R`?<0Gm~G0+?iJp@HCgTn|fMG@XL9K?H-*>3))H+3+|P?e>#sQjj{6oqswPXJ4z~ zg5{GF+{2wC!OV3B_D^{+|Cvr@SDLj573j?iu{xG#y_nLOG;ZjAP1$L zd1-%tryks#c*9i zB$!(SRhoyRe=h#kyGbt|5u_8Lgm>9gM`_KfIx8{dZ9t8J{OA+`k7bNWc3pg;AfM9y z6C<`}C#kGJcS*m>~C=`OEj z56-WPqPWz1n;F`97`S(D_#3ra0kDq_l+`TUXT#DOitD2Z0tBFmeN$e4S4b+JW zv?^y^rIa^mfHS*qU7~&fIssvOV!RZGSMv9}V+trv%pUV8+z9B*lt0h8n$Gz7%!+W-In delta 1002 zcmVxo1;9E0e8wW>^^|Y3Lk;u- zAj1P2jKw8r_O4F-jRQ@86>yXON>Y4NITyWeaUoVvPqSAL^a3E!pU97|&dq$4O4yX_ z+Sdk&LNHVe!;kx0Vjf36iO;XIYZLSWAmSCCBa=v1Pc7gei^@b@bgrPUZ1W-p8ywG4 zFQ~Ujkh6alB?EtAKCVtwFhu!sT+Yxuc7Q7=+xoOR|*BkSY7v>=B;xOEmlBS-!H~)QJxu%O&!_A}a zxt?HdlPaoz;+>Sb$~nDEapMg-)Bw+)``gX^KJ|arM1PnW`0ER8)0RnC{7x3w#~m^L z%Vl;3RZ$lfd%X%#FnN}Vvj#JCkibmVDQ`ZMEHk4R>yU#;#e+TsCkLZy_t&Y4HYxrdj8S_M1jx|xg4xny<5VAv8fA>gbzmOsEb-s*3K?4MrwLGj#MFYdSF`J_hyv% z&y_oBPNdkysp(7F0@iV>;=Z0~JC|DQZz6w=vV{|7%W!0VqC~!4_b~CHQSCwAlh9~_TrF>E2IKZnXH*C{a| zE7RAS=LI1g<1hAP+l&?DoliT0MMgp%2?P+fJ`d_z4d>#|l|bLne!kxzI;k1h^jP;# zp7`Zr%lMP?KKT$Sho^yiXt1$^r6(a87W7%R?x?k>uXKmVa^#tP)DTX>21n zwV<_O^lQ#9s_Usix6{xcc2?=x#QptppU~ItEHh^>l@+Q+Jj&AJ$CEJ|DUqEBlgc7M ze}ZPAY99)+4+3hFxo2%U*8J%PRG=EzLahOuh%;a%M~C}R=5(s(5_Gtr@xGWejEV+w zk-aI}=LTA}s5cO|v!HxO-o(-23d&(US;x8w*q61W& zbCYuB?wU#%6Z}86J;&*nf#{C=S|BhN7FN#VCMI>x3a((?&Txvma&!E;-?_FkrM>I1 z`p`*rns>I`X^f{5a2uI>QaVgATNTHlAV6G!(Db81qqu`GFADFc8O}hm782=Mf9stC zJcjbV%a`U$-1b)-=_f%TNp>lFgiOs$qSr#0`Z1;#*ul;;aAB#*NCugN9;m8pYma`P zuYEfR{Y`qs0m!EHLvDxw&>DHq_?JscCf5MbSw|>w)@P{lp-frOh#i3d8YoUX!I^a2G)w65s z^HsXA3W)>+I1@1Cx7{t&XG**Uy>qdI|2rb-*RIM|2W;B};9rg9Cgu-G3Ww$(EB?Td zybeQB`GKaJNy{%5qM`Po9rx}dLSgIv9x3sk)rw~dg`kz6KJ1jr|*hD<1 zv|ns>P6>SbIVFgFyr)Sae+p$%t4YKUgI)Y4<$}V>2mkBJ!d8W+-MLT~M>hF`?Em~8!?&y8>e<5Ilsrl`HXF^Q4 z?BhFc(l}YPI65K4xZqmCsTGS&Ppc3}TFG_+&GMQ)%^m}ho}k2>n{G{@`G;BwiO9h! z`&j_k8fQYLD{G=XGpDh%XxLtN0^~`Pj$TDF=^YviPGH)!or;#M4n(tWpwx3ffnpvS zv-EY#aU*T0iGvOce-p8iJDTmMpp^S~SGk#ss%OHBe@mYnC^fy89{oSPC)tl1$g)j? zJ43`x_JjJN4((Q6Pz$RvrSGS&PYZ97Se?WQ zH?*W}1pJnEf85)2oZ7+5McGC_P}53+cEa8N8pdRXRBwr}=H&TS*uN=T=M@WI!+tcAPU1i*ozt%;vM;LL9 X?92<3UMMJ&UMLg@1^@s600000-PmYc delta 1781 zcmVFbkbzLBS~f%-gLCSkSl;342h9i3diPy!S|A~lj~hTB zP*%A5ld1hbhVuv!~qwiJ@UKY@@-ThFV1Jv9|It5IGu!^h&HZ3Tha)&%(@II;k1h^jP;# zp7`Zr%lMP;41jx5(sdJu*AO^J~(M1?l|E2@WehqP7N@APsEvs(2lu328(P8$V3IN8@=LRdmMy*fUsJmBrq?0ilDUqEBlgc7M ze~EHh5r}mAo+XDwtr^dpi-Ay*MvsUQ)yk#4>lx~EudCsyL(d-r_H{bUTT1~?B9tuL zLUF5@3WcGp!&wmSwHo3kDcY%Nv`phB=vB!5m%es+pO|Xo>so!;KOvDj#}KZnUiA^4 zWytvsNhUxz0io!qE6=%;9H33ksa*oif3pthXV7l01LHariqM}&r#G}m6&~BM&|``E zBoRzd&sEFWxknWra*Jl#(9DVYzDIXa|M9&?n8Sol?;=~)e_mZn zR3>2ER6B;eb@I^XkhkZQ*g6qFOLxwSY)^pa@pwxCFqDU9<$lyZ34nmXEP4+o)uG-O z#@kVRm+{CwowB1}f1GwFx*cE32yGY9R&K<->28u?Uc9$W=lX26DRhqX z2yFca>nl%IUJf$qRv(-KZ_FkJo}k>szut7UY!L()lwb@2ne~0RtR#kpbXNcegA=nY zvsSU4Fb$ePM<(Xs$IKbGcT})Q?y-I2KKsTI3&rj&Mp@GBO7c{H#GHK1e{TEa0*Nhq z)u?z}2<*h36N#$iz||!zxn~V9`MnT%RjR;B_IN3f* z-U%d{g!5_+k%mI%0QvC8f8W1M*WJJXF3q3w@uEOp-O4p!fjWotS`aOBAa3wubd^<{ zvNCPv&_Xl}(%S?P_fP8okjmMeb>u!6L-e~PB(NJ=(AO$No@ zzU)U2qGxVFYq8;`8A?sA zS(z6g>*q`PS`;;ae|lMaC`A{3h0F0DJ}rg{TVw;4uJsDa-pysI`t{@)ldp3wa5-)w znxA6H6)GHfyb_t|+g(Koxmu${mBKvr?^{M~I1!z@IzRZK^FGBPn@R&W;M#~cy0}_~ zT)h=K({sZ~&80Nb0oM62@4!#Ns3b7U={ychuM?lAQxAW)f2~#MuKpuHeKQ=9Tgn5~ z{YfE|t4)iEj-{ap-@9t-rMvsZ@zAUgDa`2afB3*YkT+4&41=>3c#mhGWl;DV^X3nA$vC%*zNc zUGS-_-fs6IAW&aSUNC=1QYe<6p8(EyR(E!?8i|F3IORr1zkn=C7Q@LJ4c(8>Yw{_- z`h<#pC_Ud3u0o$?C8%zE@~v#whiNJonWTd8!H(s-O7nIGwqr|IzDU0Y;*H#$TL3@o zCQ2n~Xp&+R0A$EVJT}1&K_E3btZlW0=ItGo_-L=g=$iA2(p*N~t2OmH9lxGUal2S` XM1l>IUMMJ&UMLg@1^@s600000VwZJ- diff --git a/test_fixtures/masp_proofs/94DF56F3CCC7E6F588F0CB82C2ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin b/test_fixtures/masp_proofs/94DF56F3CCC7E6F588F0CB82C2ABE40E759AD9C4F4F6475A419BFD56CE76BA68.bin index 80a445cbbc84668564e90944cf69385858b57bc9..d204b2c3dca5175e0e458570ac0e6957fe470912 100644 GIT binary patch delta 1002 zcmVf;Y-V}5Fe;?L+lKX;uEyH4Lk;u- zAhWJe4wMD=!%wnInseoL_jm;!1eVzOsJB0PK;dKaaI;qs^a3Er0;T%`=#VGBQ!t{wo## zX`C4RN3(wxB?EtrjP9gvi;QG4_jy5&GZSM`mO+Y7BtzZ%kr{sYtnX}exh6jpCgU8l z;&g}BjB4GWu!RCGTN2l9I{0%YY2bT=Je6uHM6C!N-pvL@_xLH`eLcu(7?3auHVJyK z!Aw0D-F|IwtPn`yd##mloiO?7sZr| zuT1N=DqtNDHHiA-kAAW|L??<0^0XgS9ut_#I=4i9V#lI2iDAFfyhaeH;z}!AgCzA1 zF7$7l&DZSIe`p*X!+N%7jZ8h=1ObT-GV6dJddl8_XG_>MyF90p4liS55P3a$)K2C{T{*>92r8q_jV6~KgRBJ z3s4FXoY{u*wHa(q9)^l~rq+(*AI)y&c`*>LT4Vr{4tOULYpw$hAf9PIbr8dOlkrX> z6xZH!B>NkoOE}S|l%{RX+poyDTaTWo7;HQx6=Q!$*g8*7On5-|()Ms0 zTN4`ifJIi~(8{p_JFpT6{S;Ih4sY=;EI|xg<>6`?tg&s599jB57W9%s`8)LWr<{p|!}T*btA)$)sgino z$8uPMXK?K$ox6IJ@uTW`yiv`E-a{yv(Fgj5cWQxnx3MY+9nP2+A-iqm-$3FOr^b)2 zRS&3GZ8#`Bpi$&1!U$M1L#&5QkL20gELz;p0 zIRLA0Qx)3tsf(V^S_M23klY5ofj41PQ8Z~Zm#({ZGG$59C{Fx4Ns%!Tx2{bY0DU1%YLk;u- zAZl_6x-sAQ1`_){dsHYKC)g7EQG{N!T*Q^+`YZLSWAhsne;{*3gR(**!j-IxyAcV7Ed^~PqqBJ<$ z=ef|*fwO-WB?Et|Eo|mBwWv`GFG(gwuW=oNmsuPoiL&$MChFa1k&tPlJ=<5;QWSgN z3J)jD@}bb6SX-|Z5Mi=ITX|Y(#w<8)A0?CEnpEqTCf46WVzn6x4l~xoF2aexhR98e zbkU3n7X*Y+q7jl2j6kwrKXXvzFnp|k1Fd*_b>~as{Uv|(M#hpcRW}qNF3Ii{42=^c zvOq}dBOnzRNmy^t1m<&z)XWvDhfDmpsc;Tc*zxTg-5oPF-Rymfl2sM)gLBV=iGT9Y z?Ux3z-=Ujx1MbvVmo5VBZL->Se{83a1_z=m2%P$~zrkz)bJ9c{+cOnd#w|%2FIeVssbk#>iWxN7dupAy%r95IpdE^>STuK?4ac3l#dHAyT1p zCRK+OwGk?GcDMW8o3q)+MpLSjT6Qp|@?5LNfahx8T}Rd-%ZI(3Ojl2TaF^1ODmZfU z*{V~Xz5M#96xFee`pF)^I#c=(xf|qWQTWhm$47q(z8#@6Jn~0G;Q8$5;Nn1PpI`S3 zK$j^VXR0xg%s?&w?3Op%)uK-Yy`S29duuF-Gn9}SQ8raQt65L+Aci2`w2z0B>6^V8 zP@fAVmtO6n1B8P;?qjV)k^_DA><<#>NB7(J&2+H(0_B9;b6* z603+Uo{w}=*`v16Rhx8GdgZu=C7#~cT2GQd3jPB|h>9KaV<8R#h|1O=Wcg$yr%g9! zB@tw8<FtaFnD+ts0;A$p%R(YNu53&Ol+Sp}0*kJD7V>JnTgjeSg?v7o&8K*KZ)vGS zws^ryXXVOpvf>{b6h(@z`Q*VDnR9b)FV?}5+Ut@@z6#p_uCtMoT?5$RDg#IX=DbGY zgf}wO*5PcKk+UH$;CXecYfQMxM*fu4t9~Lawkb8r;0njgX0@i`1TsgttK|`l2HZw* YLDnMj&V0!=Bx}W?UZHF+1C!(&G!2;RAOHXW diff --git a/test_fixtures/masp_proofs/A2385FC511BDA00B7E8605BF05958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin b/test_fixtures/masp_proofs/A2385FC511BDA00B7E8605BF05958D4A1929918AB0A43EEAE93AE8BBC515E18C.bin index 0be8a6ac33a32da71a379595777d94e7f2718788..26768ebcf17521006d855313db3a76d4ac7a49b0 100644 GIT binary patch delta 1214 zcmV;v1VQ_aG>tT{<_;iF>{-rc(N>7V6ln#}-8@<5N9z1yxj_oB6Z}Unb=1w11`j|W ziGiDO2J1Q+RS9KxddR(~23R=HrLZa2K+FoWcxJ(IlV%S>AfbrhAkBmdPomjo?-h;} zxj+))c#(=%MoH`l<~|#|IJ3wP^a3E}U`Dn(TTtIjRTukh3GpeQ6rGKI0P`&2>MP@! zHBdvd+!CY$P%z;-jZ{bwIZfp1o)DH(LxizASN(Rbq|}i{R-SK5VI%t0um3`Cld~8Z zE;i8h@BpYKY~y0&f=yFWu~p1?CDOiM33q!Dt1F{AV~%-mM3c}JQGbQLjB2&AQ%{+c zN%Jple1rqe=sfZqx9mVnMj|_T^NU3;p?{S7?5TG9daz&;X}E+U;|Qhkc`dPeJK5=Y zjQ)6jPC)Abg0V}bT^{C1Zu-+ny?wFl-P_+6TiTqPg#!jkvDsh{yR7w=`c@oD!h z-1N|=^FZi@iKF&TRPN*myV;L*@h0w2zdVgV!_wR~ytD9t&Y%_RznWmdh=r%nC zP2M+sgDeak>?`pPg3bh0F$RlJL64@eDn7J3XXe5kD4)86*MA~UqPY;M9ZCiI!BrEO zPX!gb@J)vZNHi(rF+LYPIhE2&14%*oJ#uo^sD3Bqg)l&Nm4n}K#$697N4$-pQyv*% zlxUVW&GKHW!lg(jfQ+mNmV;JElfSXg^@oCyHK@+iIJDcC<9EaDCUeV}a9zXQJmyf_ zK>=pnNdC=8aDP~r#IHDhCAsn1EjEt}|E_cuVNb#x8a`AJZ)Wh`({cMM+zL|;H$1aU zTbj^79I+Ux5%^Yc#PiZuOR1h>jSHEz2(8FLUlA$+Q&k1oTs0=WzdLhzWI-pmy+pU8 zi59(`(_-)uB+(I`#NfYF=8o1Ci@uZmwxg?3tS|>8fPZo{m<5=Cy4(*J>xiEpzt0{v zJvIHicqv10{x0MV^n3xm!C*vRUTU$tym}asfFg{Jr>LaMh74g9PsJ7SY~|C59<@F0 z2kUYnQ$CGVF|`f8)wJye7@ou)O#jUf@IC+$fKV0z3N#T>_W;pfEYFWfEN~Rfhkz%4 z!5oi3gySdAR1095~&)*c@H#c=hQgqI2Wy!O^spFM}iKC?H~2?d9n zGa`MEQ+LKR_716ke1u9gm>W{CkO_AllLyPh0)JdnK0B{qk?(W(Y6=n1&oqy>-C?|e z#*;Z9px#7H*BMVGk+`(28SX%nMNZyC*0X-IOCR3wxDXE=y;ap_?qUY|>ARVk3nI#S z*~C%K+`yxZWH}c;f)XtBnPYu}YrIV?@>{Y_T)!+r4lIPqUQMV(f>qnOa!AC1WX=Rw za7W0ef46+yf%vn9j}Mxv39FwweUSuNwRsIxKhB|=R6>`6Zl^+C8d58#ZszF}Yi9M@ cE(y1!jx4&?yZas#cSFZr{&prS0F&VvEI`FbO#lD@ delta 1214 zcmV;v1VQ_aG>tT{<_;jmG^SeHxlf-6kLLeA8YxbF+4?u{u)RHmROu<6cUWeV1`j|W zjpe|>YPV&(fi)~aLqTvw^g>E*DwF&-R-gAq5eAa~H}nJKERr?Us)VB{v3 zhAJmKNYw9EbYk;OGgu-CS+mFw^a3Czq%R|PVr3_^!_g88`FNYS7^J8s_s0N&hlbBR z2hXIl+!CY$P!M799$O2Btqr}`KTv{*k%uW7EOh()DJ19IdYWoWL$#jxG(hDfu?Hpq zHq~Wy+C?-xGd#yy8bx}$_3J^OESGNE(r<1D<~ywyo6o1BXp_(tQGbm|?{ift~J;k;*J98wE3s>=2PNd%d8 zqt21;P5p~5h?0xUW=mw)TL5lH7$?5(3Cq`q-Ip$ya$qyB$ChcA3UF`6?Q^a9y79@E z8=08yTDUzx-7oE|sV~nen}zG;jPz)&dEh)D*(_Z`V1C&3J%9dBoSfPDPYwjlF+mQ< z_int3Ea7VzCz<^~f^tw8dS01N7aGUwg`n-kfysk(b`jR2;3mIX(L-sk0+DZr^${leqketxW9>El~um51%%D-DrNdkIlmWv9=Zj`q1$yo7oQPqey>@@ z^*+*m83snZ^7Ya80^O!ue|@`NdQev9jRAwBlyx}^{C{hW(*TQw`8Ze9I1z1x)>zra zfG(8d=f1&2`>A6qs_IpL10Rdt9^8b$mu2RV|75+QqP4EcRn}Ta5wpaspg~AHqx_vKQex%*9OTb@UNYF(3-Vcn>6hw^4e87nVxb1Qd3Z^`IMW zB(6E`ZGSrD2#X$LNL0-Ss>i`87UMd2u!YHjqV0h9ncXyOreYUEh3TZSDFdJ5e6p0W zH=br7S4`xUJ8JS7_$xe_wcsa9Q9h%KECzbDtTP9xAH)4mM2mO*5*yl&@#EubYE_mI z&CWn;TEhGDM)!IADr~K4$+(!f9X)eoq9u1Voqq$}sL5yxD^C3(I2oa3G(s2?1)l3m zVq-SpNt(c0gSkj#u>JJt84cP6EdG{VdFKbnT1CB!0A`FE7y9Vcf>T=~Wtc3*BV~uG z-MJgOXvw%o$D00As$5t)EziZ0X+Up*WMff;k{Uvq4&t@UJbq_XH-R@TQSQw&(D5HL z=0}t9-poLS=_X)mp)Vx>Wi~=wrX{H|nxxS~;E+kDK6CnMG!^-PW@@qS!vg#1QXAiN cw5mnYOvLIw(@A{hNF1gQ&A&N<1C!wyERhyYdH?_b diff --git a/test_fixtures/masp_proofs/AA4BBAF45B9610AD4D2BCBDDF61E860D9B5DF041F4645BFC329BE9A03AABFE47.bin b/test_fixtures/masp_proofs/AA4BBAF45B9610AD4D2BCBDDF61E860D9B5DF041F4645BFC329BE9A03AABFE47.bin index c07abaa0c29da906a5032b7fb742a1720534ff20..8b075f953da4f6252adf0bd632e6a2652a824087 100644 GIT binary patch delta 1742 zcmV;<1~K{7P1Q}XJ{cevt;>6B-rIEeT0-Rr_xy;QDg79qibG)kBUp`NrQp7R9ZCA;! zdm%ZPyYsw(jCn#Ft49xe8j~IxK|uup0{b~)GVf&uociAg9Wr!Pk69jTr_a*=tljy)51Ecx%I;a45n*uamhBbpc?c7AZF4b){ask!K@An zQkv6)KqBuAz&1sfWp0nz##RMbWV5s#qykVsR-HxGrDnRRULv1l#iFK@pPEXD5lD=B z1t%^`97k{Fk4_tb2{B5Hw3f^iDB7_>CCB*Vxp_0**K$O9E&j$6O&ZY&3wG=L|LI1B zGHP{_87YyS2$R_$K!1jXLN3Ica@nc7J&M=31I|IE(>tf2Ku~SH4S(Mk;(m zLO^@8pwg6I-`Jxt3#GY9FksD3R@@j3ubya@l}2*iibshFnH*5F1tz+ZUq(b`pKwCb zIUf)0s5lMK?%aq=4!jf(xuw}0SQNVFDyldukmDSDe^p}o5r5o)Vy}DjDAfy>)GsqR zi2($%^I@e{0qn&6)(U=>zuP*$uHP*^F3)eX&MAIu8^WTE@udzEFjz%O_DmM2=_Yfo z%7IW^y|l%4Wf3sbtBaXe}E0~qtCSl3m8 z(bw+-zlrgK7k`AiR?Zxh{;9@1flPNKNk|vT!ir;F%u=nfTBqc3BAr^>6iRMaF2O6+ z_KR^O!HFgt)D#QzaLee?+X@6V%D<4Aso&R4gnL|3T}=uE@LtNWtv0WT_*0VvyigRc z37<>q(~G~k=xgN5>+_}`Yf7llVjt<`D#wM}J?{2Tm47o?)uP8jb%n4Qg~tEW!^A6e zExdRTYYSIR+VEjy4$*+td(KXu5V{V+^b;X4aNPnUJ(fWpM~D%nqF*zpMO$*<;{kE) ze^pu#x8FYTAO=944g#LiAjg{iBYu$LdC6D_dj>3aeVxJYAo|pmH{8Eojm5KDsw{eP>0QwWq_-R1Ldpj(>@6;^god#{lY(VdP=kd@AXM(mZSWM0gj&_hM~jCPn% zZMJUGP&qWGeu)JE=|D-K_!dI9U^lUCCHBXiaPBkK=tfK%X{su=ss~0tUlNYO`v5!p zmM!u`InS(aMMY@%kxabBYvL-Wv3a$AzVBkyQH<`mb5JD>r+dhC2s z#{HKx9>Hqm-9s7o4s0CTvW9mobt?V*<^hgeK=3(F!RYs*x2g!)dHgi4=#H0%lcgC4 zQhx{t8#L_$nEz5h=g8N_y=x-@K`C34fz<+G`9`F5m0kd!k|?EWS^lzkVLR3Hi3AC+ z<{=PikEY-yjUUM(^a8ZaU*hbrZfpPAffSD}yWhhHOYdKETeE~WPJEHFdQjF3( z?qUfIj7RA_N=7}B+~pyzsBHnL(1#f?93rzHPg1;*B_jxAi&qF23KiwJVNDZrHFksp_cd;3xUYF-1RX)#h_`jiKETZEvHQN$!+x_}|) zR?j*EvfLksVUCyx^MHAU-U*Z-a*f2(bd3I3?)}{F$zPV%8aLf-5@`)xc(`z{d_+-+ zH%UI<*7iMe*2acnz(9h22VN4Ho_{)75}bBZ$iHKa#$$}3CB(SAUd&73ZDZ`ie#%|y z!Bw9CWym)K=QXQABPjN`kdfzbh!l5X^~*)17E+nn(Od4uwF!3L#tfU)n8B};~yuY|0ZgaxU<;L{Z(rt_>ign6!5fg$Nk^8TpebuaQfS-({oAY kxZ0O>s{tMxSczty>Zp;OoeKyO%=<#}^G6984U?HBJfnP2I{*Lx delta 1742 zcmV;<1~K{7P1Q}XJ{chJ<6zJ%`nf#QGJg2P(Ef}m=EOJ#PEA_&z0g3>(WDHMUKv0j z>Z!m5B>dF8A6e&5X%gX_MTh(y1~_XyVcqao)^8dOlfD^1Aj2%r&Hy59qMoC_*>LJ) zp~ejJN+McbXZPp974(1%&XXP*K|%8NN0T;VU^(INmwENB9)LwMUP(3D$MaFvmNx4; ze6<1&?aW?gaimrOG_yr|`bR&EidV+N?O8A%o}1pjWKYtwpc?c7ARwX@dYb2(dg2l& zDnJU!z$mih_CYgG)H6iSkh|bJ)w8r6qykVI(D(+QYhbhH<2*ys`?DY~?xXTpA}RgQ zi4gDB;cLdAB@27CVF$q#9wfA(lc=#1Dq)aBs49OX#0dp5vZCL%PQJ#L_^bHv?Uk1l z1u1Wm87YyS2$R_$K!1^>Wy4n=6|}EY!^LxGs~moj%pi=rkMCxc@%-Zkw;Te?G>@VT z2C=@1Z>zmQRurtHOnd`3U};=NW}njM-#HZfCr=Dl6YpG#$B1Alqm=-U!4o)z^Nnja zOEEA5l$HcjDNL3y5u+uQJbZ?_eqOU6a)FoQV~e|(3)TGh{C~c9(FuA|Zoqd00847| zr@!E;jxA%r6&7%ofwEP0GbF$Wu`cBRiqA#7Rzx#~vLkhq>=Y8GwWVzx=H@`z`(U=0 z62wD^r73ct*?J|7CdPf>7OPE0ZjVAe^QujC$sy7UzNCR^(MlTKLVeN*abbL<++e$| zc;%1t_yEL1Pk$)th@$%&a_Ftbs@b>}Z8}*~0l^!fNbAEkBbuXP9>{HU0}Bdt#AyBq z&tEFV1?z#Ct6Ni4f-er{^gTgE)B*Dk%^zDck54=@7xAy19mhe$v_c^NPkFY@`Iq-mTR7EFd!<957C?)GSE=C z(*89%zkl-SYpy4){WqHHwrBy7c<3cegy6A1NG@3vZ#n2z139FOZ5}#g_B;J z52amf*XSW+Jn`!A1Nx^L-%Z0B(?HsO=H91!mal+yv4iH*VoUJO!xe-+h7$|15AT8s zDcpS-9=~dbIZYRzZ9e@ccGxM!?z4OxxY40W5dhDLF=e}m!_eW~v=MSUzmP*A&mc zJb8rHh4a2TE3qpWwsGz!NHLHd`~|Co5qu_5yT1eeB>OYY&{!0t;{h(}oYNS8a@fbF z0hcoeD%g-B-=Jwjq1%+PWr0KDe>TK5eSb~DHE3QW=xUvS^o>ZZFUFc;f?k1XwUnEa zLxIc7O^q$58%UqU?mEGfL(3WP80BIt$p;D1S|`kQ?J=E4ge|=KN?@qoL!vJK!4zyK zCf&V8z6en@G9-$cbJ%I0m6Nf&qRMXJ7Muv~Mpe#B()80UI7(%)&pb0hku0LaAXvYTyE~MBvyESG*|KnM^6yf@<-A-^F&1? z8qC}8=J!+v?LUUr1~yhD%)>^zDMrMwVOkW1^tZO3r4|~6GGbOOqwZeOKDcx{`_B<| z1i-6=K8s+MU0)}7+6rrewz_;3x{a*?&?DH(@$s(|`(zDUC^ojGT0Jwi#DA{mZx^cc zX0f`&?-hn~VYDUH6BAKEBm6c^r>ZL!qCWRhMuED#^>ibXdv9OB)ucmz%1=HtW@VDM zenr+V?oEn$4yb*X2SU@goU&=PcY4=F(>O-@U6gV~7#=beNqLW88vTFFECzuf^KzqZ kDH#5_E;lNkk5rvVGo-0TX$#g#;gKYHFaDz%0F#*}Jg@Os+yDRo diff --git a/test_fixtures/masp_proofs/D32DFDE8713AB8AAD01125856B8F934075535EBBEC567084302C3D9B469B83FA.bin b/test_fixtures/masp_proofs/D32DFDE8713AB8AAD01125856B8F934075535EBBEC567084302C3D9B469B83FA.bin index bf78b74267f10719a7fc26e972b2ea9e753d8a37..1d3b5eb28ee61ef710b49e744a2137105053902f 100644 GIT binary patch delta 1676 zcmV;726OqbOR-C^niwGC1Uu7OGvrSaYcn}t@Ht3p!_hJ$j0@egfkK@FFGLp?#`WtO4vN_n-AKlGr+$kAxt$o32AblNuR8VJ?WIT1}Wpk0-OScja6v zuXSpeS9q+X33|Y;O{u>eSFz1P!anLaKtRhLU9kj#)YA(H!18BZNi6iu(2Q*mQv&c7 zG3}fg$FIm4+jPX|NGjh+5gs2o2>_FUb)JeXV7;@v8KeS#`DHmJ@(SzFg}Aav&}0Y$ zbX2m|@3UZzsxnbBK+zv|aP^U*s>oqXt6mfsN(1%=FPL8hRK(+!awJwBIV_FEKnlUS z**QG4Uq%A$?8NHVY-X+1i(SgI3M3%g@eQzdd#Zs77fd5^v_yf7JzBP&_9o&PopO{L zBU5$`9Pp*H-yBv0f2Z(L6E>MN{ad+#`J%#6j)Cbej)==_(ZmAyyMAfKAFdS@y+#^Z zL`Da?H9_|vtdAWrBjUP5^+tYF00Gz=h!kh9Vr^-n9h_N@ZlW6ftdIJ6Jfxwh^af5~00W@K6c$E4TkOQgX2 zB5nT}mtrHCk(HwXFD+N;|OiU%5xk*rHBLzQw%XJEI8?#K|} z&5r|Hz#8G;e?{Ni2|-Ub(}}B?OJ&)F<%}KT38Fw>J7NQM&Et1>V&4M`V=*Z4qoV1- z#`prGCLnad5cD%$@px>L5@#{+X|IM9%>9|Hbo9x*depJ^^kib0@il^;G@#j!8&r4; zXBfi0t8+{{9`P@4{vxj6r^txZ?&t6QpWwZ4AsebYe~KEq)z{9yj()g%q0*9C?ibM_ zAv~(NHf&eL`*L}glL3wtlucLo&v$M4ROXLE7UZ5m#Ck0f(RaRxo~}sEQ9W`C;TEE} zAAMYUJ;2>DvDONSZBsFNilVORk>Z9<_dc~z)OVt~aqNO6^wHeMHsZ@uCqCiA}X44~8i+s8DFkn;$9$d^ps=Qj$;Tt}*KcK!q9y}Z1SK?a>Nx2=2fljW!^ z6Y6DCc&z0Q4#PRt`r~AXw~DpBXxQkz%Aub#OGi2vn&P_tbHW zY>vk@Qrqj0pnxIsM!N)1wnx@=dM!C^UIr;0v10-$(CJ!+!S*USFovaB|4^VPj&((0 ze+?w8)zJM)?!yj`i5-{k<5Wf7$Q{>bM=UI5T%UD}o(II2%t1*Hh!7`e9JnR!q2OJg zG}OVh2+Odp*(@!M1(4sLNaWm>}k4&*7`1J_2%y zZxu4I;l?_zlA(eJkmESUZb`ke_*AM&f3|P>bie zY5D5x*3Pu*#AkIN1m<1zrIs=Gao^o%|2$(g=MI}es31n>x^Lio-&Zn)i zzbyQE%~!P>ap-Cf2qe&r04btOE};5lCa delta 1676 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 P*K3H3r%V3=lNLf`gYx@Q delta 3073 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 delta 4170 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 U)Jo4_okR9c9{6(!0FyFbe9fd5Pyhe` diff --git a/test_fixtures/masp_proofs/E3409A9853B0ECDBE4147AD52CF288D9729C50CA4AE4D1635C6D82007461517F.bin b/test_fixtures/masp_proofs/E3409A9853B0ECDBE4147AD52CF288D9729C50CA4AE4D1635C6D82007461517F.bin index cc841c9c269233397daead24acb1793162fb88f0..39c393b36086668230517b8aa60a71b31f13cfed 100644 GIT binary patch delta 1002 zcmVV*_3859fyo-Zr2Lk;u- zAp04~fFG3`u0&zs>MF?2VIeoXEJyR!M9f~F(jk7bII~v}^a3C-P+>$(yFk(y8WOo- z7KY!v^rU?+m?6gfc)0*i;gCqPYZLSWAS}M4&`U8oa)t6iT}4oz^2V@ZLfB^JGc^pR z0*P!)*|UEZB?EttIDA{=De~B@Qm(;2JFfi8!b&gBiM`$^9!e2E1tzmXYMcaU#4OhX zxxJtMm9|HbmM-3-K*B;vtErr2H&uU!J|tiq-Jh+or2n)83VKMh%d*l>8;J|Hs5nuW zHOFfbU%3&Yz_^SN8WAl6jCpwRhG|2ETuX5XAg1+QE24FG4cGclgK{ zrp=dLwzA0lnf*-v{hsy&_zCwO_zF)4%pw{~95bR(%k}(w2bm_piZ>#`EJ)PDvmR9n zT!jNWvx|cc;BTz#f%O z0W7gh#fn#*w56YQsee~!O*D&Q`UJq3!3IT}X3KxaaoDZa`_Ao$l56*285G^D$c@om zxTdMrFHr$xE?-e9+W<3kGX)Frqpdfy;-cT$Txy{h%nNwbtS;&mu@mP5vBtwLB3^dV z+fTLx=HR;uE4~N1cm{Tp2B%?vKI2BHFscGL%e$3|sR88oiXrPWW~vZzs{*fWb8sZ= z8Sj7Ey}{LBSq43r!C|Vsvv?zx=Z+hhqD0l_dBWuN;eN2e9o=H~nvLbr#0ZU)F!D`58&Yp8<;2Mx>G5WI9DR7h7 zy7TjX#6<$^>i&u)2Wn$8)OBRan%S#U`Bh?J4ie%jR42ojbsuvB>~76H+8H!dPB!;G zT|c2!8bht-t{N$o9$T)%F8L|41V8y~tF;7;C~n8#To|ZhHsHzVt;Ybq)Qk?8c_K#k zy2b}U&tIL3#obLwcNX9o%FB6P>h-lA&Qe1>L&XdzTbf6TAMkG`6=RL-H zTP;r&q7~fhffIrxJ7ZD6i4{VO7L0ROF};^dR!??z6)S%aE+oZxy@7~$1d3@GC~cor2AMYKX->XTXufzg<;jwVy2s@qc}*-@6< z{)<(;rF(ztrsDa%^hHt?d-@$NBYpd4Ofj%S;ucEaJ-KjGSQY%r-VLn14d}fo#)c9a z5QO)Utq~9O73B;Qf)mweRgoeCrrx!<=gqW4|1Jk!j`(~s`>MsXK*26KuE-tTd^>TA zVs+}Z}-sSCiWFT4wV67ou`6!vX4mgt2IQIJ6@?Eh~cADsEm)a#Y>*DP!{_6t;mfhywej4WiCwYW zp-@4MEkk`er#o(B2e}py{AlYZ_!l8jWYHHX5Zru<^pETo9O+;4z0%42lEdKs#3STTIZCO3AcH{HgTLf4Cep=xGE Yjc(#}Yowp*FfVEXucpis4wK{@G!wV$D*ylh diff --git a/test_fixtures/masp_proofs/E93FF3062E6FCF83381BEE364347FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin b/test_fixtures/masp_proofs/E93FF3062E6FCF83381BEE364347FA3E6D650FE0B00B7DF477B409EEADFE64C8.bin index 85368bee0f91fa5319a4f9b5214cf784a68a4b20..e778d2e75947d329bf12e26465696e9fc1b504cf 100644 GIT binary patch delta 2809 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 Dn?N<2 delta 2749 zcmV;u3PSancbRvv79$`V<&e=VuJ#7XSwNIX@4ok^-qT?*-9o_2{@C@!g<+zTHX}eF zgPiSnF^xxqPop6a7f+wsq0)}2%}P5?3gdRcAO+_-la?bvVYHlJ3QRl-?z5twvBR&ItQOt>AKf!U|-q9a3hHuDUX2^4Xeg% zwCLW;hUW6E%%P-;&uAMabSM=31aiocnjC_OUBjgcEIQvcTqc@CtcKU#Xx^)DLlW^x zvWWq#F_~$zTPKwQe+r?yvn@Wb*OOSgn#`D46N+!5kq-#s8w6vu`Su0)Nuf+t|eQ05^`z zj8MFcS1l~Q%THN2i8Loxy(z!);&QWoT|`vJXkhVL19Ini9ff>WD)h4Pdx#_PPK(N= zsYG500_-BFHDVfxvSizL2Kb#S`0FQQRHQe{tZYCB1s&tFoyz^A$o!b@|D3U6z{C(V z<9CA_?R722-<`1PTeE{Ll>&eM4h<2uSej*=hHyBsf(7HBvOCz%D5yWTnRep1ev^C; zBuRYwjz|@k?9oGP8EcvYAC(b8_U4yr`k*ghkVz+rXy5A{O3YU-lmD2{SB-p<>PP3i z%S@m;mBS|h4T{~qbd%IlUur*1{PUo37k|!o;bp*m2*^Z`suj-VhWfLYFqHyP>0DF4 zU*{m2ohH9Kv}6CSY(=UPkV4ayI|Z9xh+m}YuqGQX7IhD-7SybIq6(h*r60ZyI3%FO z?~Pd8Nye}vj7x@Pq8#4JG5kXx=VMWERYrXx=uyX2rX4Funez8M zBkNKstNJnGVlrE`=2Y{=(zsu;m&F)RcM!%ldurm6<|7#{{mw~5yFZ!Ed3#7TyjH5t zgthcce{b0B|Ce8~7ZpbxE0{-MU1Ws>2f;MRb1|ZHdxrR|8pdhJYV1C%SMs~^+3kVh z`JGZ4Wj3BU{)VSy>Y)T~PZ0Yp&cM@+GN(eEKt)Ls3eZO%9P{CV*QMA}3ey}B)w17Ul~ z!D5g`$HI8FZk3~%ygGr!z8p&){s=3cAwH$V ziy}yYsQy57@&!i@Dwb00j6;ESstEJhZhW)I%HU_@t6G{T0{A6U;h-B|>Xy(%f7(W{ zCPAj-H`*$Oe&>pXYtCT4J-(Gy>|6#t?Ij7hOVwt}=}G|uLA}I$u<-CFUG#6llc%`S zwGl@ymSqTcn}wqV>4CVHms)wxF<+1UDi62e;3=f;8@Jt{|-CjSqx@$`=RVn41i-~;BW~#@``V&t&k^86kk_bAJ#BWdHv-!oi7J*-3M`e{&tv5J~g({OE5z|~k7Rq)6NR8&kK=a?>95qDbmmT)` z`;-sMY> z=zwo|Mtp(#+os3%SC;|jxCW+2vF5hPoF}Hu(-(~Vu-{4)$f-seje}jb}JFYc4c*`j;;gGWN8C5e?zG1rR`X7Ej>VF zo%0|VGocLf=l6U)BX>9GlWTUouJGnp|4sn)Gybcl;3 zHw_@KIo~I+?q8XOUpZIgB(qGFKHt3k)pxYU^W_6#fZ7^tsOT<75#{SdE~gGBqBw&l zGPU~DdI9j&DqZ*1@6|v9-oUFfvhlY%!V<7X>G;WLnzdo(u(;!NJ>xzyZ?Fd{DVgcCV_6a8O zC<7IMgwt?lf5X$@4Z0s?jnd{r#M3-F0MQAisBS_|v0ae@&W5n$X@$hxM$}JvY^QIi z%nf574uVw%sqs`g*b~W8*|RLKI58i6JtReq`nmn`S@C&AT9T=kd)m~9r_685o`g~SY;w*N!dbxR@ zMF02&fBOwW2w3UBGKnDUP7H3%d^@S+1U-C&RP0Hh*%NZ6#_AMjVI6fb=g`6~tuHT= zKwG7Vlgt^_=w341y&_;{f|&fn*aWNAru186P@{-BC3P73S~|jJtvCoEFMKyU9|I!( zvQo^*cI_#`xFtx1c@*-!pB%jprJ Date: Mon, 8 Jan 2024 15:50:31 +0100 Subject: [PATCH 16/26] Refactors and fixes benchmarks --- apps/src/lib/bench_utils.rs | 164 +++++++++++++++++++++++++++++++++--- benches/native_vps.rs | 5 +- benches/txs.rs | 8 +- benches/vps.rs | 4 +- 4 files changed, 160 insertions(+), 21 deletions(-) diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index ddfae1a4cc..cbe4475114 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -61,10 +61,10 @@ use namada::ledger::queries::{ }; use namada::ledger::storage_api::StorageRead; use namada::proto::{Code, Data, Section, Signature, Tx}; -use namada::tendermint::Hash; use namada::tendermint_rpc::{self}; use namada::types::address::InternalAddress; use namada::types::chain::ChainId; +use namada::types::hash::Hash; use namada::types::io::StdIo; use namada::types::masp::{ ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, @@ -125,6 +125,9 @@ static SHELL_INIT: Once = Once::new(); pub struct BenchShell { pub inner: Shell, + // Cache of the masp transactions in the last block committed, the tx index + // coincides with the index in this collection + pub last_block_masp_txs: Vec, // NOTE: Temporary directory should be dropped last since Shell need to // flush data on drop tempdir: TempDir, @@ -158,7 +161,7 @@ impl Default for BenchShell { let tempdir = tempfile::tempdir().unwrap(); let path = tempdir.path().canonicalize().unwrap(); - let mut shell = Shell::new( + let shell = Shell::new( config::Ledger::new(path, Default::default(), TendermintMode::Full), WASM_DIR.into(), sender, @@ -167,8 +170,13 @@ impl Default for BenchShell { 50 * 1024 * 1024, // 50 kiB 50 * 1024 * 1024, // 50 kiB ); + let mut bench_shell = BenchShell { + inner: shell, + last_block_masp_txs: vec![], + tempdir, + }; - shell + bench_shell .init_chain( InitChain { time: Timestamp { @@ -208,7 +216,7 @@ impl Default for BenchShell { ) .unwrap(); // Commit tx hashes to storage - shell.commit(); + bench_shell.commit_block(); // Bond from Albert to validator let bond = Bond { @@ -217,12 +225,8 @@ impl Default for BenchShell { source: Some(defaults::albert_address()), }; let params = - proof_of_stake::storage::read_pos_params(&shell.wl_storage) + proof_of_stake::storage::read_pos_params(&bench_shell.wl_storage) .unwrap(); - let mut bench_shell = BenchShell { - inner: shell, - tempdir, - }; let signed_tx = bench_shell.generate_tx( TX_BOND_WASM, bond, @@ -259,7 +263,7 @@ impl Default for BenchShell { bench_shell.execute_tx(&signed_tx); bench_shell.wl_storage.commit_tx(); - bench_shell.inner.commit(); + bench_shell.commit_block(); // Advance epoch for pos benches for _ in 0..=(params.pipeline_len + params.unbonding_len) { @@ -468,7 +472,7 @@ impl BenchShell { let consensus_state = ConsensusStateType { timestamp: now, root: CommitmentRoot::from_bytes(&[]), - next_validators_hash: Hash::Sha256([0u8; 32]), + next_validators_hash: tendermint::Hash::Sha256([0u8; 32]), } .into(); @@ -554,6 +558,21 @@ impl BenchShell { .write(&channel_key, channel.encode_vec()) .unwrap(); } + + pub fn commit_block(&mut self) { + // Update the block height in state to guarantee a valid response to the + // client queries + self.inner + .wl_storage + .storage + .begin_block( + Hash::default().into(), + self.inner.wl_storage.storage.get_last_block_height() + 1, + ) + .unwrap(); + + self.inner.commit(); + } } pub fn generate_foreign_key_tx(signer: &SecretKey) -> Tx { @@ -725,9 +744,126 @@ impl Client for BenchShell { where R: tendermint_rpc::SimpleRequest, { - Ok(R::Output::from( - tendermint_rpc::Response::from_string("MOCK RESPONSE").unwrap(), - )) + unimplemented!( + "Arbitrary queries are not implemented in the benchmarks context" + ); + } + + async fn block( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + // NOTE: atm this is only needed to query blocks at a specific height + // for masp transactions + let height = BlockHeight(height.into().into()); + + // Given the way we setup and run benchmarks, the masp transactions can + // only present in the last block, we can mock the previous + // responses with an empty set of transactions + let last_block_txs = if height + == self.inner.wl_storage.storage.get_last_block_height() + { + self.last_block_masp_txs.clone() + } else { + vec![] + }; + Ok(tendermint_rpc::endpoint::block::Response { + block_id: tendermint::block::Id { + hash: tendermint::Hash::None, + part_set_header: tendermint::block::parts::Header::default(), + }, + block: tendermint::block::Block::new( + tendermint::block::Header { + version: tendermint::block::header::Version { + block: 0, + app: 0, + }, + chain_id: self + .inner + .chain_id + .to_string() + .try_into() + .unwrap(), + height: 1u32.into(), + time: tendermint::Time::now(), + last_block_id: None, + last_commit_hash: None, + data_hash: None, + validators_hash: tendermint::Hash::None, + next_validators_hash: tendermint::Hash::None, + consensus_hash: tendermint::Hash::None, + app_hash: tendermint::AppHash::default(), + last_results_hash: None, + evidence_hash: None, + proposer_address: tendermint::account::Id::new([0u8; 20]), + }, + last_block_txs.into_iter().map(|tx| tx.to_bytes()).collect(), + tendermint::evidence::List::default(), + None, + ) + .unwrap(), + }) + } + + async fn block_results( + &self, + height: H, + ) -> Result< + tendermint_rpc::endpoint::block_results::Response, + tendermint_rpc::Error, + > + where + H: TryInto + Send, + { + // NOTE: atm this is only needed to query block results at a specific + // height for masp transactions + let height = height.try_into().map_err(|_| { + tendermint_rpc::Error::parse( + "Could not parse block height".to_string(), + ) + })?; + + // We can expect all the masp tranfers to have happened only in the last + // block + let end_block_events = if u64::from(height) + == self.inner.wl_storage.storage.get_last_block_height().0 + { + Some( + self.last_block_masp_txs + .iter() + .enumerate() + .map(|(idx, _tx)| { + namada::tendermint::abci::Event { + kind: "applied".to_string(), + // Mock only the masp attribute + attributes: vec![ + namada::tendermint::abci::EventAttribute { + key: "is_valid_masp_tx".to_string(), + value: format!("{}", idx), + index: true, + }, + ], + } + }) + .collect(), + ) + } else { + None + }; + + Ok(tendermint_rpc::endpoint::block_results::Response { + height, + txs_results: None, + finalize_block_events: vec![], + begin_block_events: None, + end_block_events, + validator_updates: vec![], + consensus_param_updates: None, + app_hash: namada::tendermint::hash::AppHash::default(), + }) } } diff --git a/benches/native_vps.rs b/benches/native_vps.rs index ed6da9c22e..5717ff30c5 100644 --- a/benches/native_vps.rs +++ b/benches/native_vps.rs @@ -522,8 +522,9 @@ fn setup_storage_for_masp_verification( .wl_storage .write(&anchor_key, ()) .unwrap(); - - shielded_ctx.shell.commit(); + shielded_ctx.shell.commit_block(); + // Cache the masp tx so that it can be returned when queried + shielded_ctx.shell.last_block_masp_txs.push(shield_tx); let (mut shielded_ctx, signed_tx) = match bench_name { "shielding" => shielded_ctx.generate_masp_tx( diff --git a/benches/txs.rs b/benches/txs.rs index 523cd48489..1e40c4b220 100644 --- a/benches/txs.rs +++ b/benches/txs.rs @@ -91,7 +91,9 @@ fn transfer(c: &mut Criterion) { ); shielded_ctx.shell.execute_tx(&shield_tx); shielded_ctx.shell.wl_storage.commit_tx(); - shielded_ctx.shell.commit(); + shielded_ctx.shell.commit_block(); + // Cache the masp tx so that it can be returned when queried + shielded_ctx.shell.last_block_masp_txs.push(shield_tx); let (shielded_ctx, signed_tx) = match bench_name { "transparent" => shielded_ctx.generate_masp_tx( @@ -850,7 +852,7 @@ fn unjail_validator(c: &mut Criterion) { .unwrap(); shell.wl_storage.commit_tx(); - shell.commit(); + shell.commit_block(); // Advance by slash epoch offset epochs for _ in 0..=pos_params.slash_processing_epoch_offset() { shell.advance_epoch(); @@ -1008,7 +1010,7 @@ fn reactivate_validator(c: &mut Criterion) { .unwrap(); shell.wl_storage.commit_tx(); - shell.commit(); + shell.commit_block(); // Advance by slash epoch offset epochs for _ in 0..=pos_params.pipeline_len { shell.advance_epoch(); diff --git a/benches/vps.rs b/benches/vps.rs index 620d14c43f..035f99345d 100644 --- a/benches/vps.rs +++ b/benches/vps.rs @@ -275,14 +275,14 @@ fn vp_implicit(c: &mut Criterion) { // Reveal public key shell.execute_tx(&reveal_pk); shell.wl_storage.commit_tx(); - shell.commit(); + shell.commit_block(); } if bench_name == "transfer" || bench_name == "pos" { // Transfer some tokens to the implicit address shell.execute_tx(&received_transfer); shell.wl_storage.commit_tx(); - shell.commit(); + shell.commit_block(); } // Run the tx to validate From 31410a7ac561e8ec01de514416b9a7d2755d9399 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 8 Jan 2024 17:31:52 +0100 Subject: [PATCH 17/26] Moves block query logic of integration tests in `block` --- .../src/lib/node/ledger/shell/testing/node.rs | 71 +++++++------------ 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index 02c8d6bf4f..0ae364eae9 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -39,7 +39,6 @@ use namada::types::time::DateTimeUtc; use namada_sdk::queries::Client; use regex::Regex; use tendermint_rpc::endpoint::block; -use tendermint_rpc::Response; use tokio::sync::mpsc; use crate::facade::tendermint_proto::v0_37::abci::{ @@ -703,55 +702,12 @@ impl<'a> Client for &'a MockNode { async fn perform( &self, - request: R, + _request: R, ) -> std::result::Result where R: SimpleRequest, { - self.drive_mock_services_bg().await; - // NOTE: atm this is only needed to query blocks at a specific height - match request.method() { - tendermint_rpc::Method::Block => { - let request_str = request.into_json(); - const QUERY_PARSING_REGEX_STR: &str = r#".*"height": "(.)+".*"#; - - lazy_static! { - /// Compiled regular expression used to parse queries. - static ref QUERY_PARSING_REGEX: Regex = Regex::new(QUERY_PARSING_REGEX_STR).unwrap(); - } - - let captures = - QUERY_PARSING_REGEX.captures(&request_str).unwrap(); - let mut height_str = captures - .get(0) - .unwrap() - .as_str() - .rsplit(':') - .next() - .unwrap() - .to_owned(); - height_str.retain(|c| !(c.is_whitespace() || c == '"')); - let height = BlockHeight::from_str(&height_str).unwrap(); - - match self.blocks.lock().unwrap().get(&height) { - Some(block) => { - let wrapper = - tendermint_rpc::response::Wrapper::new_with_id( - tendermint_rpc::Id::None, - Some(block), - None, - ); - - let response = serde_json::to_string(&wrapper).unwrap(); - R::Response::from_string(response).map(Into::into) - } - None => Err(RpcError::invalid_params(format!( - "Could not find block height {height}" - ))), - } - } - _ => unimplemented!(), - } + unimplemented!("Client's perform method is not implemented for testing") } /// `/abci_info`: get information about the ABCI application. @@ -948,6 +904,29 @@ impl<'a> Client for &'a MockNode { }) } + async fn block( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + // NOTE: atm this is only needed to query blocks at a + // specific height for masp transactions + let height = BlockHeight(height.into().into()); + + self.blocks + .lock() + .unwrap() + .get(&height) + .cloned() + .ok_or_else(|| { + RpcError::invalid_params(format!( + "Could not find block at height {height}" + )) + }) + } + /// `/tx_search`: search for transactions with their results. async fn tx_search( &self, From ab41cf42ba5cdbf626ba5c5163c0f9c095fd0b28 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 8 Jan 2024 18:00:48 +0100 Subject: [PATCH 18/26] Cleans up sdk masp file --- sdk/src/masp.rs | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 092d21a69e..a10f2111d7 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -719,7 +719,6 @@ impl ShieldedContext { let (txs, mut tx_iter); if !unknown_keys.is_empty() { // Load all transactions accepted until this point - eprintln!("FETCHING AGAIN FROM INDEX 0 BECAUSE NEW KEY"); //FIXME: remove txs = Self::fetch_shielded_transfers(client, None).await?; tx_iter = txs.iter(); // Do this by constructing a shielding context only for unknown keys @@ -759,7 +758,6 @@ impl ShieldedContext { /// Obtain a chronologically-ordered list of all accepted shielded /// transactions from a node. - // FIXME: remove all the unwraps, we are in the sdk here pub async fn fetch_shielded_transfers( client: &C, last_indexed_tx: Option, @@ -770,9 +768,6 @@ impl ShieldedContext { .await? .map_or_else(BlockHeight::first, |block| block.height); - eprintln!("ABOUT TO REQUEST PAGINATED RESULT"); //FIXME: remove - eprintln!("LAST BLOCK HEIGHT: {}", last_block_height); //FIXME: remove - eprintln!("LAST INDEXED HEIGHT: {:#?}", last_indexed_tx); //FIXME: remove let mut shielded_txs = BTreeMap::new(); // Fetch all the transactions we do not have yet let first_height_to_query = @@ -780,7 +775,6 @@ impl ShieldedContext { let first_idx_to_query = last_indexed_tx.map_or_else(|| 0, |last| last.index.0 + 1); for height in first_height_to_query..=last_block_height.0 { - eprintln!("IN HEIGHT {height} LOOP"); //FIXME: remove // Get the valid masp transactions at the specified height let epoch = query_epoch_at_height(client, height.into()) @@ -792,14 +786,12 @@ impl ShieldedContext { ))) })?; - eprintln!("REQUESTING BLOCK AT HEIGHT: {}", height); //FIXME: remove let txs_results = match client .block_results(height) .await .map_err(|e| Error::from(QueryError::General(e.to_string())))? .end_block_events { - // FIXME: imrpove this match Some(events) => events .into_iter() .filter_map(|event| { @@ -809,7 +801,6 @@ impl ShieldedContext { event.attributes.iter().find_map(|attribute| { if attribute.key == "is_valid_masp_tx" { Some(TxIndex( - // FIXME: ok to unwrap here? u32::from_str(&attribute.value) .unwrap(), )) @@ -835,12 +826,10 @@ impl ShieldedContext { }) .collect::>(), None => { - eprintln!("NO EVENTS IN END BLOCK, CONITNUING"); //FIXME: remove continue; } }; - eprintln!("BEFORE BLOCK"); //FIXME: remove // Query the actual block to get the txs bytes. If we only need one // tx it might be slightly better to query the /tx endpoint to // reduce the amount of data sent over the network, but this is a @@ -853,7 +842,6 @@ impl ShieldedContext { .block .data; - eprintln!("SIZE OF RESPONSE: {}", txs_results.len()); //FIXME: remove for (idx, tx_event) in txs_results { let tx = Tx::try_from(block[idx.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; @@ -868,7 +856,6 @@ impl ShieldedContext { let (transfer, masp_transaction) = if let Some(wrapper_header) = tx_header.wrapper() { - eprintln!("FOUND WRAPPER MASP TX"); //FIXME: remove let hash = wrapper_header .unshield_section_hash .ok_or_else(|| { @@ -907,12 +894,10 @@ impl ShieldedContext { (transfer, masp_transaction) } else { // Expect decrypted transaction - eprintln!("FOUND DECRYPTED MASP TX AT HEIGHT: {}", height); //FIXME: remove match Transfer::try_from_slice(&tx.data().ok_or_else( || Error::Other("Missing data section".to_string()), )?) { Ok(transfer) => { - eprintln!("DECRYPTED TX: {:#?}", transfer); //FIXME: remove let masp_transaction = tx .get_section(&transfer.shielded.ok_or_else( || { @@ -944,7 +929,6 @@ impl ShieldedContext { .iter() .find_map(|attribute| { if attribute.key == "inner_tx" { - //FIXME: manage unwrap here let tx_result = TxResult::from_str(&attribute.value).unwrap(); for ibc_event in tx_result.ibc_events { @@ -956,9 +940,8 @@ impl ShieldedContext { } None } else {None } - }).unwrap().unwrap(); //FIXME:remove these unwraps + }).ok_or_else(|| Error::Other("Missing expected memo field in IBC over MASP transaction".to_string()))?.map_err(|e| Error::Other(e.to_string()))?; - eprintln!("FOUND IBC MASP EVENT"); //FIXME:remove ( shielded_transfer.transfer, shielded_transfer.masp_tx, @@ -978,8 +961,6 @@ impl ShieldedContext { } } - eprintln!("DONE REQUESTING HEIGHT"); //FIXME: remove - Ok(shielded_txs) } @@ -1145,10 +1126,8 @@ impl ShieldedContext { ) -> Result, Error> { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { - eprintln!("KEY NOT IN MAP"); //FIXME: remove return Ok(None); } - eprintln!("KEY IN MAP"); //FIXME: remove let mut val_acc = I128Sum::zero(); // Retrieve the notes that can be spent by this key if let Some(avail_notes) = self.pos_map.get(vk) { @@ -1543,12 +1522,12 @@ impl ShieldedContext { } } // Construct the key for where the transaction ID would be stored - // FIXME: should index the tx hash, not the block and height? Yes it's - // faster to query, less data let pin_key = namada_core::types::token::masp_pin_tx_key(&owner.hash()); // Obtain the transaction pointer at the key // If we don't discard the error message then a test fails, // however the error underlying this will go undetected + // FIXME: we could index the comet tx hash here so that we could just + // query it via a single rpc call to the /tx endpoint let indexed_tx = rpc::query_storage_value::(client, &pin_key) .await @@ -1590,7 +1569,8 @@ impl ShieldedContext { Error::Other("Missing masp transaction".to_string()) })?, Err(_) => { - // FIXME: add support for pinned ibc masp txs? Yes + // FIXME: add support for pinned ibc masp txs, but I need to + // query tx to do this return Err(Error::Other("IBC Masp pinned tx".to_string())); } }; From 47b9b2d3e7489506bf7f521e876bea15fa45788c Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 8 Jan 2024 19:10:04 +0100 Subject: [PATCH 19/26] Refactors wrapper tx args for execution --- .../lib/node/ledger/shell/finalize_block.rs | 247 ++++++++++-------- apps/src/lib/node/ledger/shell/governance.rs | 1 - shared/src/ledger/mod.rs | 1 - shared/src/ledger/protocol/mod.rs | 36 +-- 4 files changed, 153 insertions(+), 132 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 6f67b1651b..19d0fda391 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -8,7 +8,7 @@ use namada::core::ledger::pgf::inflation as pgf_inflation; use namada::ledger::events::EventType; use namada::ledger::gas::{GasMetering, TxGasMeter}; use namada::ledger::pos::namada_proof_of_stake; -use namada::ledger::protocol; +use namada::ledger::protocol::{self, WrapperArgs}; use namada::ledger::storage::wl_storage::WriteLogAndStorage; use namada::ledger::storage::write_log::StorageModification; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; @@ -268,134 +268,146 @@ where continue; } - let (mut tx_event, embedding_wrapper, mut tx_gas_meter, wrapper) = - match &tx_header.tx_type { - TxType::Wrapper(wrapper) => { - stats.increment_wrapper_txs(); - let tx_event = Event::new_tx_event(&tx, height.0); - let gas_meter = TxGasMeter::new(wrapper.gas_limit); - (tx_event, None, gas_meter, Some(tx.clone())) - } - TxType::Decrypted(inner) => { - // We remove the corresponding wrapper tx from the queue - let tx_in_queue = self - .wl_storage - .storage - .tx_queue - .pop() - .expect("Missing wrapper tx in queue"); - let mut event = Event::new_tx_event(&tx, height.0); - - match inner { - DecryptedTx::Decrypted => { - if let Some(code_sec) = tx - .get_section(tx.code_sechash()) - .and_then(|x| Section::code_sec(x.as_ref())) - { - stats.increment_tx_type( - code_sec.code.hash().to_string(), - ); - } - } - DecryptedTx::Undecryptable => { - tracing::info!( - "Tx with hash {} was un-decryptable", - tx_in_queue.tx.header_hash() + let ( + mut tx_event, + embedding_wrapper, + mut tx_gas_meter, + wrapper, + mut wrapper_args, + ) = match &tx_header.tx_type { + TxType::Wrapper(wrapper) => { + stats.increment_wrapper_txs(); + let tx_event = Event::new_tx_event(&tx, height.0); + let gas_meter = TxGasMeter::new(wrapper.gas_limit); + ( + tx_event, + None, + gas_meter, + Some(tx.clone()), + Some(WrapperArgs { + block_proposer: &native_block_proposer_address, + is_committed_fee_unshield: false, + }), + ) + } + TxType::Decrypted(inner) => { + // We remove the corresponding wrapper tx from the queue + let tx_in_queue = self + .wl_storage + .storage + .tx_queue + .pop() + .expect("Missing wrapper tx in queue"); + let mut event = Event::new_tx_event(&tx, height.0); + + match inner { + DecryptedTx::Decrypted => { + if let Some(code_sec) = tx + .get_section(tx.code_sechash()) + .and_then(|x| Section::code_sec(x.as_ref())) + { + stats.increment_tx_type( + code_sec.code.hash().to_string(), ); - event["info"] = - "Transaction is invalid.".into(); - event["log"] = "Transaction could not be \ - decrypted." - .into(); - event["code"] = - ResultCode::Undecryptable.into(); - response.events.push(event); - continue; } } - - ( - event, - Some(tx_in_queue.tx), - TxGasMeter::new_from_sub_limit(tx_in_queue.gas), - None, - ) - } - TxType::Raw => { - tracing::error!( - "Internal logic error: FinalizeBlock received a \ - TxType::Raw transaction" - ); - continue; + DecryptedTx::Undecryptable => { + tracing::info!( + "Tx with hash {} was un-decryptable", + tx_in_queue.tx.header_hash() + ); + event["info"] = "Transaction is invalid.".into(); + event["log"] = + "Transaction could not be decrypted.".into(); + event["code"] = ResultCode::Undecryptable.into(); + response.events.push(event); + continue; + } } - TxType::Protocol(protocol_tx) => match protocol_tx.tx { - ProtocolTxType::BridgePoolVext - | ProtocolTxType::BridgePool - | ProtocolTxType::ValSetUpdateVext - | ProtocolTxType::ValidatorSetUpdate => ( - Event::new_tx_event(&tx, height.0), - None, - TxGasMeter::new_from_sub_limit(0.into()), - None, - ), - ProtocolTxType::EthEventsVext => { - let ext = + + ( + event, + Some(tx_in_queue.tx), + TxGasMeter::new_from_sub_limit(tx_in_queue.gas), + None, + None, + ) + } + TxType::Raw => { + tracing::error!( + "Internal logic error: FinalizeBlock received a \ + TxType::Raw transaction" + ); + continue; + } + TxType::Protocol(protocol_tx) => match protocol_tx.tx { + ProtocolTxType::BridgePoolVext + | ProtocolTxType::BridgePool + | ProtocolTxType::ValSetUpdateVext + | ProtocolTxType::ValidatorSetUpdate => ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + None, + None, + ), + ProtocolTxType::EthEventsVext => { + let ext = ethereum_tx_data_variants::EthEventsVext::try_from( &tx, ) .unwrap(); - if self - .mode - .get_validator_address() - .map(|validator| { - validator == &ext.data.validator_addr - }) - .unwrap_or(false) - { - for event in ext.data.ethereum_events.iter() { - self.mode.dequeue_eth_event(event); - } + if self + .mode + .get_validator_address() + .map(|validator| { + validator == &ext.data.validator_addr + }) + .unwrap_or(false) + { + for event in ext.data.ethereum_events.iter() { + self.mode.dequeue_eth_event(event); } - ( - Event::new_tx_event(&tx, height.0), - None, - TxGasMeter::new_from_sub_limit(0.into()), - None, - ) } - ProtocolTxType::EthereumEvents => { - let digest = + ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + None, + None, + ) + } + ProtocolTxType::EthereumEvents => { + let digest = ethereum_tx_data_variants::EthereumEvents::try_from( &tx, ).unwrap(); - if let Some(address) = - self.mode.get_validator_address().cloned() + if let Some(address) = + self.mode.get_validator_address().cloned() + { + let this_signer = &( + address, + self.wl_storage.storage.get_last_block_height(), + ); + for MultiSignedEthEvent { event, signers } in + &digest.events { - let this_signer = &( - address, - self.wl_storage - .storage - .get_last_block_height(), - ); - for MultiSignedEthEvent { event, signers } in - &digest.events - { - if signers.contains(this_signer) { - self.mode.dequeue_eth_event(event); - } + if signers.contains(this_signer) { + self.mode.dequeue_eth_event(event); } } - ( - Event::new_tx_event(&tx, height.0), - None, - TxGasMeter::new_from_sub_limit(0.into()), - None, - ) } - }, - }; + ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + None, + None, + ) + } + }, + }; - let mut is_committed_fee_unshield = false; match protocol::dispatch_tx( tx, processed_tx.tx.as_ref(), @@ -408,8 +420,7 @@ where &mut self.wl_storage, &mut self.vp_wasm_cache, &mut self.tx_wasm_cache, - Some(&native_block_proposer_address), - &mut is_committed_fee_unshield, + wrapper_args.as_mut(), ) .map_err(Error::TxApply) { @@ -421,7 +432,10 @@ where "Wrapper transaction {} was accepted", tx_event["hash"] ); - if is_committed_fee_unshield { + if wrapper_args + .expect("Missing required wrapper arguments") + .is_committed_fee_unshield + { tx_event["is_valid_masp_tx"] = format!("{}", tx_index); } @@ -555,7 +569,10 @@ where tx_event["code"] = ResultCode::InvalidTx.into(); // The fee unshield operation could still have been // committed - if is_committed_fee_unshield { + if wrapper_args + .expect("Missing required wrapper arguments") + .is_committed_fee_unshield + { tx_event["is_valid_masp_tx"] = format!("{}", tx_index); } diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index ea64fa0c51..4991310d09 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -287,7 +287,6 @@ where &mut shell.vp_wasm_cache, &mut shell.tx_wasm_cache, None, - &mut false, ); shell .wl_storage diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 1a28ff12f4..abd96064bb 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -71,7 +71,6 @@ mod dry_run_tx { &mut ctx.tx_wasm_cache, ), None, - &mut false, ) .into_storage_result()?; diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index f2ee274fae..8416bd0d82 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -134,6 +134,14 @@ where /// Result of applying a transaction pub type Result = std::result::Result; +/// Arguments needed to execute a Wrapper transaction +pub struct WrapperArgs<'a> { + /// The block proposer for the current block + pub block_proposer: &'a Address, + /// Flag if the wrapper transaction committed the fee unshielding operation + pub is_committed_fee_unshield: bool, +} + /// Dispatch a given transaction to be applied based on its type. Some storage /// updates may be derived and applied natively rather than via the wasm /// environment, in which case validity predicates will be bypassed. @@ -150,10 +158,7 @@ pub fn dispatch_tx<'a, D, H, CA>( wl_storage: &'a mut WlStorage, vp_wasm_cache: &'a mut VpCache, tx_wasm_cache: &'a mut TxCache, - // FIXME: these two params together because they are only needed for - // wrappers - block_proposer: Option<&'a Address>, - is_committed_fee_unshield: &mut bool, + wrapper_args: Option<&mut WrapperArgs>, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -189,8 +194,7 @@ where vp_wasm_cache, tx_wasm_cache, }, - block_proposer, - is_committed_fee_unshield, + wrapper_args, )?; Ok(TxResult { gas_used: tx_gas_meter.get_tx_consumed_gas(), @@ -235,8 +239,7 @@ pub(crate) fn apply_wrapper_tx<'a, D, H, CA, WLS>( fee_unshield_transaction: Option, tx_bytes: &[u8], mut shell_params: ShellParams<'a, CA, WLS>, - block_proposer: Option<&Address>, - is_committed_fee_unshield: &mut bool, + wrapper_args: Option<&mut WrapperArgs>, ) -> Result> where CA: 'static + WasmCacheAccess + Sync, @@ -257,9 +260,8 @@ where wrapper, fee_unshield_transaction, &mut shell_params, - block_proposer, &mut changed_keys, - is_committed_fee_unshield, + wrapper_args, )?; // Account for gas @@ -299,9 +301,8 @@ fn charge_fee<'a, D, H, CA, WLS>( wrapper: &WrapperTx, masp_transaction: Option, shell_params: &mut ShellParams<'a, CA, WLS>, - block_proposer: Option<&Address>, changed_keys: &mut BTreeSet, - is_committed_fee_unshield: &mut bool, + wrapper_args: Option<&mut WrapperArgs>, ) -> Result<()> where CA: 'static + WasmCacheAccess + Sync, @@ -385,8 +386,11 @@ where }; // Charge or check fees - match block_proposer { - Some(proposer) => transfer_fee(*wl_storage, proposer, wrapper)?, + match wrapper_args { + Some(WrapperArgs { + block_proposer, + is_committed_fee_unshield: _, + }) => transfer_fee(*wl_storage, block_proposer, wrapper)?, None => check_fees(*wl_storage, wrapper)?, } @@ -395,7 +399,9 @@ where // Commit tx write log even in case of subsequent errors wl_storage.write_log_mut().commit_tx(); // Update the flag only after the fee payment has been committed - *is_committed_fee_unshield = requires_fee_unshield; + if let Some(args) = wrapper_args { + args.is_committed_fee_unshield = requires_fee_unshield; + } Ok(()) } From 61f6a695e3c2949903896405b278f62bd3f462c3 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 8 Jan 2024 20:21:08 +0100 Subject: [PATCH 20/26] Improves masp over ibc tx fetching in client --- core/src/ledger/ibc/mod.rs | 9 +++-- sdk/src/masp.rs | 71 ++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/core/src/ledger/ibc/mod.rs b/core/src/ledger/ibc/mod.rs index 2c6e054be2..fc6cb7416d 100644 --- a/core/src/ledger/ibc/mod.rs +++ b/core/src/ledger/ibc/mod.rs @@ -294,13 +294,18 @@ where } } -enum IbcMessage { +/// The different variants of an Ibc message +pub enum IbcMessage { + /// Ibc Envelop Envelope(MsgEnvelope), + /// Ibc transaprent transfer Transfer(MsgTransfer), + /// Ibc shielded transfer ShieldedTransfer(MsgShieldedTransfer), } -fn decode_message(tx_data: &[u8]) -> Result { +/// Tries to decode transaction data to an `IbcMessage` +pub fn decode_message(tx_data: &[u8]) -> Result { // ibc-rs message if let Ok(any_msg) = Any::decode(tx_data) { if let Ok(transfer_msg) = MsgTransfer::try_from(any_msg.clone()) { diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index a10f2111d7..cb56845e8b 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -50,9 +50,8 @@ use masp_proofs::bellman::groth16::PreparedVerifyingKey; use masp_proofs::bls12_381::Bls12; use masp_proofs::prover::LocalTxProver; use masp_proofs::sapling::SaplingVerificationContext; -use namada_core::ibc::apps::transfer::types::Memo; +use namada_core::ledger::ibc::IbcMessage; use namada_core::types::address::{Address, MASP}; -use namada_core::types::ibc::IbcShieldedTransfer; use namada_core::types::masp::{ BalanceOwner, ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, @@ -780,10 +779,11 @@ impl ShieldedContext { let epoch = query_epoch_at_height(client, height.into()) .await? .ok_or_else(|| { - Error::from(QueryError::General(format!( + Error::from(QueryError::General( "Queried height is greater than the last committed \ block height" - ))) + .to_string(), + )) })?; let txs_results = match client @@ -894,9 +894,10 @@ impl ShieldedContext { (transfer, masp_transaction) } else { // Expect decrypted transaction - match Transfer::try_from_slice(&tx.data().ok_or_else( - || Error::Other("Missing data section".to_string()), - )?) { + let tx_data = tx.data().ok_or_else(|| { + Error::Other("Missing data section".to_string()) + })?; + match Transfer::try_from_slice(&tx_data) { Ok(transfer) => { let masp_transaction = tx .get_section(&transfer.shielded.ok_or_else( @@ -923,24 +924,43 @@ impl ShieldedContext { (transfer, masp_transaction) } Err(_) => { - // This should be a MASP over IBC transaction - let shielded_transfer = tx_event - .attributes - .iter() - .find_map(|attribute| { - if attribute.key == "inner_tx" { - let tx_result = TxResult::from_str(&attribute.value).unwrap(); - - for ibc_event in tx_result.ibc_events { - for (key, value) in ibc_event.attributes { - if key == "memo" { - return Some(IbcShieldedTransfer::try_from(Memo::from(value))); - } + // This should be a MASP over IBC transaction, it + // could be a ShieldedTransfer or an Envelop + // message, need to try both + let message = + namada_core::ledger::ibc::decode_message( + &tx_data, + ) + .map_err(|e| Error::Other(e.to_string()))?; + + let shielded_transfer = match message { + IbcMessage::ShieldedTransfer(msg) => { + msg.shielded_transfer + } + IbcMessage::Envelope(_) => { + tx_event + .attributes + .iter() + .find_map(|attribute| { + if attribute.key == "inner_tx" { + let tx_result = TxResult::from_str(&attribute.value).unwrap(); + for ibc_event in &tx_result.ibc_events { + + let event = namada_core::types::ibc::get_shielded_transfer(ibc_event).ok().flatten(); + if event.is_some() { + return event; } - } + } + None + } else { None - } else {None } - }).ok_or_else(|| Error::Other("Missing expected memo field in IBC over MASP transaction".to_string()))?.map_err(|e| Error::Other(e.to_string()))?; + } + }).ok_or_else(|| Error::Other("Couldn't deserialize masp tx to ibc message envelope".to_string()))? + } + _ => { + return Err(Error::Other("Couldn't deserialize masp tx to a valid ibc message".to_string())); + } + }; ( shielded_transfer.transfer, @@ -1535,10 +1555,11 @@ impl ShieldedContext { let tx_epoch = query_epoch_at_height(client, indexed_tx.height) .await? .ok_or_else(|| { - Error::from(QueryError::General(format!( + Error::from(QueryError::General( "Queried height is greater than the last committed block \ height" - ))) + .to_string(), + )) })?; let block = client From 3261b871a28be4afd4957453e0e48d75bcbd00c9 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 8 Jan 2024 22:51:51 +0100 Subject: [PATCH 21/26] `compute_pinned_balance` supports pinned masp over ibc transactions --- sdk/src/masp.rs | 197 +++++++++++++++++++++++++------------- shared/src/vm/host_env.rs | 2 - 2 files changed, 131 insertions(+), 68 deletions(-) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index cb56845e8b..2b922972fc 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -775,7 +775,6 @@ impl ShieldedContext { last_indexed_tx.map_or_else(|| 0, |last| last.index.0 + 1); for height in first_height_to_query..=last_block_height.0 { // Get the valid masp transactions at the specified height - let epoch = query_epoch_at_height(client, height.into()) .await? .ok_or_else(|| { @@ -786,48 +785,21 @@ impl ShieldedContext { )) })?; - let txs_results = match client - .block_results(height) - .await - .map_err(|e| Error::from(QueryError::General(e.to_string())))? - .end_block_events + let first_index_to_query = if height == first_height_to_query { + Some(TxIndex(first_idx_to_query)) + } else { + None + }; + + let txs_results = match get_indexed_masp_events_at_height( + client, + height.into(), + first_index_to_query, + ) + .await? { - Some(events) => events - .into_iter() - .filter_map(|event| { - // Filter only the tx events which are valid masp txs - // and that we haven't fetched yet - let tx_index = - event.attributes.iter().find_map(|attribute| { - if attribute.key == "is_valid_masp_tx" { - Some(TxIndex( - u32::from_str(&attribute.value) - .unwrap(), - )) - } else { - None - } - }); - - match tx_index { - Some(idx) => { - if height == first_height_to_query { - if idx.0 >= first_idx_to_query { - Some((idx, event)) - } else { - None - } - } else { - Some((idx, event)) - } - } - None => None, - } - }) - .collect::>(), - None => { - continue; - } + Some(events) => events, + None => continue, }; // Query the actual block to get the txs bytes. If we only need one @@ -1546,8 +1518,6 @@ impl ShieldedContext { // Obtain the transaction pointer at the key // If we don't discard the error message then a test fails, // however the error underlying this will go undetected - // FIXME: we could index the comet tx hash here so that we could just - // query it via a single rpc call to the /tx endpoint let indexed_tx = rpc::query_storage_value::(client, &pin_key) .await @@ -1572,29 +1542,79 @@ impl ShieldedContext { let tx = Tx::try_from(block[indexed_tx.index.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; - let shielded = - match Transfer::try_from_slice(&tx.data().ok_or_else(|| { - Error::Other("Missing data section".to_string()) - })?) { - Ok(transfer) => tx - .get_section(&transfer.shielded.ok_or_else(|| { - Error::Other("Missing masp section hash".to_string()) - })?) - .ok_or_else(|| { - Error::Other( - "Missing masp section in transaction".to_string(), + let tx_data = tx + .data() + .ok_or_else(|| Error::Other("Missing data section".to_string()))?; + let shielded = match Transfer::try_from_slice(&tx_data) { + Ok(transfer) => tx + .get_section(&transfer.shielded.ok_or_else(|| { + Error::Other("Missing masp section hash".to_string()) + })?) + .ok_or_else(|| { + Error::Other( + "Missing masp section in transaction".to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other("Missing masp transaction".to_string()) + })?, + Err(_) => { + // Try Masp over IBC + + let message = + namada_core::ledger::ibc::decode_message(&tx_data) + .map_err(|e| Error::Other(e.to_string()))?; + + let shielded_transfer = match message { + IbcMessage::ShieldedTransfer(msg) => msg.shielded_transfer, + IbcMessage::Envelope(_) => { + // Need the tx event to extract the memo. The result is + // in the first index of the collection + let tx_events = get_indexed_masp_events_at_height( + client, + indexed_tx.height, + Some(indexed_tx.index), ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other("Missing masp transaction".to_string()) - })?, - Err(_) => { - // FIXME: add support for pinned ibc masp txs, but I need to - // query tx to do this - return Err(Error::Other("IBC Masp pinned tx".to_string())); - } - }; + .await? + .ok_or_else(|| { + Error::Other(format!( + "Missing required ibc event at block height {}", + indexed_tx.height + )) + })?; + + tx_events[0].1 + .attributes + .iter() + .find_map(|attribute| { + if attribute.key == "inner_tx" { + let tx_result = TxResult::from_str(&attribute.value).unwrap(); + for ibc_event in &tx_result.ibc_events { + + let event = namada_core::types::ibc::get_shielded_transfer(ibc_event).ok().flatten(); + if event.is_some() { + return event; + } + } + None + } else { + None + } + }).ok_or_else(|| Error::Other("Couldn't deserialize masp tx to ibc message envelope".to_string()))? + } + _ => { + return Err(Error::Other( + "Couldn't deserialize masp tx to a valid ibc \ + message" + .to_string(), + )); + } + }; + + shielded_transfer.masp_tx + } + }; // Accumulate the combined output note value into this Amount let mut val_acc = I128Sum::zero(); @@ -2271,6 +2291,51 @@ fn convert_amount( Ok((asset_types, amount)) } +// Retrieves all the indexes and tx events at the specified height which refer +// to a valid masp transaction. If an index is given, it filters only the +// transactions with an index equal or greater to the provided one. +async fn get_indexed_masp_events_at_height( + client: &C, + height: BlockHeight, + first_idx_to_query: Option, +) -> Result>, Error> { + let first_idx_to_query = first_idx_to_query.unwrap_or_default(); + + Ok(client + .block_results(height) + .await + .map_err(|e| Error::from(QueryError::General(e.to_string())))? + .end_block_events + .map(|events| { + events + .into_iter() + .filter_map(|event| { + let tx_index = + event.attributes.iter().find_map(|attribute| { + if attribute.key == "is_valid_masp_tx" { + Some(TxIndex( + u32::from_str(&attribute.value).unwrap(), + )) + } else { + None + } + }); + + match tx_index { + Some(idx) => { + if idx >= first_idx_to_query { + Some((idx, event)) + } else { + None + } + } + None => None, + } + }) + .collect::>() + })) +} + mod tests { /// quick and dirty test. will fail on size check #[test] diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index 85fda430ea..fb5ae3bb3b 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -113,7 +113,6 @@ where pub tx: HostRef<'a, &'a Tx>, /// The transaction index is used to identify a shielded transaction's /// parent - // FIXME: still need this? pub tx_index: HostRef<'a, &'a TxIndex>, /// The verifiers whose validity predicates should be triggered. pub verifiers: MutHostRef<'a, &'a BTreeSet
>, @@ -276,7 +275,6 @@ where pub tx: HostRef<'a, &'a Tx>, /// The transaction index is used to identify a shielded transaction's /// parent - // FIXME: still need this? pub tx_index: HostRef<'a, &'a TxIndex>, /// The runner of the [`vp_eval`] function pub eval_runner: HostRef<'a, &'a EVAL>, From 9408f4b00ffd3f1cae66bbc28fa9bf93126b99f3 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 9 Jan 2024 11:01:43 +0100 Subject: [PATCH 22/26] Changelog #2363 --- .changelog/unreleased/SDK/2363-masp-storage-optimization.md | 5 +++++ .../improvements/2363-masp-storage-optimization.md | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changelog/unreleased/SDK/2363-masp-storage-optimization.md create mode 100644 .changelog/unreleased/improvements/2363-masp-storage-optimization.md diff --git a/.changelog/unreleased/SDK/2363-masp-storage-optimization.md b/.changelog/unreleased/SDK/2363-masp-storage-optimization.md new file mode 100644 index 0000000000..fbb467e414 --- /dev/null +++ b/.changelog/unreleased/SDK/2363-masp-storage-optimization.md @@ -0,0 +1,5 @@ +- Modified `ShieldedContext` to use `IndexedTx` to track the last indexed + masp tx. Updated `fetch_shielded_transfer` and `compute_pinned_balance` + to query the cometBFT rpc endpoints to retrieve masp data. + Updated `block_search` to accept a fallible cast to `Height`. + ([\#2363](https://github.com/anoma/namada/pull/2363)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/2363-masp-storage-optimization.md b/.changelog/unreleased/improvements/2363-masp-storage-optimization.md new file mode 100644 index 0000000000..6f74c6970e --- /dev/null +++ b/.changelog/unreleased/improvements/2363-masp-storage-optimization.md @@ -0,0 +1,2 @@ +- Removed masp data from storage. Updated the client to query the cometBFT rpc + endpoints. ([\#2363](https://github.com/anoma/namada/pull/2363)) \ No newline at end of file From 09a1f21f2c79c8eb99a237b35aaeccf7f0304b28 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 9 Jan 2024 19:33:42 +0100 Subject: [PATCH 23/26] Uses `masp_pin_tx_key` helper where possible --- core/src/ledger/masp_utils.rs | 10 +++------- shared/src/ledger/native_vp/masp.rs | 11 ++++------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/core/src/ledger/masp_utils.rs b/core/src/ledger/masp_utils.rs index 343c7b1c73..661b246927 100644 --- a/core/src/ledger/masp_utils.rs +++ b/core/src/ledger/masp_utils.rs @@ -6,10 +6,9 @@ use masp_primitives::transaction::Transaction; use super::storage_api::{StorageRead, StorageWrite}; use crate::ledger::storage_api::{Error, Result}; -use crate::types::address::MASP; -use crate::types::storage::{IndexedTx, Key, KeySeg}; +use crate::types::storage::IndexedTx; use crate::types::token::{ - masp_commitment_tree_key, masp_nullifier_key, PIN_KEY_PREFIX, + masp_commitment_tree_key, masp_nullifier_key, masp_pin_tx_key, }; // Writes the nullifiers of the provided masp transaction to storage @@ -73,11 +72,8 @@ pub fn handle_masp_tx( // If storage key has been supplied, then pin this transaction to it if let Some(key) = pin_key { - let pin_key = Key::from(MASP.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + key)) - .expect("Cannot obtain a storage key"); ctx.write( - &pin_key, + &masp_pin_tx_key(key), IndexedTx { height: ctx.get_block_height()?, index: ctx.get_tx_index()?, diff --git a/shared/src/ledger/native_vp/masp.rs b/shared/src/ledger/native_vp/masp.rs index 4bdbc94ca9..de5c666d0c 100644 --- a/shared/src/ledger/native_vp/masp.rs +++ b/shared/src/ledger/native_vp/masp.rs @@ -14,12 +14,12 @@ use namada_core::ledger::storage; use namada_core::ledger::storage_api::OptionExt; use namada_core::ledger::vp_env::VpEnv; use namada_core::proto::Tx; +use namada_core::types::address::Address; use namada_core::types::address::InternalAddress::Masp; -use namada_core::types::address::{Address, MASP}; -use namada_core::types::storage::{Epoch, IndexedTx, Key, KeySeg}; +use namada_core::types::storage::{Epoch, IndexedTx, Key}; use namada_core::types::token::{ self, is_masp_allowed_key, is_masp_key, is_masp_nullifier_key, - PIN_KEY_PREFIX, + masp_pin_tx_key, }; use namada_sdk::masp::verify_shielded_tx; use ripemd::Digest as RipemdDigest; @@ -301,10 +301,7 @@ where // Validate pin key if let Some(key) = pin_key { - let pin_key = Key::from(MASP.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + key)) - .expect("Cannot obtain a storage key"); - match self.ctx.read_post::(&pin_key)? { + match self.ctx.read_post::(&masp_pin_tx_key(key))? { Some(IndexedTx { height, index }) if height == self.ctx.get_block_height()? && index == self.ctx.get_tx_index()? => {} From c3a2de2c1fe6a898aa7071f74eeb858dc3898358 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 9 Jan 2024 20:20:03 +0100 Subject: [PATCH 24/26] Reverts trait bound on `Height` for `block_results` endpoint --- apps/src/lib/bench_utils.rs | 10 +++------- apps/src/lib/node/ledger/shell/testing/node.rs | 6 ++---- sdk/src/masp.rs | 2 +- sdk/src/queries/mod.rs | 7 ++----- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index cbe4475114..24874f7799 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -816,19 +816,15 @@ impl Client for BenchShell { tendermint_rpc::Error, > where - H: TryInto + Send, + H: Into + Send, { // NOTE: atm this is only needed to query block results at a specific // height for masp transactions - let height = height.try_into().map_err(|_| { - tendermint_rpc::Error::parse( - "Could not parse block height".to_string(), - ) - })?; + let height = height.into(); // We can expect all the masp tranfers to have happened only in the last // block - let end_block_events = if u64::from(height) + let end_block_events = if height.value() == self.inner.wl_storage.storage.get_last_block_height().0 { Some( diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index 0ae364eae9..65425443e2 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -857,12 +857,10 @@ impl<'a> Client for &'a MockNode { height: H, ) -> Result where - H: TryInto + Send, + H: Into + Send, { self.drive_mock_services_bg().await; - let height = height.try_into().map_err(|_| { - RpcError::parse("Could not parse block height".to_string()) - })?; + let height = height.into(); let encoded_event = EncodedEvent(height.value()); let locked = self.shell.lock().unwrap(); let events: Vec<_> = locked diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 2b922972fc..7c715adca1 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -2302,7 +2302,7 @@ async fn get_indexed_masp_events_at_height( let first_idx_to_query = first_idx_to_query.unwrap_or_default(); Ok(client - .block_results(height) + .block_results(height.0 as u32) .await .map_err(|e| Error::from(QueryError::General(e.to_string())))? .end_block_events diff --git a/sdk/src/queries/mod.rs b/sdk/src/queries/mod.rs index 7506b4520d..4dbc5173b8 100644 --- a/sdk/src/queries/mod.rs +++ b/sdk/src/queries/mod.rs @@ -301,13 +301,10 @@ pub trait Client { height: H, ) -> Result where - H: TryInto + Send, + H: Into + Send, { - let height = height.try_into().map_err(|_| { - RpcError::parse("Could not parse to height".to_string()) - })?; self.perform(tendermint_rpc::endpoint::block_results::Request::new( - height, + height.into(), )) .await } From d000d536ffa8840d4bb750f09562f9b4a2a92430 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 10 Jan 2024 11:46:34 +0100 Subject: [PATCH 25/26] Shared function for shielded actions retrieval --- sdk/src/masp.rs | 176 ++++++++++++++++++++++++++---------------------- 1 file changed, 94 insertions(+), 82 deletions(-) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 7c715adca1..0cf8cb0752 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -52,6 +52,7 @@ use masp_proofs::prover::LocalTxProver; use masp_proofs::sapling::SaplingVerificationContext; use namada_core::ledger::ibc::IbcMessage; use namada_core::types::address::{Address, MASP}; +use namada_core::types::ibc::IbcShieldedTransfer; use namada_core::types::masp::{ BalanceOwner, ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, @@ -899,40 +900,12 @@ impl ShieldedContext { // This should be a MASP over IBC transaction, it // could be a ShieldedTransfer or an Envelop // message, need to try both - let message = - namada_core::ledger::ibc::decode_message( + let shielded_transfer = + extract_payload_from_shielded_action::( &tx_data, + ExtractShieldedActionArg::Event(tx_event), ) - .map_err(|e| Error::Other(e.to_string()))?; - - let shielded_transfer = match message { - IbcMessage::ShieldedTransfer(msg) => { - msg.shielded_transfer - } - IbcMessage::Envelope(_) => { - tx_event - .attributes - .iter() - .find_map(|attribute| { - if attribute.key == "inner_tx" { - let tx_result = TxResult::from_str(&attribute.value).unwrap(); - for ibc_event in &tx_result.ibc_events { - - let event = namada_core::types::ibc::get_shielded_transfer(ibc_event).ok().flatten(); - if event.is_some() { - return event; - } - } - None - } else { - None - } - }).ok_or_else(|| Error::Other("Couldn't deserialize masp tx to ibc message envelope".to_string()))? - } - _ => { - return Err(Error::Other("Couldn't deserialize masp tx to a valid ibc message".to_string())); - } - }; + .await?; ( shielded_transfer.transfer, @@ -1561,56 +1534,15 @@ impl ShieldedContext { })?, Err(_) => { // Try Masp over IBC - - let message = - namada_core::ledger::ibc::decode_message(&tx_data) - .map_err(|e| Error::Other(e.to_string()))?; - - let shielded_transfer = match message { - IbcMessage::ShieldedTransfer(msg) => msg.shielded_transfer, - IbcMessage::Envelope(_) => { - // Need the tx event to extract the memo. The result is - // in the first index of the collection - let tx_events = get_indexed_masp_events_at_height( - client, - indexed_tx.height, - Some(indexed_tx.index), - ) - .await? - .ok_or_else(|| { - Error::Other(format!( - "Missing required ibc event at block height {}", - indexed_tx.height - )) - })?; - - tx_events[0].1 - .attributes - .iter() - .find_map(|attribute| { - if attribute.key == "inner_tx" { - let tx_result = TxResult::from_str(&attribute.value).unwrap(); - for ibc_event in &tx_result.ibc_events { - - let event = namada_core::types::ibc::get_shielded_transfer(ibc_event).ok().flatten(); - if event.is_some() { - return event; - } - } - None - } else { - None - } - }).ok_or_else(|| Error::Other("Couldn't deserialize masp tx to ibc message envelope".to_string()))? - } - _ => { - return Err(Error::Other( - "Couldn't deserialize masp tx to a valid ibc \ - message" - .to_string(), - )); - } - }; + let shielded_transfer = extract_payload_from_shielded_action( + &tx_data, + ExtractShieldedActionArg::Request(( + client, + indexed_tx.height, + Some(indexed_tx.index), + )), + ) + .await?; shielded_transfer.masp_tx } @@ -2336,6 +2268,86 @@ async fn get_indexed_masp_events_at_height( })) } +enum ExtractShieldedActionArg<'args, C: Client + Sync> { + Event(crate::tendermint::abci::Event), + Request((&'args C, BlockHeight, Option)), +} + +// Extract the Transfer and Transaction objects from a masp over ibc message +async fn extract_payload_from_shielded_action<'args, C: Client + Sync>( + tx_data: &[u8], + args: ExtractShieldedActionArg<'args, C>, +) -> Result { + let message = namada_core::ledger::ibc::decode_message(tx_data) + .map_err(|e| Error::Other(e.to_string()))?; + + let shielded_transfer = match message { + IbcMessage::ShieldedTransfer(msg) => msg.shielded_transfer, + IbcMessage::Envelope(_) => { + let tx_event = match args { + ExtractShieldedActionArg::Event(event) => event, + ExtractShieldedActionArg::Request((client, height, index)) => { + get_indexed_masp_events_at_height(client, height, index) + .await? + .ok_or_else(|| { + Error::Other(format!( + "Missing required ibc event at block height {}", + height + )) + })? + .first() + .ok_or_else(|| { + Error::Other(format!( + "Missing required ibc event at block height {}", + height + )) + })? + .1 + .to_owned() + } + }; + + tx_event + .attributes + .iter() + .find_map(|attribute| { + if attribute.key == "inner_tx" { + let tx_result = + TxResult::from_str(&attribute.value).unwrap(); + for ibc_event in &tx_result.ibc_events { + let event = + namada_core::types::ibc::get_shielded_transfer( + ibc_event, + ) + .ok() + .flatten(); + if event.is_some() { + return event; + } + } + None + } else { + None + } + }) + .ok_or_else(|| { + Error::Other( + "Couldn't deserialize masp tx to ibc message envelope" + .to_string(), + ) + })? + } + _ => { + return Err(Error::Other( + "Couldn't deserialize masp tx to a valid ibc message" + .to_string(), + )); + } + }; + + Ok(shielded_transfer) +} + mod tests { /// quick and dirty test. will fail on size check #[test] From 454e138f012e6c522277d3ac69b50f2a5763c982 Mon Sep 17 00:00:00 2001 From: satan Date: Wed, 10 Jan 2024 13:39:47 +0100 Subject: [PATCH 26/26] [feat] Refactoring extraction of shielded parts of txs to be more dry --- sdk/src/masp.rs | 243 +++++++++++++++++++++++------------------------- 1 file changed, 115 insertions(+), 128 deletions(-) diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 0cf8cb0752..a8eafc4d82 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -818,31 +818,102 @@ impl ShieldedContext { for (idx, tx_event) in txs_results { let tx = Tx::try_from(block[idx.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; + let (transfer, masp_transaction) = Self::extract_masp_tx( + &tx, + ExtractShieldedActionArg::Event::(tx_event), + true, + ) + .await?; - let tx_header = tx.header(); - // NOTE: simply looking for masp sections attached to the tx - // is not safe. We don't validate the sections attached to a - // transaction se we could end up with transactions carrying - // an unnecessary masp section. We must instead look for the - // required masp sections in the signed commitments (hashes) - // of the transactions' headers/data sections - let (transfer, masp_transaction) = if let Some(wrapper_header) = - tx_header.wrapper() - { - let hash = wrapper_header - .unshield_section_hash - .ok_or_else(|| { - Error::Other( - "Missing expected fee unshielding section hash" - .to_string(), - ) - })?; + // Collect the current transaction + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: idx, + }, + (epoch, transfer, masp_transaction), + ); + } + } + + Ok(shielded_txs) + } + /// Extract the relevant shield portions of a [`Tx`], if any. + async fn extract_masp_tx<'args, C: Client + Sync>( + tx: &Tx, + action_arg: ExtractShieldedActionArg<'args, C>, + check_header: bool, + ) -> Result<(Transfer, Transaction), Error> { + let maybe_transaction = if check_header { + let tx_header = tx.header(); + // NOTE: simply looking for masp sections attached to the tx + // is not safe. We don't validate the sections attached to a + // transaction se we could end up with transactions carrying + // an unnecessary masp section. We must instead look for the + // required masp sections in the signed commitments (hashes) + // of the transactions' headers/data sections + if let Some(wrapper_header) = tx_header.wrapper() { + let hash = + wrapper_header.unshield_section_hash.ok_or_else(|| { + Error::Other( + "Missing expected fee unshielding section hash" + .to_string(), + ) + })?; + + let masp_transaction = tx + .get_section(&hash) + .ok_or_else(|| { + Error::Other( + "Missing expected masp section".to_string(), + ) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other("Missing masp transaction".to_string()) + })?; + + // Transfer objects for fee unshielding are absent from + // the tx because they are completely constructed in + // protocol, need to recreate it here + let transfer = Transfer { + source: MASP, + target: wrapper_header.fee_payer(), + token: wrapper_header.fee.token.clone(), + amount: wrapper_header + .get_tx_fee() + .map_err(|e| Error::Other(e.to_string()))?, + key: None, + shielded: Some(hash), + }; + Some((transfer, masp_transaction)) + } else { + None + } + } else { + None + }; + + let tx = if let Some(tx) = maybe_transaction { + tx + } else { + // Expect decrypted transaction + let tx_data = tx.data().ok_or_else(|| { + Error::Other("Missing data section".to_string()) + })?; + match Transfer::try_from_slice(&tx_data) { + Ok(transfer) => { let masp_transaction = tx - .get_section(&hash) + .get_section(&transfer.shielded.ok_or_else(|| { + Error::Other( + "Missing masp section hash".to_string(), + ) + })?) .ok_or_else(|| { Error::Other( - "Missing expected masp section".to_string(), + "Missing masp section in transaction" + .to_string(), ) })? .masp_tx() @@ -850,83 +921,22 @@ impl ShieldedContext { Error::Other("Missing masp transaction".to_string()) })?; - // Transfer objects for fee unshielding are absent from - // the tx because they are completely constructed in - // protocol, need to recreate it here - let transfer = Transfer { - source: MASP, - target: wrapper_header.fee_payer(), - token: wrapper_header.fee.token.clone(), - amount: wrapper_header - .get_tx_fee() - .map_err(|e| Error::Other(e.to_string()))?, - key: None, - shielded: Some(hash), - }; - (transfer, masp_transaction) - } else { - // Expect decrypted transaction - let tx_data = tx.data().ok_or_else(|| { - Error::Other("Missing data section".to_string()) - })?; - match Transfer::try_from_slice(&tx_data) { - Ok(transfer) => { - let masp_transaction = tx - .get_section(&transfer.shielded.ok_or_else( - || { - Error::Other( - "Missing masp section hash" - .to_string(), - ) - }, - )?) - .ok_or_else(|| { - Error::Other( - "Missing masp section in transaction" - .to_string(), - ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other( - "Missing masp transaction".to_string(), - ) - })?; - - (transfer, masp_transaction) - } - Err(_) => { - // This should be a MASP over IBC transaction, it - // could be a ShieldedTransfer or an Envelop - // message, need to try both - let shielded_transfer = - extract_payload_from_shielded_action::( - &tx_data, - ExtractShieldedActionArg::Event(tx_event), - ) - .await?; - - ( - shielded_transfer.transfer, - shielded_transfer.masp_tx, - ) - } - } - }; - - // Collect the current transaction - shielded_txs.insert( - IndexedTx { - height: height.into(), - index: idx, - }, - (epoch, transfer, masp_transaction), - ); + } + Err(_) => { + // This should be a MASP over IBC transaction, it + // could be a ShieldedTransfer or an Envelop + // message, need to try both + let shielded_transfer = + extract_payload_from_shielded_action::( + &tx_data, action_arg, + ) + .await?; + (shielded_transfer.transfer, shielded_transfer.masp_tx) + } } - } - - Ok(shielded_txs) + }; + Ok(tx) } /// Applies the given transaction to the supplied context. More precisely, @@ -1514,39 +1524,16 @@ impl ShieldedContext { let tx = Tx::try_from(block[indexed_tx.index.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; - - let tx_data = tx - .data() - .ok_or_else(|| Error::Other("Missing data section".to_string()))?; - let shielded = match Transfer::try_from_slice(&tx_data) { - Ok(transfer) => tx - .get_section(&transfer.shielded.ok_or_else(|| { - Error::Other("Missing masp section hash".to_string()) - })?) - .ok_or_else(|| { - Error::Other( - "Missing masp section in transaction".to_string(), - ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other("Missing masp transaction".to_string()) - })?, - Err(_) => { - // Try Masp over IBC - let shielded_transfer = extract_payload_from_shielded_action( - &tx_data, - ExtractShieldedActionArg::Request(( - client, - indexed_tx.height, - Some(indexed_tx.index), - )), - ) - .await?; - - shielded_transfer.masp_tx - } - }; + let (_, shielded) = Self::extract_masp_tx( + &tx, + ExtractShieldedActionArg::Request(( + client, + indexed_tx.height, + Some(indexed_tx.index), + )), + false, + ) + .await?; // Accumulate the combined output note value into this Amount let mut val_acc = I128Sum::zero();