From e4ef149cecf2910ddb51940cf12afe3b880599ec Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 8 Aug 2022 11:26:13 -0500 Subject: [PATCH] update withdrawal tree nodes --- .github/workflows/ci.yml | 2 +- src/clarity_vm/withdrawal.rs | 239 +++++++++++++++++++++-------------- src/net/rpc.rs | 23 +++- 3 files changed, 167 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3693772bd..cd7f2188d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v3 with: repository: stacks-network/stacks-blockchain - ref: feat/configurable-epochs + ref: next - name: Build stacks-node 2.1 env: DOCKER_BUILDKIT: 1 diff --git a/src/clarity_vm/withdrawal.rs b/src/clarity_vm/withdrawal.rs index aff3df50c..15a14da17 100644 --- a/src/clarity_vm/withdrawal.rs +++ b/src/clarity_vm/withdrawal.rs @@ -1,4 +1,5 @@ use crate::chainstate::stacks::events::StacksTransactionReceipt; +use clarity::codec::StacksMessageCodec; use clarity::types::chainstate::{BlockHeaderHash, ConsensusHash, StacksBlockId, TrieHash}; use clarity::util::hash::{MerkleTree, Sha512Trunc256Sum}; use clarity::vm::database::ClarityBackingStore; @@ -6,30 +7,77 @@ use clarity::vm::events::{ FTEventType, FTWithdrawEventData, NFTEventType, NFTWithdrawEventData, STXEventType, STXWithdrawEventData, StacksTransactionEvent, }; -use clarity::vm::types::{AssetIdentifier, PrincipalData}; +use clarity::vm::types::{AssetIdentifier, PrincipalData, SequenceData, TupleData}; use clarity::vm::Value; use regex::internal::Input; -pub fn make_key_for_withdrawal( - data: String, - recipient: &PrincipalData, - withdrawal_id: u32, - block_height: u64, -) -> String { - format!( - "withdrawal::{}::{}::{}::{}", - block_height, - data, - recipient.to_string(), - withdrawal_id, - ) +fn clarity_ascii_str(input: &str) -> Value { + Value::string_ascii_from_bytes(input.as_bytes().to_vec()).expect("Supplied string was not ASCII") } pub fn buffer_from_hash(hash: Sha512Trunc256Sum) -> Value { Value::buff_from(hash.0.to_vec()).expect("Failed to construct buffer from hash") } -pub fn make_key_for_ft_withdrawal_event(data: &FTWithdrawEventData, block_height: u64) -> String { + +/// The supplied withdrawal ID is inserted into the supplied withdraw event +/// (this is why the event are supplied as a mutable argument). +/// +/// The format for withdrawal event keys is that each key in the +/// Merkle withdrawal tree is the consensus serialization of one of the following +/// tuples: +/// +/// ``` +/// { type: "stx", +/// block-height: u128, +/// withdrawal-id: u128, +/// recipient: principal, +/// amount: u128 } +/// ``` +/// +/// ``` +/// { type: "nft", +/// asset-contract: principal, +/// asset-name: utf8, +/// block-height: u128, +/// withdrawal-id: u128, +/// recipient: principal, +/// nft-id: u128 } +/// ``` +/// +/// ``` +/// { type: "ft", +/// asset-contract: principal, +/// asset-name: utf8, +/// block-height: u128, +/// withdrawal-id: u128, +/// recipient: principal, +/// amount: u128 } +/// ``` + +pub fn generate_key_from_event( + event: &mut StacksTransactionEvent, + withdrawal_id: u32, + block_height: u64, +) -> Option { + match event { + StacksTransactionEvent::NFTEvent(NFTEventType::NFTWithdrawEvent(data)) => { + data.withdrawal_id = Some(withdrawal_id); + make_key_for_nft_withdrawal_event(data, block_height) + } + StacksTransactionEvent::FTEvent(FTEventType::FTWithdrawEvent(data)) => { + data.withdrawal_id = Some(withdrawal_id); + make_key_for_ft_withdrawal_event(data, block_height) + } + StacksTransactionEvent::STXEvent(STXEventType::STXWithdrawEvent(data)) => { + data.withdrawal_id = Some(withdrawal_id); + Some(make_key_for_stx_withdrawal_event(data, block_height)) + } + _ => None, + } +} + +pub fn make_key_for_ft_withdrawal_event(data: &FTWithdrawEventData, block_height: u64) -> Option { let withdrawal_id = data .withdrawal_id .expect("Tried to serialize a withdraw event before setting withdrawal ID"); @@ -40,6 +88,7 @@ pub fn make_key_for_ft_withdrawal_event(data: &FTWithdrawEventData, block_height "withdrawal_id" => withdrawal_id, "amount" => %data.amount, "asset_id" => %data.asset_identifier); + make_key_for_ft_withdrawal( &data.sender, withdrawal_id, @@ -49,7 +98,7 @@ pub fn make_key_for_ft_withdrawal_event(data: &FTWithdrawEventData, block_height ) } -pub fn make_key_for_nft_withdrawal_event(data: &NFTWithdrawEventData, block_height: u64) -> String { +pub fn make_key_for_nft_withdrawal_event(data: &NFTWithdrawEventData, block_height: u64) -> Option { let withdrawal_id = data .withdrawal_id .expect("Tried to serialize a withdraw event before setting withdrawal ID"); @@ -68,7 +117,7 @@ pub fn make_key_for_nft_withdrawal_event(data: &NFTWithdrawEventData, block_heig ) } -pub fn make_key_for_stx_withdrawal_event(data: &STXWithdrawEventData, block_height: u64) -> String { +pub fn make_key_for_stx_withdrawal_event(data: &STXWithdrawEventData, block_height: u64) -> Value { let withdrawal_id = data .withdrawal_id .expect("Tried to serialize a withdraw event before setting withdrawal ID"); @@ -81,15 +130,52 @@ pub fn make_key_for_stx_withdrawal_event(data: &STXWithdrawEventData, block_heig make_key_for_stx_withdrawal(&data.sender, withdrawal_id, data.amount, block_height) } +pub fn make_key_for_stx_withdrawal( + recipient: &PrincipalData, + withdrawal_id: u32, + amount: u128, + block_height: u64, +) -> Value { + TupleData::from_data( + vec![ + ("type".into(), clarity_ascii_str("stx")), + ("block-height".into(), Value::UInt(u128::from(block_height))), + ("withdrawal-id".into(), Value::UInt(u128::from(withdrawal_id))), + ("recipient".into(), Value::Principal(recipient.clone())), + ("amount".into(), Value::UInt(amount)), + ] + ).expect("Withdrawal key tuple is too large for Clarity").into() +} + pub fn make_key_for_nft_withdrawal( sender: &PrincipalData, withdrawal_id: u32, asset_identifier: &AssetIdentifier, id: u128, block_height: u64, -) -> String { - let str_data = format!("nft::{}::{}", asset_identifier, id); - make_key_for_withdrawal(str_data, sender, withdrawal_id, block_height) +) -> Option { + let asset_contract = Value::Principal(PrincipalData::from(asset_identifier.contract_identifier.clone())); + let asset_name = Value::string_utf8_from_bytes(asset_identifier.asset_name.as_bytes().to_vec()); + let asset_name = match asset_name { + Ok(x) => x, + Err(e) => { + warn!("Failed to create FT withdrawal entry: asset name is non-UTF8"; + "asset_identifier" => %asset_identifier, + "err" => ?e); + return None + } + }; + Some(TupleData::from_data( + vec![ + ("type".into(), clarity_ascii_str("nft")), + ("asset-contract".into(), asset_contract), + ("asset-name".into(), asset_name), + ("block-height".into(), Value::UInt(u128::from(block_height))), + ("withdrawal-id".into(), Value::UInt(u128::from(withdrawal_id))), + ("recipient".into(), Value::Principal(sender.clone())), + ("nft-id".into(), Value::UInt(id)), + ] + ).expect("Withdrawal key tuple is too large for Clarity").into()) } pub fn make_key_for_ft_withdrawal( @@ -98,47 +184,33 @@ pub fn make_key_for_ft_withdrawal( asset_identifier: &AssetIdentifier, amount: u128, block_height: u64, -) -> String { - let str_data = format!("ft::{}::{}", asset_identifier, amount); - make_key_for_withdrawal(str_data, sender, withdrawal_id, block_height) +) -> Option { + let asset_contract = Value::Principal(PrincipalData::from(asset_identifier.contract_identifier.clone())); + let asset_name = Value::string_utf8_from_bytes(asset_identifier.asset_name.as_bytes().to_vec()); + let asset_name = match asset_name { + Ok(x) => x, + Err(e) => { + warn!("Failed to create FT withdrawal entry: asset name is non-UTF8"; + "asset_identifier" => %asset_identifier, + "err" => ?e); + return None + } + }; + Some(TupleData::from_data( + vec![ + ("type".into(), clarity_ascii_str("ft")), + ("asset-contract".into(), asset_contract), + ("asset-name".into(), asset_name), + ("block-height".into(), Value::UInt(u128::from(block_height))), + ("withdrawal-id".into(), Value::UInt(u128::from(withdrawal_id))), + ("recipient".into(), Value::Principal(sender.clone())), + ("amount".into(), Value::UInt(amount)), + ] + ).expect("Withdrawal key tuple is too large for Clarity").into()) } -pub fn make_key_for_stx_withdrawal( - sender: &PrincipalData, - withdrawal_id: u32, - amount: u128, - block_height: u64, -) -> String { - let str_data = format!("stx::{}", amount); - make_key_for_withdrawal(str_data, sender, withdrawal_id, block_height) -} - -/// The supplied withdrawal ID is inserted into the supplied withdraw event -/// (this is why the event are supplied as a mutable argument). -pub fn generate_key_from_event( - event: &mut StacksTransactionEvent, - withdrawal_id: u32, - block_height: u64, -) -> Option { - match event { - StacksTransactionEvent::NFTEvent(NFTEventType::NFTWithdrawEvent(data)) => { - data.withdrawal_id = Some(withdrawal_id); - Some(make_key_for_nft_withdrawal_event(data, block_height)) - } - StacksTransactionEvent::FTEvent(FTEventType::FTWithdrawEvent(data)) => { - data.withdrawal_id = Some(withdrawal_id); - Some(make_key_for_ft_withdrawal_event(data, block_height)) - } - StacksTransactionEvent::STXEvent(STXEventType::STXWithdrawEvent(data)) => { - data.withdrawal_id = Some(withdrawal_id); - Some(make_key_for_stx_withdrawal_event(data, block_height)) - } - _ => None, - } -} - -pub fn convert_withdrawal_key_to_bytes(key: &str) -> Vec { - key.as_bytes().to_vec() +pub fn convert_withdrawal_key_to_bytes(key: &Value) -> Vec { + key.serialize_to_vec() } /// The order of withdrawal events in the transaction receipts will determine the withdrawal IDs @@ -154,15 +226,12 @@ pub fn generate_withdrawal_keys( for event in receipt.events.iter_mut() { if let Some(key) = generate_key_from_event(event, withdrawal_id, block_height) { withdrawal_id += 1; - items.push(key); + items.push(convert_withdrawal_key_to_bytes(&key)); } } } items - .iter() - .map(|item: &String| convert_withdrawal_key_to_bytes(item)) - .collect() } /// Put all withdrawal keys and values into a single Merkle tree. @@ -181,6 +250,8 @@ pub fn create_withdrawal_merkle_tree( #[cfg(test)] mod test { + use clarity::util::hash::to_hex; + use crate::chainstate::stacks::events::{StacksTransactionReceipt, TransactionOrigin}; use crate::chainstate::stacks::{ CoinbasePayload, StacksTransaction, TransactionAuth, TransactionPayload, @@ -281,11 +352,8 @@ mod test { let stx_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash(stx_withdrawal_key_bytes.as_slice()); assert_eq!( - stx_withdrawal_leaf_hash.as_bytes().to_vec(), - vec![ - 172, 139, 11, 211, 5, 246, 229, 87, 32, 65, 240, 19, 169, 240, 51, 242, 145, 194, - 35, 50, 110, 250, 125, 182, 250, 233, 86, 22, 132, 34, 54, 87 - ] + to_hex(stx_withdrawal_leaf_hash.as_bytes()), + "ac19cdbd2ba696e608a290fac03d9c6990f76d2681dba17a08f8cf9e41577be0", ); let ft_withdrawal_key = generate_key_from_event(&mut ft_withdraw_event, 1, 0).unwrap(); @@ -293,11 +361,8 @@ mod test { let ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash(ft_withdrawal_key_bytes.as_slice()); assert_eq!( - ft_withdrawal_leaf_hash.as_bytes().to_vec(), - vec![ - 150, 1, 86, 186, 197, 139, 77, 69, 128, 76, 166, 52, 11, 95, 39, 180, 157, 104, 85, - 120, 195, 235, 158, 52, 62, 173, 45, 78, 125, 176, 10, 181 - ] + to_hex(ft_withdrawal_leaf_hash.as_bytes()), + "30747fd7af1ef218d55509f40cf34afb098c5372dada8a7cd20850f445dcf781", ); let nft_withdrawal_key = generate_key_from_event(&mut nft_withdraw_event, 2, 0).unwrap(); @@ -305,11 +370,8 @@ mod test { let nft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash(nft_withdrawal_key_bytes.as_slice()); assert_eq!( - nft_withdrawal_leaf_hash.as_bytes().to_vec(), - vec![ - 192, 142, 158, 115, 44, 43, 219, 156, 110, 47, 200, 153, 159, 226, 157, 254, 243, - 156, 131, 240, 141, 179, 228, 34, 45, 250, 148, 88, 182, 39, 52, 121 - ] + to_hex(nft_withdrawal_leaf_hash.as_bytes()), + "ae4ca9344bd846cd3e5c34021e91d578aee6baf70043075b2637289d1090a46b", ); let first_level_first_node = MerkleTree::::get_node_hash( @@ -317,22 +379,16 @@ mod test { &ft_withdrawal_leaf_hash, ); assert_eq!( - first_level_first_node.as_bytes().to_vec(), - vec![ - 33, 59, 115, 31, 248, 41, 193, 100, 153, 181, 12, 29, 119, 128, 236, 142, 63, 0, - 103, 65, 45, 101, 65, 135, 50, 36, 6, 86, 23, 242, 101, 86 - ] + to_hex(first_level_first_node.as_bytes()), + "0ce53ee167f74b784b6ce98284c7d4199f519a53a82495f8ba41d951091d4e1e", ); let first_level_second_node = MerkleTree::::get_node_hash( &nft_withdrawal_leaf_hash, &nft_withdrawal_leaf_hash, ); assert_eq!( - first_level_second_node.as_bytes().to_vec(), - vec![ - 143, 101, 57, 194, 25, 34, 84, 26, 183, 99, 126, 86, 66, 243, 83, 115, 185, 210, - 20, 198, 46, 34, 238, 63, 37, 111, 253, 68, 89, 39, 195, 245 - ] + to_hex(first_level_second_node.as_bytes()), + "45a070a5f00300efa4f0a90ba3a9835899db27da972420c27f080f09e2ed506b" ); let calculated_root_hash = MerkleTree::::get_node_hash( @@ -340,11 +396,8 @@ mod test { &first_level_second_node, ); assert_eq!( - calculated_root_hash.as_bytes().to_vec(), - vec![ - 125, 119, 23, 55, 33, 63, 210, 96, 23, 203, 132, 126, 25, 3, 23, 127, 138, 157, - 172, 210, 18, 252, 170, 7, 192, 175, 57, 178, 110, 63, 246, 49 - ] + to_hex(calculated_root_hash.as_bytes()), + "7d7e8202df32ebfb4fa510a7ddb856ca5a234540901fb15428a930da2c6ce102", ); assert_eq!(root_hash, calculated_root_hash); } diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 1bc7fe594..48e9ca501 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1032,6 +1032,22 @@ impl ConversationHttp { id, requested_block_height, ); + let withdrawal_key = match withdrawal_key { + Some(x) => x, + None => { + let response_metadata = + HttpResponseMetadata::from_http_request_type(req, Some(canonical_stacks_tip_height)); + + return HttpResponseType::BadRequestJSON( + response_metadata, + json!({ + "error": "Withdrawal NFT contract uses invalid NFT asset name" + }), + ) + .send(http, fd) + .map(|_| ()) + } + }; Self::handle_get_generic_withdrawal_entry( http, fd, @@ -1051,11 +1067,12 @@ impl ConversationHttp { chainstate: &mut StacksChainState, canonical_tip: &StacksBlockId, requested_block_height: u64, - withdrawal_key: String, + withdrawal_key: Value, canonical_stacks_tip_height: u64, ) -> Result<(), net_error> { let response_metadata = HttpResponseMetadata::from_http_request_type(req, Some(canonical_stacks_tip_height)); + let withdrawal_key_bytes = withdrawal_key.serialize_to_vec(); let requested_block = match chainstate .index_conn() @@ -1092,7 +1109,7 @@ impl ConversationHttp { } }; - let merkle_path = match withdrawal_tree.path(withdrawal_key.as_bytes()) { + let merkle_path = match withdrawal_tree.path(&withdrawal_key_bytes) { Some(path) => path, None => { return HttpResponseType::NotFound( @@ -1141,7 +1158,7 @@ impl ConversationHttp { let withdrawal_root = withdrawal::buffer_from_hash(withdrawal_tree.root()); let withdrawal_leaf_hash = withdrawal::buffer_from_hash( - MerkleTree::::get_leaf_hash(withdrawal_key.as_bytes()), + MerkleTree::::get_leaf_hash(&withdrawal_key_bytes), ); let response = WithdrawalResponse {