From 048d80b5c619b91213dbd0fed43700cfcc3ae4de Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Wed, 8 Feb 2023 13:58:06 -0500 Subject: [PATCH 01/12] feat(rpc): Add RPC endpoint for Fungible Tokens --- src/net/http.rs | 68 +++++++++++ src/net/mod.rs | 8 ++ src/net/rpc.rs | 67 +++++++++++ .../stacks-node/src/tests/l1_observer_test.rs | 109 +++++++++++++++++- .../src/tests/neon_integrations.rs | 34 ++++++ 5 files changed, 285 insertions(+), 1 deletion(-) diff --git a/src/net/http.rs b/src/net/http.rs index 88d27058a..24f9e5965 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1859,6 +1859,50 @@ impl HttpRequestType { }) } + fn parse_get_ft_withdrawal( + _protocol: &mut StacksHttp, + preamble: &HttpRequestPreamble, + captures: &Captures, + _query: Option<&str>, + _fd: &mut R, + ) -> Result { + if preamble.get_content_length() != 0 { + return Err(net_error::DeserializeError( + "Invalid Http request: expected 0-length body for GetAccount".to_string(), + )); + } + + let sender = PrincipalData::parse(&captures["sender"]).map_err(|_e| { + net_error::DeserializeError("Failed to parse account principal".into()) + })?; + + let withdraw_block_height = u64::from_str(&captures["block_height"]) + .map_err(|_e| net_error::DeserializeError("Failed to parse block height".into()))?; + + let withdrawal_id = u32::from_str(&captures["withdrawal_id"]) + .map_err(|_e| net_error::DeserializeError("Failed to parse block height".into()))?; + let contract_addr = + StacksAddress::from_string(&captures["contract_address"]).ok_or_else(|| { + net_error::DeserializeError("Failed to parse contract address".into()) + })?; + let contract_name = ContractName::try_from(captures["contract_name"].to_string()) + .map_err(|_e| net_error::DeserializeError("Failed to parse contract name".into()))?; + let id = u128::from_str(&captures["id"]) + .map_err(|_e| net_error::DeserializeError("Failed to parse amount".into()))?; + + Ok(HttpRequestType::GetWithdrawalFt { + metadata: HttpRequestMetadata::from_preamble(preamble), + withdraw_block_height, + sender, + withdrawal_id, + contract_identifier: QualifiedContractIdentifier::new( + contract_addr.into(), + contract_name, + ), + id, + }) + } + fn parse_get_nft_withdrawal( _protocol: &mut StacksHttp, preamble: &HttpRequestPreamble, @@ -2818,6 +2862,7 @@ impl HttpRequestType { HttpRequestType::ClientError(ref md, ..) => md, HttpRequestType::GetWithdrawalStx { ref metadata, .. } => metadata, HttpRequestType::BlockProposal(ref metadata, ..) => metadata, + HttpRequestType::GetWithdrawalFt { ref metadata, .. } => metadata, HttpRequestType::GetWithdrawalNft { ref metadata, .. } => metadata, } } @@ -2853,6 +2898,9 @@ impl HttpRequestType { HttpRequestType::GetWithdrawalStx { ref mut metadata, .. } => metadata, + HttpRequestType::GetWithdrawalFt { + ref mut metadata, .. + } => metadata, HttpRequestType::GetWithdrawalNft { ref mut metadata, .. } => metadata, @@ -3035,6 +3083,22 @@ impl HttpRequestType { withdraw_block_height, sender, withdrawal_id, amount ), HttpRequestType::BlockProposal(..) => self.get_path().to_string(), + HttpRequestType::GetWithdrawalFt { + metadata: _, + withdraw_block_height, + sender, + withdrawal_id, + contract_identifier, + id, + } => format!( + "/v2/withdrawal/ft/{}/{}/{}/{}/{}/{}", + withdraw_block_height, + sender, + withdrawal_id, + StacksAddress::from(contract_identifier.issuer.clone()), + contract_identifier.name.as_str(), + id + ), HttpRequestType::GetWithdrawalNft { metadata: _, withdraw_block_height, @@ -3090,6 +3154,9 @@ impl HttpRequestType { "/v2/withdrawal/stx/:block-height/:sender/:withdrawal_id/:amount" } HttpRequestType::BlockProposal(..) => PATH_STR_POST_BLOCK_PROPOSAL, + HttpRequestType::GetWithdrawalFt { .. } => { + "/v2/withdrawal/ft/:block-height/:sender/:withdrawal_id/:contract_address/:contract_name/:id" + } HttpRequestType::GetWithdrawalNft { .. } => { "/v2/withdrawal/nft/:block-height/:sender/:withdrawal_id/:contract_address/:contract_name/:id" } @@ -4624,6 +4691,7 @@ impl MessageSequence for StacksHttpMessage { HttpRequestType::FeeRateEstimate(_, _, _) => "HTTP(FeeRateEstimate)", HttpRequestType::GetWithdrawalStx { .. } => "HTTP(GetWithdrawalStx)", HttpRequestType::BlockProposal(_, _) => "HTTP(BlockProposal)", + HttpRequestType::GetWithdrawalFt { .. } => "HTTP(GetWithdrawalFt)", HttpRequestType::GetWithdrawalNft { .. } => "HTTP(GetWithdrawalNft)", }, StacksHttpMessage::Response(ref res) => match res { diff --git a/src/net/mod.rs b/src/net/mod.rs index 4d975bd1a..205318911 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1441,6 +1441,14 @@ pub enum HttpRequestType { withdrawal_id: u32, amount: u128, }, + GetWithdrawalFt { + metadata: HttpRequestMetadata, + withdraw_block_height: u64, + sender: PrincipalData, + withdrawal_id: u32, + contract_identifier: QualifiedContractIdentifier, + id: u128, + }, GetWithdrawalNft { metadata: HttpRequestMetadata, withdraw_block_height: u64, diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 0e2f0d562..c638e188a 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1045,6 +1045,38 @@ impl ConversationHttp { ) } + fn handle_get_withdrawal_ft_entry( + http: &mut StacksHttp, + fd: &mut W, + req: &HttpRequestType, + chainstate: &mut StacksChainState, + canonical_tip: &StacksBlockId, + requested_block_height: u64, + sender: &PrincipalData, + withdrawal_id: u32, + contract_identifier: &QualifiedContractIdentifier, + id: u128, + canonical_stacks_tip_height: u64, + ) -> Result<(), net_error> { + let withdrawal_key = withdrawal::make_key_for_ft_withdrawal( + sender, + withdrawal_id, + contract_identifier, + id, + requested_block_height, + ); + Self::handle_get_generic_withdrawal_entry( + http, + fd, + req, + chainstate, + canonical_tip, + requested_block_height, + withdrawal_key, + canonical_stacks_tip_height, + ) + } + fn handle_get_withdrawal_nft_entry( http: &mut StacksHttp, fd: &mut W, @@ -2855,6 +2887,41 @@ impl ConversationHttp { )?; None } + + HttpRequestType::GetWithdrawalFt { + withdraw_block_height, + ref sender, + withdrawal_id, + id, + ref contract_identifier, + .. + } => { + if let Some(tip) = ConversationHttp::handle_load_stacks_chain_tip( + &mut self.connection.protocol, + &mut reply, + &req, + &TipRequest::UseLatestAnchoredTip, + sortdb, + chainstate, + network.burnchain_tip.canonical_stacks_tip_height, + )? { + ConversationHttp::handle_get_withdrawal_ft_entry( + &mut self.connection.protocol, + &mut reply, + &req, + chainstate, + &tip, + withdraw_block_height, + &sender.clone(), + withdrawal_id, + contract_identifier, + id, + network.burnchain_tip.canonical_stacks_tip_height, + )?; + } + None + } + HttpRequestType::GetWithdrawalNft { withdraw_block_height, ref sender, diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 63b43397b..89ceb7335 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -5,7 +5,7 @@ use std::thread::{self, JoinHandle}; use crate::config::{EventKeyType, EventObserverConfig}; use crate::tests::l1_multiparty::MOCKNET_EPOCH_2_1; use crate::tests::neon_integrations::{ - filter_map_events, get_account, get_nft_withdrawal_entry, get_withdrawal_entry, submit_tx, + filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, get_withdrawal_entry, submit_tx, test_observer, }; use crate::tests::{make_contract_call, make_contract_publish, to_addr}; @@ -1010,6 +1010,15 @@ fn l1_deposit_and_withdraw_asset_integration_test() { 1, ); + let ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + withdrawal_id, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-nft")), + 1, + ); + // Create the withdrawal merkle tree by mocking the ft & nft withdraw event (if the root hash of // this constructed merkle tree is not identical to the root hash published by the subnet node, // then the test will fail). @@ -1153,6 +1162,19 @@ fn l1_deposit_and_withdraw_asset_integration_test() { "Sibling hashes should match value returned via RPC" ); + assert_eq!( + &root_hash_val, &ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &leaf_hash_val, &ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &siblings_val, &ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); + // TODO: call withdraw from unauthorized principal once leaf verification is added to the subnet contract let l1_withdraw_ft_tx = make_contract_call( @@ -2283,6 +2305,23 @@ fn nft_deposit_and_withdraw_integration_test() { 5, ); + let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 0, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 1, + ); + let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 1, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 5, + ); + // Create the withdrawal merkle tree by mocking both nft withdraw events (if the root hash of // this constructed merkle tree is not identical to the root hash published by the subnet node, // then the test will fail). @@ -2412,6 +2451,18 @@ fn nft_deposit_and_withdraw_integration_test() { &l1_native_siblings_val, &l1_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); + assert_eq!( + &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); let subnet_native_nft_withdrawal_key = generate_key_from_event(&mut subnet_native_nft_withdraw_event, 1, withdrawal_height) @@ -2427,6 +2478,20 @@ fn nft_deposit_and_withdraw_integration_test() { .path(&subnet_native_nft_withdrawal_key_bytes) .unwrap(); + let subnet_native_ft_withdrawal_key = + generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) + .unwrap(); + let subnet_native_ft_withdrawal_key_bytes = + convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); + let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( + subnet_native_ft_withdrawal_key_bytes.as_slice(), + ) + .as_bytes() + .to_vec(); + let subnet_native_ft_path = withdrawal_tree + .path(&subnet_native_ft_withdrawal_key_bytes) + .unwrap(); + let mut subnet_native_nft_sib_data = Vec::new(); for (_i, sib) in subnet_native_nft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2439,6 +2504,18 @@ fn nft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_nft_sib_data.push(sib_tuple); } + let mut subnet_native_ft_sib_data = Vec::new(); + for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + subnet_native_ft_sib_data.push(sib_tuple); + } let subnet_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); let subnet_native_leaf_hash_val = @@ -2457,6 +2534,36 @@ fn nft_deposit_and_withdraw_integration_test() { &subnet_native_siblings_val, &subnet_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); + + let mut subnet_native_ft_sib_data = Vec::new(); + for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + subnet_native_ft_sib_data.push(sib_tuple); + } + + let subnet_native_leaf_hash_val = + Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); + let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); + + assert_eq!( + &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); // TODO: call withdraw from unauthorized principal once leaf verification is added to the subnet contract diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 1c6531706..bc4c43c8d 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -708,6 +708,40 @@ pub fn get_withdrawal_entry( } } +pub fn get_ft_withdrawal_entry( + http_origin: &str, + block_height: u64, + sender: F, + withdrawal_id: u64, + contract_identifier: QualifiedContractIdentifier, + id: u64, +) -> WithdrawalEntry { + let client = reqwest::blocking::Client::new(); + let path = format!( + "{}/v2/withdrawal/ft/{}/{}/{}/{}/{}/{}", + http_origin, + block_height, + sender, + withdrawal_id, + StacksAddress::from(contract_identifier.issuer), + contract_identifier.name.as_str(), + id + ); + + let res = client + .get(&path) + .send() + .unwrap() + .json::() + .unwrap(); + info!("Withdrawal response: {:#?}", res); + WithdrawalEntry { + leaf_hash: ClarityValue::try_deserialize_hex_untyped(&res.withdrawal_leaf_hash).unwrap(), + root_hash: ClarityValue::try_deserialize_hex_untyped(&res.withdrawal_root).unwrap(), + siblings: ClarityValue::try_deserialize_hex_untyped(&res.sibling_hashes).unwrap(), + } +} + pub fn get_nft_withdrawal_entry( http_origin: &str, block_height: u64, From eb0ab3bdc0f6bf63821e18e0358b986a06dce77e Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Fri, 17 Feb 2023 10:42:46 -0500 Subject: [PATCH 02/12] fix: Broken unit test introduced by previous commit --- .../stacks-node/src/tests/l1_observer_test.rs | 111 +++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 89ceb7335..740608659 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -2404,10 +2404,86 @@ fn nft_deposit_and_withdraw_integration_test() { microblock_header: None, tx_index: 0, }; + let mut l1_native_ft_withdraw_event = + StacksTransactionEvent::SmartContractEvent(SmartContractEventData { + key: (boot_code_id("subnet".into(), false), "print".into()), + value: Value::Tuple( + TupleData::from_data(vec![ + ( + "type".into(), + Value::string_ascii_from_bytes("nft".to_string().into_bytes()).unwrap(), + ), + ( + "asset-contract".into(), + Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ), + )), + ), + ( + "sender".into(), + Value::Principal(PrincipalData::Standard(user_addr.into())), + ), + ("id".into(), Value::UInt(11)), + ]) + .expect("Failed to create tuple data."), + ), + }); + let mut subnet_native_ft_withdraw_event = + StacksTransactionEvent::SmartContractEvent(SmartContractEventData { + key: (boot_code_id("subnet".into(), false), "print".into()), + value: Value::Tuple( + TupleData::from_data(vec![ + ( + "type".into(), + Value::string_ascii_from_bytes("nft".to_string().into_bytes()).unwrap(), + ), + ( + "asset-contract".into(), + Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ), + )), + ), + ( + "sender".into(), + Value::Principal(PrincipalData::Standard(user_addr.into())), + ), + ("id".into(), Value::UInt(15)), + ]) + .expect("Failed to create tuple data."), + ), + }); + let withdrawal_receipt_ft = StacksTransactionReceipt { + transaction: TransactionOrigin::Stacks(StacksTransaction::new( + TransactionVersion::Testnet, + auth.clone(), + TransactionPayload::Coinbase(CoinbasePayload([0u8; 32])), + )), + events: vec![ + l1_native_ft_withdraw_event.clone(), + subnet_native_ft_withdraw_event.clone(), + ], + post_condition_aborted: false, + result: Value::err_none(), + stx_burned: 0, + contract_analysis: None, + execution_cost: ExecutionCost::zero(), + microblock_header: None, + tx_index: 0, + }; let withdrawal_tree = create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt], withdrawal_height); let root_hash = withdrawal_tree.root().as_bytes().to_vec(); + let withdrawal_tree_ft = + create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt_ft], withdrawal_height); + let root_hash_ft = withdrawal_tree_ft.root().as_bytes().to_vec(); + let l1_native_nft_withdrawal_key = generate_key_from_event(&mut l1_native_nft_withdraw_event, 0, withdrawal_height).unwrap(); let l1_native_nft_withdrawal_key_bytes = @@ -2421,6 +2497,19 @@ fn nft_deposit_and_withdraw_integration_test() { .path(&l1_native_nft_withdrawal_key_bytes) .unwrap(); + let l1_native_ft_withdrawal_key = + generate_key_from_event(&mut l1_native_ft_withdraw_event, 0, withdrawal_height).unwrap(); + let l1_native_ft_withdrawal_key_bytes = + convert_withdrawal_key_to_bytes(&l1_native_ft_withdrawal_key); + let l1_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( + l1_native_ft_withdrawal_key_bytes.as_slice(), + ) + .as_bytes() + .to_vec(); + let l1_native_ft_path = withdrawal_tree + .path(&l1_native_ft_withdrawal_key_bytes) + .unwrap(); + let mut l1_native_nft_sib_data = Vec::new(); for (_i, sib) in l1_native_nft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2451,6 +2540,25 @@ fn nft_deposit_and_withdraw_integration_test() { &l1_native_siblings_val, &l1_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); + + let mut l1_native_ft_sib_data = Vec::new(); + for (_i, sib) in l1_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + l1_native_ft_sib_data.push(sib_tuple); + } + + let l1_native_root_hash_val = Value::buff_from(root_hash_ft.clone()).unwrap(); + let l1_native_leaf_hash_val = + Value::buff_from(l1_native_ft_withdrawal_leaf_hash.clone()).unwrap(); + let l1_native_siblings_val = Value::list_from(l1_native_ft_sib_data.clone()).unwrap(); + assert_eq!( &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, "Root hash should match value returned via RPC" @@ -2464,6 +2572,7 @@ fn nft_deposit_and_withdraw_integration_test() { "Sibling hashes should match value returned via RPC" ); + let subnet_native_nft_withdrawal_key = generate_key_from_event(&mut subnet_native_nft_withdraw_event, 1, withdrawal_height) .unwrap(); @@ -2547,7 +2656,7 @@ fn nft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_ft_sib_data.push(sib_tuple); } - + let subnet_native_leaf_hash_val = Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); From 05f773ed382d7b84e6cf26f8410e2697ef7e94c9 Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Fri, 17 Feb 2023 10:45:16 -0500 Subject: [PATCH 03/12] fix: code formatting --- testnet/stacks-node/src/tests/l1_observer_test.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 740608659..c2c8339c1 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -5,8 +5,8 @@ use std::thread::{self, JoinHandle}; use crate::config::{EventKeyType, EventObserverConfig}; use crate::tests::l1_multiparty::MOCKNET_EPOCH_2_1; use crate::tests::neon_integrations::{ - filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, get_withdrawal_entry, submit_tx, - test_observer, + filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, + get_withdrawal_entry, submit_tx, test_observer, }; use crate::tests::{make_contract_call, make_contract_publish, to_addr}; use crate::{neon, Config}; @@ -2504,8 +2504,8 @@ fn nft_deposit_and_withdraw_integration_test() { let l1_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( l1_native_ft_withdrawal_key_bytes.as_slice(), ) - .as_bytes() - .to_vec(); + .as_bytes() + .to_vec(); let l1_native_ft_path = withdrawal_tree .path(&l1_native_ft_withdrawal_key_bytes) .unwrap(); @@ -2572,7 +2572,6 @@ fn nft_deposit_and_withdraw_integration_test() { "Sibling hashes should match value returned via RPC" ); - let subnet_native_nft_withdrawal_key = generate_key_from_event(&mut subnet_native_nft_withdraw_event, 1, withdrawal_height) .unwrap(); @@ -2643,7 +2642,7 @@ fn nft_deposit_and_withdraw_integration_test() { &subnet_native_siblings_val, &subnet_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - + let mut subnet_native_ft_sib_data = Vec::new(); for (_i, sib) in subnet_native_ft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); From e0229bb9d73bacefde7f2ec7daacd5f2a528a1a8 Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Sat, 18 Feb 2023 12:26:29 -0500 Subject: [PATCH 04/12] Update src/net/http.rs Co-authored-by: Brice Dobry --- src/net/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/http.rs b/src/net/http.rs index 24f9e5965..fcec1de74 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1868,7 +1868,7 @@ impl HttpRequestType { ) -> Result { if preamble.get_content_length() != 0 { return Err(net_error::DeserializeError( - "Invalid Http request: expected 0-length body for GetAccount".to_string(), + "Invalid Http request: expected 0-length body for GetNFTWithdrawal".to_string(), )); } From 500c751f96b7f2e4c161a67e79fc974b3a346b58 Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Sat, 18 Feb 2023 12:29:08 -0500 Subject: [PATCH 05/12] fix: Use the correct API name for error messages --- src/net/http.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net/http.rs b/src/net/http.rs index fcec1de74..59e0dfffe 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1868,7 +1868,7 @@ impl HttpRequestType { ) -> Result { if preamble.get_content_length() != 0 { return Err(net_error::DeserializeError( - "Invalid Http request: expected 0-length body for GetNFTWithdrawal".to_string(), + "Invalid Http request: expected 0-length body for GetFTWithdrawal".to_string(), )); } @@ -1912,7 +1912,7 @@ impl HttpRequestType { ) -> Result { if preamble.get_content_length() != 0 { return Err(net_error::DeserializeError( - "Invalid Http request: expected 0-length body for GetAccount".to_string(), + "Invalid Http request: expected 0-length body for GetNFTWithdrawal".to_string(), )); } From 8b6250e4acbd8ed1d50171c247f7206d81a2422b Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Tue, 21 Feb 2023 04:41:47 -0500 Subject: [PATCH 06/12] Undo L1 test changes --- .../stacks-node/src/tests/l1_observer_test.rs | 116 +----------------- 1 file changed, 4 insertions(+), 112 deletions(-) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index c2c8339c1..89ceb7335 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -5,8 +5,8 @@ use std::thread::{self, JoinHandle}; use crate::config::{EventKeyType, EventObserverConfig}; use crate::tests::l1_multiparty::MOCKNET_EPOCH_2_1; use crate::tests::neon_integrations::{ - filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, - get_withdrawal_entry, submit_tx, test_observer, + filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, get_withdrawal_entry, submit_tx, + test_observer, }; use crate::tests::{make_contract_call, make_contract_publish, to_addr}; use crate::{neon, Config}; @@ -2404,86 +2404,10 @@ fn nft_deposit_and_withdraw_integration_test() { microblock_header: None, tx_index: 0, }; - let mut l1_native_ft_withdraw_event = - StacksTransactionEvent::SmartContractEvent(SmartContractEventData { - key: (boot_code_id("subnet".into(), false), "print".into()), - value: Value::Tuple( - TupleData::from_data(vec![ - ( - "type".into(), - Value::string_ascii_from_bytes("nft".to_string().into_bytes()).unwrap(), - ), - ( - "asset-contract".into(), - Value::Principal(PrincipalData::Contract( - QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("simple-ft"), - ), - )), - ), - ( - "sender".into(), - Value::Principal(PrincipalData::Standard(user_addr.into())), - ), - ("id".into(), Value::UInt(11)), - ]) - .expect("Failed to create tuple data."), - ), - }); - let mut subnet_native_ft_withdraw_event = - StacksTransactionEvent::SmartContractEvent(SmartContractEventData { - key: (boot_code_id("subnet".into(), false), "print".into()), - value: Value::Tuple( - TupleData::from_data(vec![ - ( - "type".into(), - Value::string_ascii_from_bytes("nft".to_string().into_bytes()).unwrap(), - ), - ( - "asset-contract".into(), - Value::Principal(PrincipalData::Contract( - QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("simple-ft"), - ), - )), - ), - ( - "sender".into(), - Value::Principal(PrincipalData::Standard(user_addr.into())), - ), - ("id".into(), Value::UInt(15)), - ]) - .expect("Failed to create tuple data."), - ), - }); - let withdrawal_receipt_ft = StacksTransactionReceipt { - transaction: TransactionOrigin::Stacks(StacksTransaction::new( - TransactionVersion::Testnet, - auth.clone(), - TransactionPayload::Coinbase(CoinbasePayload([0u8; 32])), - )), - events: vec![ - l1_native_ft_withdraw_event.clone(), - subnet_native_ft_withdraw_event.clone(), - ], - post_condition_aborted: false, - result: Value::err_none(), - stx_burned: 0, - contract_analysis: None, - execution_cost: ExecutionCost::zero(), - microblock_header: None, - tx_index: 0, - }; let withdrawal_tree = create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt], withdrawal_height); let root_hash = withdrawal_tree.root().as_bytes().to_vec(); - let withdrawal_tree_ft = - create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt_ft], withdrawal_height); - let root_hash_ft = withdrawal_tree_ft.root().as_bytes().to_vec(); - let l1_native_nft_withdrawal_key = generate_key_from_event(&mut l1_native_nft_withdraw_event, 0, withdrawal_height).unwrap(); let l1_native_nft_withdrawal_key_bytes = @@ -2497,19 +2421,6 @@ fn nft_deposit_and_withdraw_integration_test() { .path(&l1_native_nft_withdrawal_key_bytes) .unwrap(); - let l1_native_ft_withdrawal_key = - generate_key_from_event(&mut l1_native_ft_withdraw_event, 0, withdrawal_height).unwrap(); - let l1_native_ft_withdrawal_key_bytes = - convert_withdrawal_key_to_bytes(&l1_native_ft_withdrawal_key); - let l1_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( - l1_native_ft_withdrawal_key_bytes.as_slice(), - ) - .as_bytes() - .to_vec(); - let l1_native_ft_path = withdrawal_tree - .path(&l1_native_ft_withdrawal_key_bytes) - .unwrap(); - let mut l1_native_nft_sib_data = Vec::new(); for (_i, sib) in l1_native_nft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2540,25 +2451,6 @@ fn nft_deposit_and_withdraw_integration_test() { &l1_native_siblings_val, &l1_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - - let mut l1_native_ft_sib_data = Vec::new(); - for (_i, sib) in l1_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - l1_native_ft_sib_data.push(sib_tuple); - } - - let l1_native_root_hash_val = Value::buff_from(root_hash_ft.clone()).unwrap(); - let l1_native_leaf_hash_val = - Value::buff_from(l1_native_ft_withdrawal_leaf_hash.clone()).unwrap(); - let l1_native_siblings_val = Value::list_from(l1_native_ft_sib_data.clone()).unwrap(); - assert_eq!( &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, "Root hash should match value returned via RPC" @@ -2642,7 +2534,7 @@ fn nft_deposit_and_withdraw_integration_test() { &subnet_native_siblings_val, &subnet_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - + let mut subnet_native_ft_sib_data = Vec::new(); for (_i, sib) in subnet_native_ft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2655,7 +2547,7 @@ fn nft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_ft_sib_data.push(sib_tuple); } - + let subnet_native_leaf_hash_val = Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); From dfabd03c0d88323b98e89db7f2baa01d8574aef3 Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Tue, 21 Feb 2023 04:47:13 -0500 Subject: [PATCH 07/12] Create duplicate unit test for fungible tokens --- .../stacks-node/src/tests/l1_observer_test.rs | 980 ++++++++++++++++++ 1 file changed, 980 insertions(+) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 89ceb7335..3abb5593f 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -2682,3 +2682,983 @@ fn nft_deposit_and_withdraw_integration_test() { stacks_l1_controller.kill_process(); run_loop_thread.join().expect("Failed to join run loop."); } + +/// This integration test verifies that: +/// (a) assets minted on L1 chain can be deposited into subnet +/// (b) assets minted on subnet can be withdrawn to the L1 +#[test] +#[allow(unused_assignments)] +fn ft_deposit_and_withdraw_integration_test() { + // running locally: + // STACKS_BASE_DIR=~/devel/stacks-blockchain/target/release/stacks-node STACKS_NODE_TEST=1 cargo test --workspace ft_deposit_and_withdraw_integration_test + if env::var("STACKS_NODE_TEST") != Ok("1".into()) { + return; + } + + // Start Stacks L1. + let l1_toml_file = "../../contrib/conf/stacks-l1-mocknet.toml"; + let l1_rpc_origin = "http://127.0.0.1:20443"; + let trait_standards_contract_name = "trait-standards"; + + // Start the L2 run loop. + let mut config = super::new_test_conf(); + config.node.mining_key = Some(MOCKNET_PRIVATE_KEY_2.clone()); + let miner_account = to_addr(&MOCKNET_PRIVATE_KEY_2); + let user_addr = to_addr(&MOCKNET_PRIVATE_KEY_1); + config.add_initial_balance(user_addr.to_string(), 10000000); + config.add_initial_balance(miner_account.to_string(), 10000000); + + config.burnchain.first_burn_header_height = 1; + config.burnchain.chain = "stacks_layer_1".to_string(); + config.burnchain.rpc_ssl = false; + config.burnchain.rpc_port = 20443; + config.burnchain.peer_host = "127.0.0.1".into(); + config.node.wait_time_for_microblocks = 10_000; + config.node.rpc_bind = "127.0.0.1:30443".into(); + config.node.p2p_bind = "127.0.0.1:30444".into(); + let l2_rpc_origin = format!("http://{}", &config.node.rpc_bind); + let mut l2_nonce = 0; + + config.burnchain.contract_identifier = + QualifiedContractIdentifier::new(user_addr.into(), "subnet-controller".into()); + + config.node.miner = true; + + config.events_observers.push(EventObserverConfig { + endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT), + events_keys: vec![EventKeyType::AnyEvent], + }); + + test_observer::spawn(); + + let mut run_loop = neon::RunLoop::new(config.clone()); + let termination_switch = run_loop.get_termination_switch(); + let run_loop_thread = thread::spawn(move || run_loop.start(None, 0)); + + // Give the run loop time to start. + thread::sleep(Duration::from_millis(2_000)); + + // The burnchain should have registered what the listener recorded. + let burnchain = Burnchain::new(&config.get_burn_db_path(), &config.burnchain.chain).unwrap(); + let (sortition_db, burndb) = burnchain.open_db(true).unwrap(); + + let mut stacks_l1_controller = StacksL1Controller::new(l1_toml_file.to_string(), true); + let _stacks_res = stacks_l1_controller + .start_process() + .expect("stacks l1 controller didn't start"); + let mut l1_nonce = 0; + + // Sleep to give the L1 chain time to start + thread::sleep(Duration::from_millis(10_000)); + wait_for_target_l1_block(&sortition_db, MOCKNET_EPOCH_2_1); + + // Publish the ft/FT/mint-from-subnet traits onto L1 + let trait_content = + include_str!("../../../../core-contracts/contracts/helper/trait-standards.clar"); + let trait_publish = make_contract_publish( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &trait_standards_contract_name, + &trait_content, + ); + l1_nonce += 1; + + // Publish a simple ft onto L1 + let ft_content = include_str!("../../../../core-contracts/contracts/helper/simple-ft.clar"); + let ft_publish = make_contract_publish( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + "simple-ft", + &ft_content, + ); + l1_nonce += 1; + let ft_contract_name = ContractName::from("simple-ft"); + let ft_contract_id = QualifiedContractIdentifier::new(user_addr.into(), ft_contract_name); + + // Publish the default subnet contract on the L1 chain + let contract_content = include_str!("../../../../core-contracts/contracts/subnet.clar") + .replace( + "(define-data-var miner (optional principal) none)", + &format!( + "(define-data-var miner (optional principal) (some '{}))", + &miner_account + ), + ); + + let subnet_contract_publish = make_contract_publish( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + config.burnchain.contract_identifier.name.as_str(), + &format!( + "{}\n (as-contract (setup-allowed-contracts))", + contract_content + ), + ); + l1_nonce += 1; + + submit_tx(l1_rpc_origin, &trait_publish); + submit_tx(l1_rpc_origin, &ft_publish); + // Because the nonce ensures that the trait contracts + // are published before the subnet contract, we can broadcast them + // all at once, even though the subnet contract depends on those + // contracts. + submit_tx(l1_rpc_origin, &subnet_contract_publish); + + println!("Submitted ft and Subnet contracts onto L1!"); + + // Sleep to give the run loop time to listen to blocks, + // and start mining L2 blocks + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + let tip = burndb + .get_canonical_chain_tip() + .expect("couldn't get chain tip"); + + // Ensure that the tip height has moved beyond height 0. + // We check that we have moved past 3 just to establish we are reliably getting blocks. + assert!(tip.block_height > 3); + + // test the miner's nonce has incremented: this shows that L2 blocks have + // been mined (because the coinbase transactions bump the miner's nonce) + let account = get_account(&l2_rpc_origin, &miner_account); + assert!( + account.nonce >= 2, + "Miner should have produced at least 2 coinbase transactions" + ); + + // Publish the traits onto the subnet + let l2_trait_publish = make_contract_publish( + &MOCKNET_PRIVATE_KEY_1, + config.node.chain_id, + l2_nonce, + 1_000_000, + &trait_standards_contract_name, + &trait_content, + ); + l2_nonce += 1; + + // Publish subnet contract for ft-token + let subnet_simple_ft = " + (impl-trait .trait-standards.ft-trait) + + (define-constant ERR_NOT_AUTHORIZED (err u1001)) + + (define-non-fungible-token ft-token uint) + (define-data-var lastId uint u0) + + ;; ft trait implementation + (define-read-only (get-last-token-id) + (ok (var-get lastId)) + ) + + (define-read-only (get-owner (id uint)) + (ok (ft-get-owner? ft-token id)) + ) + + (define-read-only (get-token-uri (id uint)) + (ok none) + ) + + (define-public (transfer (id uint) (sender principal) (recipient principal)) + (begin + (asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED) + (ft-transfer? ft-token id sender recipient) + ) + ) + + ;; Other methods + (define-public (mint-on-subnet (id uint) (recipient principal)) + (begin + (var-set lastId id) + (ft-mint? ft-token id recipient) + ) + ) + + (define-public (subnet-deposit-ft-token (id uint) (recipient principal)) + (begin + (var-set lastId id) + (ft-mint? ft-token id recipient) + ) + ) + + (define-read-only (get-token-owner (id uint)) + (ft-get-owner? ft-token id) + ) + "; + let subnet_ft_publish = make_contract_publish( + &MOCKNET_PRIVATE_KEY_1, + config.node.chain_id, + l2_nonce, + 1_000_000, + "simple-ft", + subnet_simple_ft, + ); + l2_nonce += 1; + let subnet_ft_contract_id = + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")); + + submit_tx(&l2_rpc_origin, &l2_trait_publish); + submit_tx(&l2_rpc_origin, &subnet_ft_publish); + + // Sleep to give the run loop time to mine a block + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + // Mint a ft-token for user on L1 chain (ID = 1) + let l1_mint_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &user_addr, + "simple-ft", + "test-mint", + &[Value::Principal(user_addr.into())], + ); + l1_nonce += 1; + + // Mint a ft-token for user on subnet (ID = 5) + let l2_mint_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + config.node.chain_id, + l2_nonce, + 1_000_000, + &user_addr, + "simple-ft", + "mint-on-subnet", + &[Value::UInt(5), Value::Principal(user_addr.into())], + ); + l2_nonce += 1; + + submit_tx(&l2_rpc_origin, &l2_mint_ft_tx); + submit_tx(l1_rpc_origin, &l1_mint_ft_tx); + + // Sleep to give the run loop time to mine a block + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + // Check that the user does not own the L1 native ft on the subnet now + let res = call_read_only( + &l2_rpc_origin, + &user_addr, + "simple-ft", + "get-token-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let addr = Value::deserialize( + &result, + &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + ); + assert_eq!(addr, Value::none()); + + let l1_deposit_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &user_addr, + config.burnchain.contract_identifier.name.as_str(), + "deposit-ft-asset", + &[ + Value::UInt(1), + Value::Principal(user_addr.into()), + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::Principal(PrincipalData::Contract(subnet_ft_contract_id.clone())), + ], + ); + l1_nonce += 1; + + // deposit ft-token into subnet contract on L1 + submit_tx(&l1_rpc_origin, &l1_deposit_ft_tx); + + // Sleep to give the run loop time to mine a block + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + // Check that the user owns the L1 native ft on the subnet now + let res = call_read_only( + &l2_rpc_origin, + &user_addr, + "simple-ft", + "get-token-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let addr = Value::deserialize( + &result, + &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + ); + assert_eq!( + addr, + Value::some(Value::Principal(user_addr.into())).unwrap() + ); + + // Check that the user does not own the L1 native ft on the L1 anymore (the contract should own it) + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + let subnet_contract_principal = Value::okay( + Value::some(Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("subnet-controller"), + ), + ))) + .unwrap(), + ) + .unwrap(); + assert_eq!(owner, subnet_contract_principal); + + // Check that the no one owns the subnet native ft on the L1 + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(5).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + assert_eq!(owner, Value::okay(Value::none()).unwrap()); + + // Withdraw the L1 native ft from the L2 (with `ft-withdraw?`) + let l2_withdraw_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + config.node.chain_id, + l2_nonce, + 1_000_000, + &boot_code_addr(false), + "subnet", + "ft-withdraw?", + &[ + Value::Principal(PrincipalData::Contract(QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ))), + Value::UInt(1), + Value::Principal(user_addr.into()), + ], + ); + l2_nonce += 1; + // Withdraw the subnet native ft from the L2 (with `ft-withdraw?`) + let l2_withdraw_native_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + config.node.chain_id, + l2_nonce, + 1_000_000, + &boot_code_addr(false), + "subnet", + "ft-withdraw?", + &[ + Value::Principal(PrincipalData::Contract(QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ))), + Value::UInt(5), + Value::Principal(user_addr.into()), + ], + ); + l2_nonce += 1; + // Submit withdrawal function calls + submit_tx(&l2_rpc_origin, &l2_withdraw_ft_tx); + submit_tx(&l2_rpc_origin, &l2_withdraw_native_ft_tx); + + // Sleep to give the run loop time to mine a block + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + // Check that user no longer owns the l1 native ft on L2 chain, + // instead, the subnet contract should own it. + let res = call_read_only( + &l2_rpc_origin, + &user_addr, + "simple-ft", + "get-token-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let addr = Value::deserialize( + &result, + &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + ); + assert_eq!( + addr, + Value::some(Value::Principal(PrincipalData::Contract( + boot_code_id("subnet".into(), false).into() + ))) + .unwrap() + ); + // Check that user no longer owns the subnet native ft on L2 chain, + // instead, the subnet contract should own it. + let res = call_read_only( + &l2_rpc_origin, + &user_addr, + "simple-ft", + "get-token-owner", + vec![Value::UInt(5).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let addr = Value::deserialize( + &result, + &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + ); + assert_eq!( + addr, + Value::some(Value::Principal(PrincipalData::Contract( + boot_code_id("subnet".into(), false).into() + ))) + .unwrap() + ); + + // Check that the user does not *yet* own the L1 native ft on the L1 (the contract should still own it) + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + let subnet_contract_principal = Value::okay( + Value::some(Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("subnet-controller"), + ), + ))) + .unwrap(), + ) + .unwrap(); + assert_eq!(owner, subnet_contract_principal); + // Check that the user does not *yet* own the subnet native ft on the L1 (no one should own it) + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(5).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + assert_eq!(owner, Value::okay(Value::none()).unwrap()); + + let block_data = test_observer::get_blocks(); + let mut withdraw_events = filter_map_events(&block_data, |height, event| { + let ev_type = event.get("type").unwrap().as_str().unwrap(); + if ev_type == "contract_event" { + let contract_event = event.get("contract_event").unwrap(); + let contract_identifier = contract_event + .get("contract_identifier") + .unwrap() + .as_str() + .unwrap(); + let topic = contract_event.get("topic").unwrap().as_str().unwrap(); + match (contract_identifier, topic) { + ("ST000000000000000000002AMW42H.subnet", "print") => { + let value: Value = + serde_json::from_value(contract_event.get("value").unwrap().clone()) + .unwrap(); + let data_map = value.expect_tuple(); + if data_map.get("type").unwrap().clone().expect_ascii() != "ft" { + return None; + } + Some((height, data_map.clone())) + } + _ => None, + } + } else { + None + } + }); + assert_eq!(withdraw_events.len(), 2); + let (withdrawal_height, _withdrawal) = withdraw_events.pop().unwrap(); + + let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 0, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 1, + ); + let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 1, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 5, + ); + + let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 0, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 1, + ); + let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + &l2_rpc_origin, + withdrawal_height, + &user_addr, + 1, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), + 5, + ); + + // Create the withdrawal merkle tree by mocking both ft withdraw events (if the root hash of + // this constructed merkle tree is not identical to the root hash published by the subnet node, + // then the test will fail). + let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh( + StacksPublicKey::from_private(&MOCKNET_PRIVATE_KEY_1), + ) + .expect("Failed to create p2pkh spending condition from public key."); + spending_condition.set_nonce(l2_nonce - 1); + spending_condition.set_tx_fee(1000); + let auth = TransactionAuth::Standard(spending_condition); + let mut l1_native_ft_withdraw_event = + StacksTransactionEvent::SmartContractEvent(SmartContractEventData { + key: (boot_code_id("subnet".into(), false), "print".into()), + value: Value::Tuple( + TupleData::from_data(vec![ + ( + "type".into(), + Value::string_ascii_from_bytes("ft".to_string().into_bytes()).unwrap(), + ), + ( + "asset-contract".into(), + Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ), + )), + ), + ( + "sender".into(), + Value::Principal(PrincipalData::Standard(user_addr.into())), + ), + ("id".into(), Value::UInt(1)), + ]) + .expect("Failed to create tuple data."), + ), + }); + let mut subnet_native_ft_withdraw_event = + StacksTransactionEvent::SmartContractEvent(SmartContractEventData { + key: (boot_code_id("subnet".into(), false), "print".into()), + value: Value::Tuple( + TupleData::from_data(vec![ + ( + "type".into(), + Value::string_ascii_from_bytes("ft".to_string().into_bytes()).unwrap(), + ), + ( + "asset-contract".into(), + Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::new( + user_addr.into(), + ContractName::from("simple-ft"), + ), + )), + ), + ( + "sender".into(), + Value::Principal(PrincipalData::Standard(user_addr.into())), + ), + ("id".into(), Value::UInt(5)), + ]) + .expect("Failed to create tuple data."), + ), + }); + let withdrawal_receipt = StacksTransactionReceipt { + transaction: TransactionOrigin::Stacks(StacksTransaction::new( + TransactionVersion::Testnet, + auth.clone(), + TransactionPayload::Coinbase(CoinbasePayload([0u8; 32])), + )), + events: vec![ + l1_native_ft_withdraw_event.clone(), + subnet_native_ft_withdraw_event.clone(), + ], + post_condition_aborted: false, + result: Value::err_none(), + stx_burned: 0, + contract_analysis: None, + execution_cost: ExecutionCost::zero(), + microblock_header: None, + tx_index: 0, + }; + let withdrawal_tree = + create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt], withdrawal_height); + let root_hash = withdrawal_tree.root().as_bytes().to_vec(); + + let l1_native_ft_withdrawal_key = + generate_key_from_event(&mut l1_native_ft_withdraw_event, 0, withdrawal_height).unwrap(); + let l1_native_ft_withdrawal_key_bytes = + convert_withdrawal_key_to_bytes(&l1_native_ft_withdrawal_key); + let l1_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( + l1_native_ft_withdrawal_key_bytes.as_slice(), + ) + .as_bytes() + .to_vec(); + let l1_native_ft_path = withdrawal_tree + .path(&l1_native_ft_withdrawal_key_bytes) + .unwrap(); + + let mut l1_native_ft_sib_data = Vec::new(); + for (_i, sib) in l1_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + l1_native_ft_sib_data.push(sib_tuple); + } + + let l1_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); + let l1_native_leaf_hash_val = + Value::buff_from(l1_native_ft_withdrawal_leaf_hash.clone()).unwrap(); + let l1_native_siblings_val = Value::list_from(l1_native_ft_sib_data.clone()).unwrap(); + + assert_eq!( + &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); + assert_eq!( + &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); + + let subnet_native_ft_withdrawal_key = + generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) + .unwrap(); + let subnet_native_ft_withdrawal_key_bytes = + convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); + let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( + subnet_native_ft_withdrawal_key_bytes.as_slice(), + ) + .as_bytes() + .to_vec(); + let subnet_native_ft_path = withdrawal_tree + .path(&subnet_native_ft_withdrawal_key_bytes) + .unwrap(); + + let subnet_native_ft_withdrawal_key = + generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) + .unwrap(); + let subnet_native_ft_withdrawal_key_bytes = + convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); + let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( + subnet_native_ft_withdrawal_key_bytes.as_slice(), + ) + .as_bytes() + .to_vec(); + let subnet_native_ft_path = withdrawal_tree + .path(&subnet_native_ft_withdrawal_key_bytes) + .unwrap(); + + let mut subnet_native_ft_sib_data = Vec::new(); + for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + subnet_native_ft_sib_data.push(sib_tuple); + } + let mut subnet_native_ft_sib_data = Vec::new(); + for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + subnet_native_ft_sib_data.push(sib_tuple); + } + + let subnet_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); + let subnet_native_leaf_hash_val = + Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); + let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); + + assert_eq!( + &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); + + let mut subnet_native_ft_sib_data = Vec::new(); + for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); + // the sibling's side is the opposite of what PathOrder is set to + let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); + let curr_sib_data = vec![ + (ClarityName::from("hash"), sib_hash), + (ClarityName::from("is-left-side"), sib_is_left), + ]; + let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); + subnet_native_ft_sib_data.push(sib_tuple); + } + + let subnet_native_leaf_hash_val = + Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); + let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); + + assert_eq!( + &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, + "Root hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, + "Leaf hash should match value returned via RPC" + ); + assert_eq!( + &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, + "Sibling hashes should match value returned via RPC" + ); + + // TODO: call withdraw from unauthorized principal once leaf verification is added to the subnet contract + + let l1_withdraw_l1_native_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &user_addr, + config.burnchain.contract_identifier.name.as_str(), + "withdraw-ft-asset", + &[ + Value::UInt(1), + Value::Principal(user_addr.into()), + Value::UInt(0), + Value::UInt(withdrawal_height.into()), + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::some(Value::Principal(PrincipalData::Contract( + ft_contract_id.clone(), + ))) + .unwrap(), + Value::buff_from(root_hash.clone()).unwrap(), + Value::buff_from(l1_native_ft_withdrawal_leaf_hash).unwrap(), + Value::list_from(l1_native_ft_sib_data).unwrap(), + ], + ); + l1_nonce += 1; + let l1_withdraw_subnet_native_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &user_addr, + config.burnchain.contract_identifier.name.as_str(), + "withdraw-ft-asset", + &[ + Value::UInt(5), + Value::Principal(user_addr.into()), + Value::UInt(1), + Value::UInt(withdrawal_height.into()), + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::some(Value::Principal(PrincipalData::Contract( + ft_contract_id.clone(), + ))) + .unwrap(), + Value::buff_from(root_hash).unwrap(), + Value::buff_from(subnet_native_ft_withdrawal_leaf_hash).unwrap(), + Value::list_from(subnet_native_ft_sib_data).unwrap(), + ], + ); + l1_nonce += 1; + // Withdraw ft-token from subnet contract on L1 + submit_tx(&l1_rpc_origin, &l1_withdraw_l1_native_ft_tx); + submit_tx(&l1_rpc_origin, &l1_withdraw_subnet_native_ft_tx); + + // Sleep to give the run loop time to mine a block + wait_for_next_stacks_block(&sortition_db); + wait_for_next_stacks_block(&sortition_db); + + // Check that the user owns the L1 native ft on the L1 chain now + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(1).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + assert_eq!( + owner, + Value::okay(Value::some(Value::Principal(user_addr.into())).unwrap()).unwrap() + ); + // Check that the user owns the subnet native ft on the L1 chain now + let res = call_read_only( + &l1_rpc_origin, + &user_addr, + "simple-ft", + "get-owner", + vec![Value::UInt(5).serialize()], + ); + assert!(res.get("cause").is_none()); + assert!(res["okay"].as_bool().unwrap()); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let owner = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new(( + TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + TypeSignature::UIntType, + ))), + ); + assert_eq!( + owner, + Value::okay(Value::some(Value::Principal(user_addr.into())).unwrap()).unwrap() + ); + + termination_switch.store(false, Ordering::SeqCst); + stacks_l1_controller.kill_process(); + run_loop_thread.join().expect("Failed to join run loop."); +} From fc92467346019b8469b943d7edf6971c01197216 Mon Sep 17 00:00:00 2001 From: Scott A <119700554+fpbgg@users.noreply.github.com> Date: Tue, 21 Feb 2023 04:53:18 -0500 Subject: [PATCH 08/12] nit: Code style consistency --- testnet/stacks-node/src/tests/l1_observer_test.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 3abb5593f..52f2fad96 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -5,8 +5,8 @@ use std::thread::{self, JoinHandle}; use crate::config::{EventKeyType, EventObserverConfig}; use crate::tests::l1_multiparty::MOCKNET_EPOCH_2_1; use crate::tests::neon_integrations::{ - filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, get_withdrawal_entry, submit_tx, - test_observer, + filter_map_events, get_account, get_ft_withdrawal_entry, get_nft_withdrawal_entry, + get_withdrawal_entry, submit_tx, test_observer, }; use crate::tests::{make_contract_call, make_contract_publish, to_addr}; use crate::{neon, Config}; @@ -2534,7 +2534,7 @@ fn nft_deposit_and_withdraw_integration_test() { &subnet_native_siblings_val, &subnet_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - + let mut subnet_native_ft_sib_data = Vec::new(); for (_i, sib) in subnet_native_ft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2547,7 +2547,7 @@ fn nft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_ft_sib_data.push(sib_tuple); } - + let subnet_native_leaf_hash_val = Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); @@ -3514,7 +3514,7 @@ fn ft_deposit_and_withdraw_integration_test() { &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - + let mut subnet_native_ft_sib_data = Vec::new(); for (_i, sib) in subnet_native_ft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -3527,7 +3527,7 @@ fn ft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_ft_sib_data.push(sib_tuple); } - + let subnet_native_leaf_hash_val = Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); From 3357445212b30ab7c67f1ede79e96ab65bb36d57 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 21 Feb 2023 17:45:25 -0500 Subject: [PATCH 09/12] fix: finish FT withdrawal endpoint implementation --- src/net/http.rs | 20 ++++++++++++++----- src/net/mod.rs | 2 +- src/net/rpc.rs | 4 ++-- .../src/tests/neon_integrations.rs | 6 +++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/net/http.rs b/src/net/http.rs index 59e0dfffe..a1f3bdae9 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -130,6 +130,11 @@ lazy_static! { .unwrap(); static ref PATH_POST_BLOCK_PROPOSAL: Regex = Regex::new(&format!("^{}$", PATH_STR_POST_BLOCK_PROPOSAL)) .unwrap(); + static ref PATH_GET_FT_WITHDRAWAL: Regex = Regex::new(&format!( + "^/v2/withdrawal/ft/(?P[0-9]+)/(?P{})/(?P[0-9]+)/(?P{})/(?P{})/(?P[0-9]+)$", + *PRINCIPAL_DATA_REGEX_STRING, *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING + )) + .unwrap(); static ref PATH_GET_NFT_WITHDRAWAL: Regex = Regex::new(&format!( "^/v2/withdrawal/nft/(?P[0-9]+)/(?P{})/(?P[0-9]+)/(?P{})/(?P{})/(?P[0-9]+)$", *PRINCIPAL_DATA_REGEX_STRING, *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING @@ -1629,6 +1634,11 @@ impl HttpRequestType { &PATH_POST_BLOCK_PROPOSAL, &HttpRequestType::parse_block_proposal, ), + ( + "GET", + &PATH_GET_FT_WITHDRAWAL, + &HttpRequestType::parse_get_ft_withdrawal, + ), ( "GET", &PATH_GET_NFT_WITHDRAWAL, @@ -1887,7 +1897,7 @@ impl HttpRequestType { })?; let contract_name = ContractName::try_from(captures["contract_name"].to_string()) .map_err(|_e| net_error::DeserializeError("Failed to parse contract name".into()))?; - let id = u128::from_str(&captures["id"]) + let amount = u128::from_str(&captures["amount"]) .map_err(|_e| net_error::DeserializeError("Failed to parse amount".into()))?; Ok(HttpRequestType::GetWithdrawalFt { @@ -1899,7 +1909,7 @@ impl HttpRequestType { contract_addr.into(), contract_name, ), - id, + amount, }) } @@ -3089,7 +3099,7 @@ impl HttpRequestType { sender, withdrawal_id, contract_identifier, - id, + amount, } => format!( "/v2/withdrawal/ft/{}/{}/{}/{}/{}/{}", withdraw_block_height, @@ -3097,7 +3107,7 @@ impl HttpRequestType { withdrawal_id, StacksAddress::from(contract_identifier.issuer.clone()), contract_identifier.name.as_str(), - id + amount ), HttpRequestType::GetWithdrawalNft { metadata: _, @@ -3155,7 +3165,7 @@ impl HttpRequestType { } HttpRequestType::BlockProposal(..) => PATH_STR_POST_BLOCK_PROPOSAL, HttpRequestType::GetWithdrawalFt { .. } => { - "/v2/withdrawal/ft/:block-height/:sender/:withdrawal_id/:contract_address/:contract_name/:id" + "/v2/withdrawal/ft/:block-height/:sender/:withdrawal_id/:contract_address/:contract_name/:amount" } HttpRequestType::GetWithdrawalNft { .. } => { "/v2/withdrawal/nft/:block-height/:sender/:withdrawal_id/:contract_address/:contract_name/:id" diff --git a/src/net/mod.rs b/src/net/mod.rs index 205318911..1f5dfa2f5 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1447,7 +1447,7 @@ pub enum HttpRequestType { sender: PrincipalData, withdrawal_id: u32, contract_identifier: QualifiedContractIdentifier, - id: u128, + amount: u128, }, GetWithdrawalNft { metadata: HttpRequestMetadata, diff --git a/src/net/rpc.rs b/src/net/rpc.rs index c638e188a..7d774bb70 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -2892,7 +2892,7 @@ impl ConversationHttp { withdraw_block_height, ref sender, withdrawal_id, - id, + amount, ref contract_identifier, .. } => { @@ -2915,7 +2915,7 @@ impl ConversationHttp { &sender.clone(), withdrawal_id, contract_identifier, - id, + amount, network.burnchain_tip.canonical_stacks_tip_height, )?; } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index bc4c43c8d..49c956139 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -685,7 +685,7 @@ pub fn get_withdrawal_entry( http_origin: &str, block_height: u64, sender: F, - withdrawal_id: u64, + withdrawal_id: u32, amount: u64, ) -> WithdrawalEntry { let client = reqwest::blocking::Client::new(); @@ -712,7 +712,7 @@ pub fn get_ft_withdrawal_entry( http_origin: &str, block_height: u64, sender: F, - withdrawal_id: u64, + withdrawal_id: u32, contract_identifier: QualifiedContractIdentifier, id: u64, ) -> WithdrawalEntry { @@ -746,7 +746,7 @@ pub fn get_nft_withdrawal_entry( http_origin: &str, block_height: u64, sender: F, - withdrawal_id: u64, + withdrawal_id: u32, contract_identifier: QualifiedContractIdentifier, id: u64, ) -> WithdrawalEntry { From 60485d2870f39fda2a9d41deb1ee35639045bb65 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 22 Feb 2023 11:32:41 -0500 Subject: [PATCH 10/12] fix: resolve issues in FT tests --- .../contracts/helper/simple-ft-l2.clar | 10 +- .../contracts/helper/simple-ft.clar | 7 +- .../stacks-node/src/tests/l1_observer_test.rs | 779 +++++------------- .../src/tests/neon_integrations.rs | 4 +- 4 files changed, 208 insertions(+), 592 deletions(-) diff --git a/core-contracts/contracts/helper/simple-ft-l2.clar b/core-contracts/contracts/helper/simple-ft-l2.clar index 663b48792..62a5af55e 100644 --- a/core-contracts/contracts/helper/simple-ft-l2.clar +++ b/core-contracts/contracts/helper/simple-ft-l2.clar @@ -6,8 +6,7 @@ ;; get the token balance of owner (define-read-only (get-balance (owner principal)) - (begin - (ok (ft-get-balance ft-token owner)))) + (ok (ft-get-balance ft-token owner))) ;; returns the total number of tokens (define-read-only (get-total-supply) @@ -38,8 +37,11 @@ (ok none) ) -(define-read-only (get-token-balance (user principal)) - (ft-get-balance ft-token user) +(define-public (gift-tokens (amount uint) (recipient principal)) + (begin + (asserts! (is-eq tx-sender recipient) ERR_NOT_AUTHORIZED) + (ft-mint? ft-token amount recipient) + ) ) (impl-trait 'ST000000000000000000002AMW42H.subnet.subnet-asset) diff --git a/core-contracts/contracts/helper/simple-ft.clar b/core-contracts/contracts/helper/simple-ft.clar index 555fb2aaf..884267ef4 100644 --- a/core-contracts/contracts/helper/simple-ft.clar +++ b/core-contracts/contracts/helper/simple-ft.clar @@ -7,8 +7,7 @@ ;; get the token balance of owner (define-read-only (get-balance (owner principal)) - (begin - (ok (ft-get-balance ft-token owner)))) + (ok (ft-get-balance ft-token owner))) ;; returns the total number of tokens (define-read-only (get-total-supply) @@ -49,9 +48,9 @@ (define-read-only (get-token-uri) (ok none)) -(define-public (gift-tokens (recipient principal)) +(define-public (gift-tokens (amount uint) (recipient principal)) (begin (asserts! (is-eq tx-sender recipient) ERR_NOT_AUTHORIZED) - (ft-mint? ft-token u1 recipient) + (ft-mint? ft-token amount recipient) ) ) \ No newline at end of file diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index 52f2fad96..ec38a0fd3 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -602,7 +602,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &user_addr, "simple-ft", "gift-tokens", - &[Value::Principal(user_addr.into())], + &[Value::UInt(1), Value::Principal(user_addr.into())], ); l1_nonce += 1; // Mint a nft-token for user on L1 chain (ID = 1) @@ -670,7 +670,17 @@ fn l1_deposit_and_withdraw_asset_integration_test() { ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); - assert_eq!(res["result"], "0x0100000000000000000000000000000000"); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let amount = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), + ); + assert_eq!(amount, Value::okay(Value::UInt(0)).unwrap()); // Check that the user does not own the NFT on the subnet now let res = call_read_only( @@ -751,8 +761,18 @@ fn l1_deposit_and_withdraw_asset_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let amount = Value::deserialize(&result, &TypeSignature::UIntType); - assert_eq!(amount, Value::UInt(1)); + let result = res["result"] + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap() + .to_string(); + let amount = Value::deserialize( + &result, + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), + ); + assert_eq!(amount, Value::okay(Value::UInt(1)).unwrap()); + // Check that the user owns the NFT on the subnet now let res = call_read_only( &l2_rpc_origin, @@ -799,6 +819,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); assert_eq!(amount, Value::okay(Value::UInt(0)).unwrap()); + // Check that the user does not own the NFT on the L1 (the contract should own it) let res = call_read_only( &l1_rpc_origin, @@ -846,6 +867,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { ], ); l2_nonce += 1; + // Withdraw the nft on the L2 let l2_withdraw_nft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, @@ -865,6 +887,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { ], ); l2_nonce += 1; + // Withdraw ft-token from subnet contract on L2 submit_tx(&l2_rpc_origin, &l2_withdraw_ft_tx); // Withdraw nft-token from subnet contract on L2 @@ -894,6 +917,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); assert_eq!(amount, Value::okay(Value::UInt(0)).unwrap()); + // Check that user no longer owns the nft on L2 chain. let res = call_read_only( &l2_rpc_origin, @@ -915,6 +939,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), ); assert_eq!(addr, Value::none(),); + // Check that the user does not *yet* own the FT on the L1 let res = call_read_only( &l1_rpc_origin, @@ -936,6 +961,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); assert_eq!(amount, Value::okay(Value::UInt(0)).unwrap()); + // Check that the user does not *yet* own the NFT on the L1 (the contract should own it) let res = call_read_only( &l1_rpc_origin, @@ -965,7 +991,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { ); let block_data = test_observer::get_blocks(); - let mut withdraw_events = filter_map_events(&block_data, |height, event| { + let withdraw_events = filter_map_events(&block_data, |height, event| { let ev_type = event.get("type").unwrap().as_str().unwrap(); if ev_type == "contract_event" { let contract_event = event.get("contract_event").unwrap(); @@ -981,10 +1007,16 @@ fn l1_deposit_and_withdraw_asset_integration_test() { serde_json::from_value(contract_event.get("value").unwrap().clone()) .unwrap(); let data_map = value.expect_tuple(); - if data_map.get("type").unwrap().clone().expect_ascii() != "nft" { - return None; + match data_map + .get("type") + .unwrap() + .clone() + .expect_ascii() + .as_str() + { + "ft" | "nft" => Some((height, data_map.clone())), + _ => None, } - Some((height, data_map.clone())) } _ => None, } @@ -992,20 +1024,26 @@ fn l1_deposit_and_withdraw_asset_integration_test() { None } }); - assert_eq!(withdraw_events.len(), 1); - let (withdrawal_height, withdrawal) = withdraw_events.pop().unwrap(); + assert_eq!(withdraw_events.len(), 2); - let withdrawal_id = withdrawal - .get("withdrawal_id") - .unwrap() - .clone() - .expect_u128() as u64; + let mut ft_withdrawal_id = 0; + let mut nft_withdrawal_id = 0; + let mut withdrawal_height = 0; + for (height, event) in withdraw_events { + withdrawal_height = height; + let withdrawal_id = event.get("withdrawal_id").unwrap().clone().expect_u128() as u32; + match event.get("type").unwrap().clone().expect_ascii().as_str() { + "ft" => ft_withdrawal_id = withdrawal_id, + "nft" => nft_withdrawal_id = withdrawal_id, + _ => panic!("Unexpected withdrawal event type"), + } + } let nft_withdrawal_entry = get_nft_withdrawal_entry( &l2_rpc_origin, withdrawal_height, &user_addr, - withdrawal_id, + nft_withdrawal_id, QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-nft")), 1, ); @@ -1014,8 +1052,8 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &l2_rpc_origin, withdrawal_height, &user_addr, - withdrawal_id, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-nft")), + ft_withdrawal_id, + QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), 1, ); @@ -1103,7 +1141,8 @@ fn l1_deposit_and_withdraw_asset_integration_test() { let root_hash = withdrawal_tree.root().as_bytes().to_vec(); let ft_withdrawal_key = - generate_key_from_event(&mut ft_withdraw_event, 0, withdrawal_height).unwrap(); + generate_key_from_event(&mut ft_withdraw_event, ft_withdrawal_id, withdrawal_height) + .unwrap(); let ft_withdrawal_key_bytes = convert_withdrawal_key_to_bytes(&ft_withdrawal_key); let ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash(ft_withdrawal_key_bytes.as_slice()) @@ -1111,8 +1150,12 @@ fn l1_deposit_and_withdraw_asset_integration_test() { .to_vec(); let ft_path = withdrawal_tree.path(&ft_withdrawal_key_bytes).unwrap(); - let nft_withdrawal_key = - generate_key_from_event(&mut nft_withdraw_event, 1, withdrawal_height).unwrap(); + let nft_withdrawal_key = generate_key_from_event( + &mut nft_withdraw_event, + nft_withdrawal_id, + withdrawal_height, + ) + .unwrap(); let nft_withdrawal_key_bytes = convert_withdrawal_key_to_bytes(&nft_withdrawal_key); let nft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash(nft_withdrawal_key_bytes.as_slice()) @@ -1146,19 +1189,21 @@ fn l1_deposit_and_withdraw_asset_integration_test() { } let root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); - let leaf_hash_val = Value::buff_from(nft_withdrawal_leaf_hash.clone()).unwrap(); - let siblings_val = Value::list_from(nft_sib_data.clone()).unwrap(); + let nft_leaf_hash_val = Value::buff_from(nft_withdrawal_leaf_hash.clone()).unwrap(); + let ft_leaf_hash_val = Value::buff_from(ft_withdrawal_leaf_hash.clone()).unwrap(); + let nft_siblings_val = Value::list_from(nft_sib_data.clone()).unwrap(); + let ft_siblings_val = Value::list_from(ft_sib_data.clone()).unwrap(); assert_eq!( &root_hash_val, &nft_withdrawal_entry.root_hash, "Root hash should match value returned via RPC" ); assert_eq!( - &leaf_hash_val, &nft_withdrawal_entry.leaf_hash, + &nft_leaf_hash_val, &nft_withdrawal_entry.leaf_hash, "Leaf hash should match value returned via RPC" ); assert_eq!( - &siblings_val, &nft_withdrawal_entry.siblings, + &nft_siblings_val, &nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); @@ -1167,11 +1212,11 @@ fn l1_deposit_and_withdraw_asset_integration_test() { "Root hash should match value returned via RPC" ); assert_eq!( - &leaf_hash_val, &ft_withdrawal_entry.leaf_hash, + &ft_leaf_hash_val, &ft_withdrawal_entry.leaf_hash, "Leaf hash should match value returned via RPC" ); assert_eq!( - &siblings_val, &ft_withdrawal_entry.siblings, + &ft_siblings_val, &ft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); @@ -1256,6 +1301,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); assert_eq!(amount, Value::okay(Value::UInt(1)).unwrap()); + // Check that the user owns the NFT on the L1 chain now let res = call_read_only( &l1_rpc_origin, @@ -1516,7 +1562,7 @@ fn l1_deposit_and_withdraw_stx_integration_test() { .get("withdrawal_id") .unwrap() .clone() - .expect_u128() as u64; + .expect_u128() as u32; let withdrawal_amount: u64 = withdrawal.get("amount").unwrap().clone().expect_u128() as u64; let withdrawal_sender = withdrawal .get("sender") @@ -2478,20 +2524,6 @@ fn nft_deposit_and_withdraw_integration_test() { .path(&subnet_native_nft_withdrawal_key_bytes) .unwrap(); - let subnet_native_ft_withdrawal_key = - generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) - .unwrap(); - let subnet_native_ft_withdrawal_key_bytes = - convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); - let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( - subnet_native_ft_withdrawal_key_bytes.as_slice(), - ) - .as_bytes() - .to_vec(); - let subnet_native_ft_path = withdrawal_tree - .path(&subnet_native_ft_withdrawal_key_bytes) - .unwrap(); - let mut subnet_native_nft_sib_data = Vec::new(); for (_i, sib) in subnet_native_nft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); @@ -2504,18 +2536,6 @@ fn nft_deposit_and_withdraw_integration_test() { let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); subnet_native_nft_sib_data.push(sib_tuple); } - let mut subnet_native_ft_sib_data = Vec::new(); - for (_i, sib) in subnet_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - subnet_native_ft_sib_data.push(sib_tuple); - } let subnet_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); let subnet_native_leaf_hash_val = @@ -2535,36 +2555,6 @@ fn nft_deposit_and_withdraw_integration_test() { "Sibling hashes should match value returned via RPC" ); - let mut subnet_native_ft_sib_data = Vec::new(); - for (_i, sib) in subnet_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - subnet_native_ft_sib_data.push(sib_tuple); - } - - let subnet_native_leaf_hash_val = - Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); - let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); - - assert_eq!( - &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, - "Root hash should match value returned via RPC" - ); - assert_eq!( - &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, - "Leaf hash should match value returned via RPC" - ); - assert_eq!( - &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, - "Sibling hashes should match value returned via RPC" - ); - // TODO: call withdraw from unauthorized principal once leaf verification is added to the subnet contract let l1_withdraw_l1_native_nft_tx = make_contract_call( @@ -2752,18 +2742,12 @@ fn ft_deposit_and_withdraw_integration_test() { thread::sleep(Duration::from_millis(10_000)); wait_for_target_l1_block(&sortition_db, MOCKNET_EPOCH_2_1); - // Publish the ft/FT/mint-from-subnet traits onto L1 - let trait_content = - include_str!("../../../../core-contracts/contracts/helper/trait-standards.clar"); - let trait_publish = make_contract_publish( - &MOCKNET_PRIVATE_KEY_1, - LAYER_1_CHAIN_ID_TESTNET, + l1_nonce = publish_subnet_contracts_to_l1( l1_nonce, - 1_000_000, - &trait_standards_contract_name, - &trait_content, + &config, + miner_account.clone().into(), + user_addr.clone().into(), ); - l1_nonce += 1; // Publish a simple ft onto L1 let ft_content = include_str!("../../../../core-contracts/contracts/helper/simple-ft.clar"); @@ -2779,36 +2763,7 @@ fn ft_deposit_and_withdraw_integration_test() { let ft_contract_name = ContractName::from("simple-ft"); let ft_contract_id = QualifiedContractIdentifier::new(user_addr.into(), ft_contract_name); - // Publish the default subnet contract on the L1 chain - let contract_content = include_str!("../../../../core-contracts/contracts/subnet.clar") - .replace( - "(define-data-var miner (optional principal) none)", - &format!( - "(define-data-var miner (optional principal) (some '{}))", - &miner_account - ), - ); - - let subnet_contract_publish = make_contract_publish( - &MOCKNET_PRIVATE_KEY_1, - LAYER_1_CHAIN_ID_TESTNET, - l1_nonce, - 1_000_000, - config.burnchain.contract_identifier.name.as_str(), - &format!( - "{}\n (as-contract (setup-allowed-contracts))", - contract_content - ), - ); - l1_nonce += 1; - - submit_tx(l1_rpc_origin, &trait_publish); submit_tx(l1_rpc_origin, &ft_publish); - // Because the nonce ensures that the trait contracts - // are published before the subnet contract, we can broadcast them - // all at once, even though the subnet contract depends on those - // contracts. - submit_tx(l1_rpc_origin, &subnet_contract_publish); println!("Submitted ft and Subnet contracts onto L1!"); @@ -2833,65 +2788,9 @@ fn ft_deposit_and_withdraw_integration_test() { "Miner should have produced at least 2 coinbase transactions" ); - // Publish the traits onto the subnet - let l2_trait_publish = make_contract_publish( - &MOCKNET_PRIVATE_KEY_1, - config.node.chain_id, - l2_nonce, - 1_000_000, - &trait_standards_contract_name, - &trait_content, - ); - l2_nonce += 1; - // Publish subnet contract for ft-token - let subnet_simple_ft = " - (impl-trait .trait-standards.ft-trait) - - (define-constant ERR_NOT_AUTHORIZED (err u1001)) - - (define-non-fungible-token ft-token uint) - (define-data-var lastId uint u0) - - ;; ft trait implementation - (define-read-only (get-last-token-id) - (ok (var-get lastId)) - ) - - (define-read-only (get-owner (id uint)) - (ok (ft-get-owner? ft-token id)) - ) - - (define-read-only (get-token-uri (id uint)) - (ok none) - ) - - (define-public (transfer (id uint) (sender principal) (recipient principal)) - (begin - (asserts! (is-eq tx-sender sender) ERR_NOT_AUTHORIZED) - (ft-transfer? ft-token id sender recipient) - ) - ) - - ;; Other methods - (define-public (mint-on-subnet (id uint) (recipient principal)) - (begin - (var-set lastId id) - (ft-mint? ft-token id recipient) - ) - ) - - (define-public (subnet-deposit-ft-token (id uint) (recipient principal)) - (begin - (var-set lastId id) - (ft-mint? ft-token id recipient) - ) - ) - - (define-read-only (get-token-owner (id uint)) - (ft-get-owner? ft-token id) - ) - "; + let subnet_simple_ft = + include_str!("../../../../core-contracts/contracts/helper/simple-ft-l2.clar"); let subnet_ft_publish = make_contract_publish( &MOCKNET_PRIVATE_KEY_1, config.node.chain_id, @@ -2904,14 +2803,31 @@ fn ft_deposit_and_withdraw_integration_test() { let subnet_ft_contract_id = QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")); - submit_tx(&l2_rpc_origin, &l2_trait_publish); submit_tx(&l2_rpc_origin, &subnet_ft_publish); // Sleep to give the run loop time to mine a block wait_for_next_stacks_block(&sortition_db); wait_for_next_stacks_block(&sortition_db); - // Mint a ft-token for user on L1 chain (ID = 1) + // Register the contract with the subnet + let subnet_setup_ft_tx = make_contract_call( + &MOCKNET_PRIVATE_KEY_1, + LAYER_1_CHAIN_ID_TESTNET, + l1_nonce, + 1_000_000, + &user_addr, + config.burnchain.contract_identifier.name.as_str(), + "register-new-ft-contract", + &[ + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::Principal(PrincipalData::Contract(subnet_ft_contract_id.clone())), + ], + ); + l1_nonce += 1; + + submit_tx(l1_rpc_origin, &subnet_setup_ft_tx); + + // Mint 2 ft-tokens for user on L1 chain let l1_mint_ft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, LAYER_1_CHAIN_ID_TESTNET, @@ -2919,12 +2835,12 @@ fn ft_deposit_and_withdraw_integration_test() { 1_000_000, &user_addr, "simple-ft", - "test-mint", - &[Value::Principal(user_addr.into())], + "gift-tokens", + &[Value::UInt(2), Value::Principal(user_addr.into())], ); l1_nonce += 1; - // Mint a ft-token for user on subnet (ID = 5) + // Mint 5 ft-tokens for user on subnet let l2_mint_ft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, config.node.chain_id, @@ -2932,7 +2848,7 @@ fn ft_deposit_and_withdraw_integration_test() { 1_000_000, &user_addr, "simple-ft", - "mint-on-subnet", + "gift-tokens", &[Value::UInt(5), Value::Principal(user_addr.into())], ); l2_nonce += 1; @@ -2949,8 +2865,8 @@ fn ft_deposit_and_withdraw_integration_test() { &l2_rpc_origin, &user_addr, "simple-ft", - "get-token-owner", - vec![Value::UInt(1).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -2960,11 +2876,11 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let addr = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); - assert_eq!(addr, Value::none()); + assert_eq!(amount, Value::okay(Value::UInt(5)).unwrap()); let l1_deposit_ft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, @@ -2975,16 +2891,16 @@ fn ft_deposit_and_withdraw_integration_test() { config.burnchain.contract_identifier.name.as_str(), "deposit-ft-asset", &[ + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), Value::UInt(1), Value::Principal(user_addr.into()), - Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), - Value::Principal(PrincipalData::Contract(subnet_ft_contract_id.clone())), + Value::none(), ], ); l1_nonce += 1; - // deposit ft-token into subnet contract on L1 - submit_tx(&l1_rpc_origin, &l1_deposit_ft_tx); + // deposit 1 ft-token into subnet contract on L1 + let tx_res = submit_tx(&l1_rpc_origin, &l1_deposit_ft_tx); // Sleep to give the run loop time to mine a block wait_for_next_stacks_block(&sortition_db); @@ -2995,8 +2911,8 @@ fn ft_deposit_and_withdraw_integration_test() { &l2_rpc_origin, &user_addr, "simple-ft", - "get-token-owner", - vec![Value::UInt(1).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3006,22 +2922,19 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let addr = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - ); - assert_eq!( - addr, - Value::some(Value::Principal(user_addr.into())).unwrap() + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); + assert_eq!(amount, Value::okay(Value::UInt(6)).unwrap()); - // Check that the user does not own the L1 native ft on the L1 anymore (the contract should own it) + // Check that the user now only owns 1 ft on the L1 let res = call_read_only( &l1_rpc_origin, &user_addr, "simple-ft", - "get-owner", - vec![Value::UInt(1).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3031,32 +2944,22 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let owner = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); - let subnet_contract_principal = Value::okay( - Value::some(Value::Principal(PrincipalData::Contract( - QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("subnet-controller"), - ), - ))) - .unwrap(), - ) - .unwrap(); - assert_eq!(owner, subnet_contract_principal); + assert_eq!(amount, Value::okay(Value::UInt(1)).unwrap()); - // Check that the no one owns the subnet native ft on the L1 + // Check that the subnet contract owns 1 ft on the L1 + let subnet_contract_principal = Value::Principal(PrincipalData::Contract( + config.burnchain.contract_identifier.clone(), + )); let res = call_read_only( &l1_rpc_origin, &user_addr, "simple-ft", - "get-owner", - vec![Value::UInt(5).serialize()], + "get-balance", + vec![subnet_contract_principal.serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3066,16 +2969,13 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let owner = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); - assert_eq!(owner, Value::okay(Value::none()).unwrap()); + assert_eq!(amount, Value::okay(Value::UInt(1)).unwrap()); - // Withdraw the L1 native ft from the L2 (with `ft-withdraw?`) + // Withdraw the 4 (out of 6) of the ft-tokens from the L2 (with `ft-withdraw?`) let l2_withdraw_ft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, config.node.chain_id, @@ -3089,46 +2989,26 @@ fn ft_deposit_and_withdraw_integration_test() { user_addr.into(), ContractName::from("simple-ft"), ))), - Value::UInt(1), - Value::Principal(user_addr.into()), - ], - ); - l2_nonce += 1; - // Withdraw the subnet native ft from the L2 (with `ft-withdraw?`) - let l2_withdraw_native_ft_tx = make_contract_call( - &MOCKNET_PRIVATE_KEY_1, - config.node.chain_id, - l2_nonce, - 1_000_000, - &boot_code_addr(false), - "subnet", - "ft-withdraw?", - &[ - Value::Principal(PrincipalData::Contract(QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("simple-ft"), - ))), - Value::UInt(5), + Value::UInt(4), Value::Principal(user_addr.into()), ], ); l2_nonce += 1; - // Submit withdrawal function calls + + // Submit withdrawal function call submit_tx(&l2_rpc_origin, &l2_withdraw_ft_tx); - submit_tx(&l2_rpc_origin, &l2_withdraw_native_ft_tx); // Sleep to give the run loop time to mine a block wait_for_next_stacks_block(&sortition_db); wait_for_next_stacks_block(&sortition_db); - // Check that user no longer owns the l1 native ft on L2 chain, - // instead, the subnet contract should own it. + // Check that user owns the remainder of the tokens on the subnet let res = call_read_only( &l2_rpc_origin, &user_addr, "simple-ft", - "get-token-owner", - vec![Value::UInt(1).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3138,87 +3018,19 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let addr = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - ); - assert_eq!( - addr, - Value::some(Value::Principal(PrincipalData::Contract( - boot_code_id("subnet".into(), false).into() - ))) - .unwrap() - ); - // Check that user no longer owns the subnet native ft on L2 chain, - // instead, the subnet contract should own it. - let res = call_read_only( - &l2_rpc_origin, - &user_addr, - "simple-ft", - "get-token-owner", - vec![Value::UInt(5).serialize()], - ); - assert!(res.get("cause").is_none()); - assert!(res["okay"].as_bool().unwrap()); - let result = res["result"] - .as_str() - .unwrap() - .strip_prefix("0x") - .unwrap() - .to_string(); - let addr = Value::deserialize( - &result, - &TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - ); - assert_eq!( - addr, - Value::some(Value::Principal(PrincipalData::Contract( - boot_code_id("subnet".into(), false).into() - ))) - .unwrap() + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); + assert_eq!(amount, Value::okay(Value::UInt(2)).unwrap()); - // Check that the user does not *yet* own the L1 native ft on the L1 (the contract should still own it) - let res = call_read_only( - &l1_rpc_origin, - &user_addr, - "simple-ft", - "get-owner", - vec![Value::UInt(1).serialize()], - ); - assert!(res.get("cause").is_none()); - assert!(res["okay"].as_bool().unwrap()); - let result = res["result"] - .as_str() - .unwrap() - .strip_prefix("0x") - .unwrap() - .to_string(); - let owner = Value::deserialize( - &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), - ); - let subnet_contract_principal = Value::okay( - Value::some(Value::Principal(PrincipalData::Contract( - QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("subnet-controller"), - ), - ))) - .unwrap(), - ) - .unwrap(); - assert_eq!(owner, subnet_contract_principal); - // Check that the user does not *yet* own the subnet native ft on the L1 (no one should own it) + // Check that the user does not *yet* own the additional ft tokens on the L1 let res = call_read_only( &l1_rpc_origin, &user_addr, "simple-ft", - "get-owner", - vec![Value::UInt(5).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3228,14 +3040,11 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let owner = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); - assert_eq!(owner, Value::okay(Value::none()).unwrap()); + assert_eq!(amount, Value::okay(Value::UInt(1)).unwrap()); let block_data = test_observer::get_blocks(); let mut withdraw_events = filter_map_events(&block_data, |height, event| { @@ -3265,44 +3074,19 @@ fn ft_deposit_and_withdraw_integration_test() { None } }); - assert_eq!(withdraw_events.len(), 2); + assert_eq!(withdraw_events.len(), 1); let (withdrawal_height, _withdrawal) = withdraw_events.pop().unwrap(); - let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( - &l2_rpc_origin, - withdrawal_height, - &user_addr, - 0, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 1, - ); - let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( - &l2_rpc_origin, - withdrawal_height, - &user_addr, - 1, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 5, - ); - - let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( + let ft_withdrawal_entry = get_ft_withdrawal_entry( &l2_rpc_origin, withdrawal_height, &user_addr, 0, QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 1, - ); - let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( - &l2_rpc_origin, - withdrawal_height, - &user_addr, - 1, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 5, + 4, ); - // Create the withdrawal merkle tree by mocking both ft withdraw events (if the root hash of + // Create the withdrawal merkle tree by mocking the ft withdraw event (if the root hash of // this constructed merkle tree is not identical to the root hash published by the subnet node, // then the test will fail). let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh( @@ -3312,34 +3096,7 @@ fn ft_deposit_and_withdraw_integration_test() { spending_condition.set_nonce(l2_nonce - 1); spending_condition.set_tx_fee(1000); let auth = TransactionAuth::Standard(spending_condition); - let mut l1_native_ft_withdraw_event = - StacksTransactionEvent::SmartContractEvent(SmartContractEventData { - key: (boot_code_id("subnet".into(), false), "print".into()), - value: Value::Tuple( - TupleData::from_data(vec![ - ( - "type".into(), - Value::string_ascii_from_bytes("ft".to_string().into_bytes()).unwrap(), - ), - ( - "asset-contract".into(), - Value::Principal(PrincipalData::Contract( - QualifiedContractIdentifier::new( - user_addr.into(), - ContractName::from("simple-ft"), - ), - )), - ), - ( - "sender".into(), - Value::Principal(PrincipalData::Standard(user_addr.into())), - ), - ("id".into(), Value::UInt(1)), - ]) - .expect("Failed to create tuple data."), - ), - }); - let mut subnet_native_ft_withdraw_event = + let mut ft_withdraw_event = StacksTransactionEvent::SmartContractEvent(SmartContractEventData { key: (boot_code_id("subnet".into(), false), "print".into()), value: Value::Tuple( @@ -3361,7 +3118,7 @@ fn ft_deposit_and_withdraw_integration_test() { "sender".into(), Value::Principal(PrincipalData::Standard(user_addr.into())), ), - ("id".into(), Value::UInt(5)), + ("amount".into(), Value::UInt(4)), ]) .expect("Failed to create tuple data."), ), @@ -3372,10 +3129,7 @@ fn ft_deposit_and_withdraw_integration_test() { auth.clone(), TransactionPayload::Coinbase(CoinbasePayload([0u8; 32])), )), - events: vec![ - l1_native_ft_withdraw_event.clone(), - subnet_native_ft_withdraw_event.clone(), - ], + events: vec![ft_withdraw_event.clone()], post_condition_aborted: false, result: Value::err_none(), stx_burned: 0, @@ -3388,104 +3142,17 @@ fn ft_deposit_and_withdraw_integration_test() { create_withdrawal_merkle_tree(&mut vec![withdrawal_receipt], withdrawal_height); let root_hash = withdrawal_tree.root().as_bytes().to_vec(); - let l1_native_ft_withdrawal_key = - generate_key_from_event(&mut l1_native_ft_withdraw_event, 0, withdrawal_height).unwrap(); - let l1_native_ft_withdrawal_key_bytes = - convert_withdrawal_key_to_bytes(&l1_native_ft_withdrawal_key); - let l1_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( - l1_native_ft_withdrawal_key_bytes.as_slice(), - ) - .as_bytes() - .to_vec(); - let l1_native_ft_path = withdrawal_tree - .path(&l1_native_ft_withdrawal_key_bytes) - .unwrap(); - - let mut l1_native_ft_sib_data = Vec::new(); - for (_i, sib) in l1_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - l1_native_ft_sib_data.push(sib_tuple); - } - - let l1_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); - let l1_native_leaf_hash_val = - Value::buff_from(l1_native_ft_withdrawal_leaf_hash.clone()).unwrap(); - let l1_native_siblings_val = Value::list_from(l1_native_ft_sib_data.clone()).unwrap(); - - assert_eq!( - &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, - "Root hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, - "Leaf hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, - "Sibling hashes should match value returned via RPC" - ); - assert_eq!( - &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, - "Root hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, - "Leaf hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, - "Sibling hashes should match value returned via RPC" - ); - - let subnet_native_ft_withdrawal_key = - generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) - .unwrap(); - let subnet_native_ft_withdrawal_key_bytes = - convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); - let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( - subnet_native_ft_withdrawal_key_bytes.as_slice(), - ) - .as_bytes() - .to_vec(); - let subnet_native_ft_path = withdrawal_tree - .path(&subnet_native_ft_withdrawal_key_bytes) - .unwrap(); - - let subnet_native_ft_withdrawal_key = - generate_key_from_event(&mut subnet_native_ft_withdraw_event, 1, withdrawal_height) - .unwrap(); - let subnet_native_ft_withdrawal_key_bytes = - convert_withdrawal_key_to_bytes(&subnet_native_ft_withdrawal_key); - let subnet_native_ft_withdrawal_leaf_hash = MerkleTree::::get_leaf_hash( - subnet_native_ft_withdrawal_key_bytes.as_slice(), - ) - .as_bytes() - .to_vec(); - let subnet_native_ft_path = withdrawal_tree - .path(&subnet_native_ft_withdrawal_key_bytes) - .unwrap(); + let ft_withdrawal_key = + generate_key_from_event(&mut ft_withdraw_event, 0, withdrawal_height).unwrap(); + let ft_withdrawal_key_bytes = convert_withdrawal_key_to_bytes(&ft_withdrawal_key); + let ft_withdrawal_leaf_hash = + MerkleTree::::get_leaf_hash(ft_withdrawal_key_bytes.as_slice()) + .as_bytes() + .to_vec(); + let ft_path = withdrawal_tree.path(&ft_withdrawal_key_bytes).unwrap(); - let mut subnet_native_ft_sib_data = Vec::new(); - for (_i, sib) in subnet_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - subnet_native_ft_sib_data.push(sib_tuple); - } - let mut subnet_native_ft_sib_data = Vec::new(); - for (_i, sib) in subnet_native_ft_path.iter().enumerate() { + let mut ft_sib_data = Vec::new(); + for (_i, sib) in ft_path.iter().enumerate() { let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); // the sibling's side is the opposite of what PathOrder is set to let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); @@ -3494,60 +3161,41 @@ fn ft_deposit_and_withdraw_integration_test() { (ClarityName::from("is-left-side"), sib_is_left), ]; let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - subnet_native_ft_sib_data.push(sib_tuple); + ft_sib_data.push(sib_tuple); } - let subnet_native_root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); - let subnet_native_leaf_hash_val = - Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); - let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); + let root_hash_val = Value::buff_from(root_hash.clone()).unwrap(); + let leaf_hash_val = Value::buff_from(ft_withdrawal_leaf_hash.clone()).unwrap(); + let siblings_val = Value::list_from(ft_sib_data.clone()).unwrap(); assert_eq!( - &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, + &root_hash_val, &ft_withdrawal_entry.root_hash, "Root hash should match value returned via RPC" ); assert_eq!( - &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, + &leaf_hash_val, &ft_withdrawal_entry.leaf_hash, "Leaf hash should match value returned via RPC" ); assert_eq!( - &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, + &siblings_val, &ft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - - let mut subnet_native_ft_sib_data = Vec::new(); - for (_i, sib) in subnet_native_ft_path.iter().enumerate() { - let sib_hash = Value::buff_from(sib.hash.as_bytes().to_vec()).unwrap(); - // the sibling's side is the opposite of what PathOrder is set to - let sib_is_left = Value::Bool(sib.order == MerklePathOrder::Right); - let curr_sib_data = vec![ - (ClarityName::from("hash"), sib_hash), - (ClarityName::from("is-left-side"), sib_is_left), - ]; - let sib_tuple = Value::Tuple(TupleData::from_data(curr_sib_data).unwrap()); - subnet_native_ft_sib_data.push(sib_tuple); - } - - let subnet_native_leaf_hash_val = - Value::buff_from(subnet_native_ft_withdrawal_leaf_hash.clone()).unwrap(); - let subnet_native_siblings_val = Value::list_from(subnet_native_ft_sib_data.clone()).unwrap(); - assert_eq!( - &subnet_native_root_hash_val, &subnet_native_ft_withdrawal_entry.root_hash, + &root_hash_val, &ft_withdrawal_entry.root_hash, "Root hash should match value returned via RPC" ); assert_eq!( - &subnet_native_leaf_hash_val, &subnet_native_ft_withdrawal_entry.leaf_hash, + &leaf_hash_val, &ft_withdrawal_entry.leaf_hash, "Leaf hash should match value returned via RPC" ); assert_eq!( - &subnet_native_siblings_val, &subnet_native_ft_withdrawal_entry.siblings, + &siblings_val, &ft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); // TODO: call withdraw from unauthorized principal once leaf verification is added to the subnet contract - let l1_withdraw_l1_native_ft_tx = make_contract_call( + let l1_withdraw_ft_tx = make_contract_call( &MOCKNET_PRIVATE_KEY_1, LAYER_1_CHAIN_ID_TESTNET, l1_nonce, @@ -3556,60 +3204,37 @@ fn ft_deposit_and_withdraw_integration_test() { config.burnchain.contract_identifier.name.as_str(), "withdraw-ft-asset", &[ - Value::UInt(1), + Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::UInt(4), Value::Principal(user_addr.into()), Value::UInt(0), Value::UInt(withdrawal_height.into()), - Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), + Value::none(), Value::some(Value::Principal(PrincipalData::Contract( ft_contract_id.clone(), ))) .unwrap(), Value::buff_from(root_hash.clone()).unwrap(), - Value::buff_from(l1_native_ft_withdrawal_leaf_hash).unwrap(), - Value::list_from(l1_native_ft_sib_data).unwrap(), - ], - ); - l1_nonce += 1; - let l1_withdraw_subnet_native_ft_tx = make_contract_call( - &MOCKNET_PRIVATE_KEY_1, - LAYER_1_CHAIN_ID_TESTNET, - l1_nonce, - 1_000_000, - &user_addr, - config.burnchain.contract_identifier.name.as_str(), - "withdraw-ft-asset", - &[ - Value::UInt(5), - Value::Principal(user_addr.into()), - Value::UInt(1), - Value::UInt(withdrawal_height.into()), - Value::Principal(PrincipalData::Contract(ft_contract_id.clone())), - Value::some(Value::Principal(PrincipalData::Contract( - ft_contract_id.clone(), - ))) - .unwrap(), - Value::buff_from(root_hash).unwrap(), - Value::buff_from(subnet_native_ft_withdrawal_leaf_hash).unwrap(), - Value::list_from(subnet_native_ft_sib_data).unwrap(), + Value::buff_from(ft_withdrawal_leaf_hash).unwrap(), + Value::list_from(ft_sib_data).unwrap(), ], ); l1_nonce += 1; + // Withdraw ft-token from subnet contract on L1 - submit_tx(&l1_rpc_origin, &l1_withdraw_l1_native_ft_tx); - submit_tx(&l1_rpc_origin, &l1_withdraw_subnet_native_ft_tx); + submit_tx(&l1_rpc_origin, &l1_withdraw_ft_tx); // Sleep to give the run loop time to mine a block wait_for_next_stacks_block(&sortition_db); wait_for_next_stacks_block(&sortition_db); - // Check that the user owns the L1 native ft on the L1 chain now + // Check that the user owns the tokens on the L1 chain now let res = call_read_only( &l1_rpc_origin, &user_addr, "simple-ft", - "get-owner", - vec![Value::UInt(1).serialize()], + "get-balance", + vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3619,24 +3244,20 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let owner = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), - ); - assert_eq!( - owner, - Value::okay(Value::some(Value::Principal(user_addr.into())).unwrap()).unwrap() + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); - // Check that the user owns the subnet native ft on the L1 chain now + assert_eq!(amount, Value::okay(Value::UInt(5)).unwrap()); + + // Check that the subnet contract no longer owns any tokens. It should have + // transferred the 1 that it had, then minted the remaining 3. let res = call_read_only( &l1_rpc_origin, &user_addr, "simple-ft", - "get-owner", - vec![Value::UInt(5).serialize()], + "get-balance", + vec![subnet_contract_principal.serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); @@ -3646,17 +3267,11 @@ fn ft_deposit_and_withdraw_integration_test() { .strip_prefix("0x") .unwrap() .to_string(); - let owner = Value::deserialize( + let amount = Value::deserialize( &result, - &TypeSignature::ResponseType(Box::new(( - TypeSignature::OptionalType(Box::new(TypeSignature::PrincipalType)), - TypeSignature::UIntType, - ))), - ); - assert_eq!( - owner, - Value::okay(Value::some(Value::Principal(user_addr.into())).unwrap()).unwrap() + &TypeSignature::ResponseType(Box::new((TypeSignature::UIntType, TypeSignature::UIntType))), ); + assert_eq!(amount, Value::okay(Value::UInt(0)).unwrap()); termination_switch.store(false, Ordering::SeqCst); stacks_l1_controller.kill_process(); diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 49c956139..f47559fa7 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -714,7 +714,7 @@ pub fn get_ft_withdrawal_entry( sender: F, withdrawal_id: u32, contract_identifier: QualifiedContractIdentifier, - id: u64, + amount: u64, ) -> WithdrawalEntry { let client = reqwest::blocking::Client::new(); let path = format!( @@ -725,7 +725,7 @@ pub fn get_ft_withdrawal_entry( withdrawal_id, StacksAddress::from(contract_identifier.issuer), contract_identifier.name.as_str(), - id + amount ); let res = client From c4b06ec3a67b6c2c614dccc5b5ed643afa7e4080 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 22 Feb 2023 12:53:17 -0500 Subject: [PATCH 11/12] fix: update clarity unit tests --- core-contracts/tests/subnets/subnet_test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core-contracts/tests/subnets/subnet_test.ts b/core-contracts/tests/subnets/subnet_test.ts index efbeb2906..464ea4f93 100644 --- a/core-contracts/tests/subnets/subnet_test.ts +++ b/core-contracts/tests/subnets/subnet_test.ts @@ -604,7 +604,7 @@ Clarinet.test({ Tx.contractCall( "simple-ft", "gift-tokens", - [types.principal(charlie.address)], + [types.uint(1), types.principal(charlie.address)], charlie.address ), ]); @@ -614,7 +614,7 @@ Clarinet.test({ Tx.contractCall( "simple-ft", "gift-tokens", - [types.principal(charlie.address)], + [types.uint(1), types.principal(charlie.address)], charlie.address ), ]); @@ -844,7 +844,7 @@ Clarinet.test({ Tx.contractCall( "simple-ft", "gift-tokens", - [types.principal(charlie.address)], + [types.uint(1), types.principal(charlie.address)], charlie.address ), ]); @@ -1055,7 +1055,7 @@ Clarinet.test({ Tx.contractCall( "simple-ft", "gift-tokens", - [types.principal(charlie.address)], + [types.uint(1), types.principal(charlie.address)], charlie.address ), ]); @@ -1065,7 +1065,7 @@ Clarinet.test({ Tx.contractCall( "simple-ft", "gift-tokens", - [types.principal(charlie.address)], + [types.uint(1), types.principal(charlie.address)], charlie.address ), ]); From 63827c5a814bcbbf3652504146e5fbb298c6bea7 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 22 Feb 2023 13:37:19 -0500 Subject: [PATCH 12/12] fix: cleanup tests --- .../stacks-node/src/tests/l1_observer_test.rs | 39 +------------------ 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/testnet/stacks-node/src/tests/l1_observer_test.rs b/testnet/stacks-node/src/tests/l1_observer_test.rs index ec38a0fd3..143636f1e 100644 --- a/testnet/stacks-node/src/tests/l1_observer_test.rs +++ b/testnet/stacks-node/src/tests/l1_observer_test.rs @@ -665,7 +665,7 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &l2_rpc_origin, &user_addr, "simple-ft", - "get-token-balance", + "get-balance", vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); @@ -750,17 +750,11 @@ fn l1_deposit_and_withdraw_asset_integration_test() { &l2_rpc_origin, &user_addr, "simple-ft", - "get-token-balance", + "get-balance", vec![Value::Principal(user_addr.into()).serialize()], ); assert!(res.get("cause").is_none()); assert!(res["okay"].as_bool().unwrap()); - let result = res["result"] - .as_str() - .unwrap() - .strip_prefix("0x") - .unwrap() - .to_string(); let result = res["result"] .as_str() .unwrap() @@ -2351,23 +2345,6 @@ fn nft_deposit_and_withdraw_integration_test() { 5, ); - let l1_native_ft_withdrawal_entry = get_ft_withdrawal_entry( - &l2_rpc_origin, - withdrawal_height, - &user_addr, - 0, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 1, - ); - let subnet_native_ft_withdrawal_entry = get_ft_withdrawal_entry( - &l2_rpc_origin, - withdrawal_height, - &user_addr, - 1, - QualifiedContractIdentifier::new(user_addr.into(), ContractName::from("simple-ft")), - 5, - ); - // Create the withdrawal merkle tree by mocking both nft withdraw events (if the root hash of // this constructed merkle tree is not identical to the root hash published by the subnet node, // then the test will fail). @@ -2497,18 +2474,6 @@ fn nft_deposit_and_withdraw_integration_test() { &l1_native_siblings_val, &l1_native_nft_withdrawal_entry.siblings, "Sibling hashes should match value returned via RPC" ); - assert_eq!( - &l1_native_root_hash_val, &l1_native_ft_withdrawal_entry.root_hash, - "Root hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_leaf_hash_val, &l1_native_ft_withdrawal_entry.leaf_hash, - "Leaf hash should match value returned via RPC" - ); - assert_eq!( - &l1_native_siblings_val, &l1_native_ft_withdrawal_entry.siblings, - "Sibling hashes should match value returned via RPC" - ); let subnet_native_nft_withdrawal_key = generate_key_from_event(&mut subnet_native_nft_withdraw_event, 1, withdrawal_height)