From a2fb19c79b9d5553aef15b1362e8796f7a546c1a Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Mon, 18 Oct 2021 11:57:45 +0300 Subject: [PATCH 1/7] pools: Fetch AccountId of the smart contract from ChainExtension Instead of allowing the smart contract caller to define the AccountId of the contract, fetch the value from the ChainExtension's runtime memory. This allows the programmable pool to force the smart contract to only spend its own funds. --- pallets/pp/src/lib.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index 36055b8..226f8a0 100644 --- a/pallets/pp/src/lib.rs +++ b/pallets/pp/src/lib.rs @@ -27,7 +27,7 @@ mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -use codec::Encode; +use codec::{Decode, Encode}; pub use frame_support::{ construct_runtime, dispatch::Vec, @@ -267,10 +267,18 @@ impl ChainExtension for Pallet< where ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, { + // Fetch AccountId of the caller from the ChainExtension's memory + // This way the progrmmable pool can force the caller of the ChainExtension + // to only spend their own funds as `ContractBalances` will be queried + // using `acc_id` and user cannot control the value of this variable + let mut env = env.buf_in_buf_out(); + let acc_id = env.ext().address().encode(); + let acc_id: T::AccountId = T::AccountId::decode(&mut &acc_id[..]) + .map_err(|_| "Failed to get smart contract's AccountId")?; + match func_id { x if x == ChainExtensionCall::Transfer as u32 => { - let mut env = env.buf_in_buf_out(); - let (acc_id, dest, value): (T::AccountId, T::AccountId, u128) = env.read_as()?; + let (dest, value): (T::AccountId, u128) = env.read_as()?; if !>::get(&dest).is_none() { return Err(DispatchError::Other( @@ -281,9 +289,6 @@ impl ChainExtension for Pallet< send_p2pk_tx::(&acc_id, &dest, value)? } x if x == ChainExtensionCall::Balance as u32 => { - let mut env = env.buf_in_buf_out(); - let acc_id: T::AccountId = env.read_as()?; - let fund_info = >::get(&acc_id).ok_or(DispatchError::Other( "Contract doesn't own any UTXO or it doesn't exist!", ))?; @@ -294,13 +299,8 @@ impl ChainExtension for Pallet< x if x == ChainExtensionCall::Call as u32 => { // `read_as_unbounded()` has to be used here because the size of `data` // is only known during runtime - let mut env = env.buf_in_buf_out(); - let (acc_id, dest, selector, mut data): ( - T::AccountId, - T::AccountId, - [u8; 4], - Vec, - ) = env.read_as_unbounded(env.in_len())?; + let (dest, selector, mut data): (T::AccountId, [u8; 4], Vec) = + env.read_as_unbounded(env.in_len())?; if >::get(&dest).is_none() { return Err(DispatchError::Other("Destination doesn't exist")); From 46d6347fe328452163fe0c965483fe825b8dc0b8 Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Mon, 18 Oct 2021 16:59:34 +0300 Subject: [PATCH 2/7] utxo: Improve Destination::CallPP spending validation When PP is constructing the final TX that spends the smart contract's funds, save the outpoint hashes of the vins to a separate storage. This indicates to the local node that the smart contract has spent its funds and when it receives a TX trying to spend CallPPs of a given smart contract, the outpoint hashes in the witness field can be queried from this storage and if they are found, the TX is valid (or at least the local node agrees with the TX and accepts it). --- pallets/pp/src/lib.rs | 4 +- pallets/utxo/src/lib.rs | 80 +++++++++++++++++++++----------------- traits/utxo-api/src/lib.rs | 4 +- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index 226f8a0..d8e6e31 100644 --- a/pallets/pp/src/lib.rs +++ b/pallets/pp/src/lib.rs @@ -144,7 +144,7 @@ fn send_p2pk_tx( ensure!(fund_info.funds >= value, "Caller doesn't have enough funds"); let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::send_conscrit_p2pk(caller, dest, value, &outpoints) + T::Utxo::submit_c2pk_tx(caller, dest, value, &outpoints) } /// Create Contract-to-Contract transfer that allows smart contracts to @@ -169,7 +169,7 @@ fn send_c2c_tx( ))?; let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::send_conscrit_c2c(caller, dest, fund_info.funds, data, &outpoints) + T::Utxo::submit_c2c_tx(caller, dest, fund_info.funds, data, &outpoints) } impl ProgrammablePoolApi for Pallet diff --git a/pallets/utxo/src/lib.rs b/pallets/utxo/src/lib.rs index 1b16303..0bc1c2e 100644 --- a/pallets/utxo/src/lib.rs +++ b/pallets/utxo/src/lib.rs @@ -515,6 +515,10 @@ pub mod pallet { pub(super) type StakingCount = StorageMap<_, Identity, T::AccountId, (u64, Value), OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn ectl_store)] + pub(super) type EctlStore = StorageMap<_, Identity, H256, bool>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::metadata(T::AccountId = "AccountId")] @@ -1040,13 +1044,22 @@ pub mod pallet { log::info!("TODO validate spending of OP_CREATE"); } Destination::CallPP(_, _, _) => { - let spend = - u16::from_le_bytes(input.witness[1..].try_into().or_else(|_| { - Err(DispatchError::Other( - "Failed to convert witness to an opcode", - )) - })?); - ensure!(spend == 0x1337, "OP_SPEND not found"); + // 32-byte hash + 1 byte length + ensure!( + input.witness.len() == 33, + "Witness field doesn't contain valid data" + ); + + let hash: [u8; 32] = input.witness[1..] + .try_into() + .map_err(|_| DispatchError::Other("Failed to convert the slice"))?; + + ensure!( + >::get(&H256::from(hash)).is_some(), + "Transaction does not have access to smart contract outputs" + ); + + >::remove(&H256::from(hash)); } Destination::ScriptHash(_hash) => { let witness = input.witness.clone(); @@ -1371,10 +1384,11 @@ impl crate::Pallet { // } } -fn coin_picker(outpoints: &Vec) -> Result, DispatchError> { +fn construct_inputs( + outpoints: &Vec, +) -> Result, DispatchError> { let mut inputs: Vec = Vec::new(); - // consensus-critical sorting function... let mut outpoints = outpoints.clone(); outpoints.sort(); @@ -1385,8 +1399,13 @@ fn coin_picker(outpoints: &Vec) -> Result inputs.push(TransactionInput::new_script( *outpoint, Builder::new().into_script(), - Builder::new().push_int(0x1337).into_script(), + Builder::new().push_slice(&outpoint.encode()).into_script(), )); + + // save the outpoint hash of the input UTXO to ECTL + // from which it can be fetched for validation when + // the node receives a TX that tries to spend OP_CALLs + >::insert(outpoint, true); } _ => { return Err(DispatchError::Other("Only CallPP vouts can be spent!")); @@ -1430,7 +1449,7 @@ where staking::withdraw::(stash_account_caller.clone()) } - fn send_conscrit_p2pk( + fn submit_c2pk_tx( caller: &T::AccountId, dest: &T::AccountId, value: u128, @@ -1439,39 +1458,30 @@ where let pubkey_raw: [u8; 32] = dest.encode().try_into().map_err(|_| "Failed to get caller's public key")?; - spend::( - caller, - &Transaction { - inputs: coin_picker::(outpoints)?, - outputs: vec![TransactionOutput::new_pubkey(value, H256::from(pubkey_raw))], - time_lock: Default::default(), - }, - ) - .map_err(|_| "Failed to spend the transaction!")?; + let tx = Transaction { + inputs: construct_inputs::(outpoints)?, + outputs: vec![TransactionOutput::new_pubkey(value, H256::from(pubkey_raw))], + time_lock: Default::default(), + }; + + spend::(caller, &tx).map_err(|_| "Failed to spend the transaction!")?; Ok(()) } - fn send_conscrit_c2c( + fn submit_c2c_tx( caller: &Self::AccountId, dest: &Self::AccountId, value: u128, data: &Vec, outpoints: &Vec, ) -> Result<(), DispatchError> { - spend::( - caller, - &Transaction { - inputs: coin_picker::(outpoints)?, - outputs: vec![TransactionOutput::new_call_pp( - value, - dest.clone(), - true, - data.clone(), - )], - time_lock: Default::default(), - }, - ) - .map_err(|_| "Failed to spend the transaction!")?; + let tx = Transaction { + inputs: construct_inputs::(outpoints)?, + outputs: vec![TransactionOutput::new_call_pp(value, dest.clone(), true, data.clone())], + time_lock: Default::default(), + }; + + spend::(caller, &tx).map_err(|_| "Failed to spend the transaction!")?; Ok(()) } } diff --git a/traits/utxo-api/src/lib.rs b/traits/utxo-api/src/lib.rs index e345ae9..b2de034 100644 --- a/traits/utxo-api/src/lib.rs +++ b/traits/utxo-api/src/lib.rs @@ -39,14 +39,14 @@ pub trait UtxoApi { fn withdraw_stake(stash_account_caller: &Self::AccountId) -> DispatchResultWithPostInfo; - fn send_conscrit_p2pk( + fn submit_c2pk_tx( caller: &Self::AccountId, destination: &Self::AccountId, value: u128, outpoints: &Vec, ) -> Result<(), DispatchError>; - fn send_conscrit_c2c( + fn submit_c2c_tx( caller: &Self::AccountId, destination: &Self::AccountId, value: u128, From 1699cfb7c1ac86e9c67c35027546448b6185c2dc Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Tue, 19 Oct 2021 10:52:55 +0300 Subject: [PATCH 3/7] utxo: Return Result from Utxo::create()/call() --- pallets/utxo/src/lib.rs | 64 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/pallets/utxo/src/lib.rs b/pallets/utxo/src/lib.rs index 0bc1c2e..065216d 100644 --- a/pallets/utxo/src/lib.rs +++ b/pallets/utxo/src/lib.rs @@ -565,42 +565,36 @@ pub mod pallet { } pub fn create( - _caller: &T::AccountId, - _code: &Vec, - _utxo_hash: H256, - _utxo_value: u128, - _data: &Vec, - ) { - // let weight: Weight = 6000000000; - - // match T::ProgrammablePool::create(caller, weight, code, utxo_hash, utxo_value, data) { - // Ok(_) => log::info!("success!"), - // Err(e) => log::error!("failure: {:#?}", e), - // } + caller: &T::AccountId, + code: &Vec, + utxo_hash: H256, + utxo_value: u128, + data: &Vec, + ) -> Result<(), &'static str> { + let weight: Weight = 6000000000; + + T::ProgrammablePool::create(caller, weight, code, utxo_hash, utxo_value, data) } pub fn call( - _caller: &T::AccountId, - _dest: &T::AccountId, - _utxo_hash: H256, - _utxo_value: u128, - _fund_contract: bool, - _data: &Vec, - ) { - // let weight: Weight = 6000000000; - - // match T::ProgrammablePool::call( - // caller, - // dest, - // weight, - // utxo_hash, - // utxo_value, - // fund_contract, - // data, - // ) { - // Ok(_) => log::info!("success!"), - // Err(e) => log::error!("failure: {:#?}", e), - // } + caller: &T::AccountId, + dest: &T::AccountId, + utxo_hash: H256, + utxo_value: u128, + fund_contract: bool, + data: &Vec, + ) -> Result<(), &'static str> { + let weight: Weight = 6000000000; + + T::ProgrammablePool::call( + caller, + dest, + weight, + utxo_hash, + utxo_value, + fund_contract, + data, + ) } pub fn validate_transaction( @@ -1152,12 +1146,12 @@ pub mod pallet { Destination::CreatePP(script, data) => { log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); >::insert(hash, output); - create::(caller, script, hash, output.value, &data); + create::(caller, script, hash, output.value, &data)?; } Destination::CallPP(acct_id, fund, data) => { log::debug!("inserting to UtxoStore {:?} as key {:?}", output, hash); >::insert(hash, output); - call::(caller, acct_id, hash, output.value, *fund, data); + call::(caller, acct_id, hash, output.value, *fund, data)?; } Destination::LockForStaking { .. } => { staking::lock_for_staking::(hash, output)?; From 889a1f093b5c9236b43e22283b407d58b78739e8 Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Wed, 20 Oct 2021 13:04:00 +0300 Subject: [PATCH 4/7] tests: Implement get_pubkey() for DestCallPP This allows the test framework to query UTXOs of the smart contract. --- test/functional/test_framework/mintlayer/utxo.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/functional/test_framework/mintlayer/utxo.py b/test/functional/test_framework/mintlayer/utxo.py index 20b512a..d95e5b3 100644 --- a/test/functional/test_framework/mintlayer/utxo.py +++ b/test/functional/test_framework/mintlayer/utxo.py @@ -3,6 +3,7 @@ import substrateinterface from substrateinterface import SubstrateInterface, Keypair from substrateinterface.exceptions import SubstrateRequestException +from substrateinterface.utils.ss58 import ss58_decode import scalecodec import os import logging @@ -50,7 +51,10 @@ def utxos(self, storage_name): """ Get UTXOs for given key """ def utxos_for(self, keypair): - matching = lambda e: e[1].destination.get_pubkey() == keypair.public_key + if type(keypair) == str: + matching = lambda e: e[1].destination.get_pubkey() == keypair + else: + matching = lambda e: e[1].destination.get_pubkey() == keypair.public_key return filter(matching, self.utxos('UtxoStore')) """ Get UTXOs for given key """ @@ -212,11 +216,14 @@ def __init__(self, dest_account, fund, input_data): @staticmethod def load(obj): - return DestCallPP(obj['dest_account'], obj['fund'], obj['input_data']) + return DestCallPP(ss58_decode(obj['dest_account']), obj['fund'], obj['input_data']) def json(self): return { 'CallPP': { 'dest_account': self.acct, 'fund': self.fund, 'input_data': self.data } } + def get_pubkey(self): + return str(self.acct) + class DestLockForStaking(Destination): def __init__(self, stash_account, controller_account, session_key): self.stash = stash_account @@ -248,7 +255,6 @@ def json(self): def get_ss58_address(self): return self.stash - class Output(): def __init__(self, value, destination, data): self.value = value From 9012d3b6b4e736f31ba9b6de20ace2a8e187278e Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Thu, 21 Oct 2021 16:39:03 +0300 Subject: [PATCH 5/7] pools: Prevent contracts from calling themselves Prevent a contract from calling itself through the ChainExtension as the smart contract already has capabilities of calling its own functions even without the existence of the ChainExtension, makes it harder to handle contract balances correctly and makes it very easy for the user to shoot themselves in the foot by accidentally calling the function that called into the ChainExtension, effectively resulting in infite recursion (bounded by the amount of gas, obviously). If this is found to be needed, the issue can be revisited. --- pallets/pp/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index d8e6e31..f00bf4c 100644 --- a/pallets/pp/src/lib.rs +++ b/pallets/pp/src/lib.rs @@ -306,6 +306,10 @@ impl ChainExtension for Pallet< return Err(DispatchError::Other("Destination doesn't exist")); } + if acc_id == dest { + return Err(DispatchError::Other("Contract cannot call itself")); + } + // append data to the selector so the final data // passed on to the contract is in correct format let mut selector = selector.to_vec(); From c1a4b9467a4246c867e0166401fdaba1596880dc Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Thu, 21 Oct 2021 16:43:28 +0300 Subject: [PATCH 6/7] pools: Update smart contract balance after UTXO transfer If the UTXO transfer from contract to P2PK or contract succeeds, zero the balance of the smart contract as the contract no longer holds any funds. --- pallets/pp/src/lib.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index f00bf4c..c92495f 100644 --- a/pallets/pp/src/lib.rs +++ b/pallets/pp/src/lib.rs @@ -144,7 +144,12 @@ fn send_p2pk_tx( ensure!(fund_info.funds >= value, "Caller doesn't have enough funds"); let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::submit_c2pk_tx(caller, dest, value, &outpoints) + T::Utxo::submit_c2pk_tx(caller, dest, value, &outpoints).map(|_| { + >::mutate(&caller, |info| { + info.as_mut().unwrap().utxos = Vec::new(); + info.as_mut().unwrap().funds = 0; + }); + }) } /// Create Contract-to-Contract transfer that allows smart contracts to @@ -169,7 +174,12 @@ fn send_c2c_tx( ))?; let outpoints = fund_info.utxos.iter().map(|x| x.0).collect::>(); - T::Utxo::submit_c2c_tx(caller, dest, fund_info.funds, data, &outpoints) + T::Utxo::submit_c2c_tx(caller, dest, fund_info.funds, data, &outpoints).map(|_| { + >::mutate(&caller, |info| { + info.as_mut().unwrap().utxos = Vec::new(); + info.as_mut().unwrap().funds = 0; + }); + }) } impl ProgrammablePoolApi for Pallet From 661f654f544007ffb39f4d66a98e130f5500ffc6 Mon Sep 17 00:00:00 2001 From: Aaro Altonen Date: Wed, 20 Oct 2021 15:35:38 +0300 Subject: [PATCH 7/7] tests: Implement functional tests for programmable pools Implement functional tests for PP and create assets directory which contains all smart contract related files --- .../{metadata.json => assets/c2c_tester.json} | 60 ++-- test/functional/assets/c2c_tester.wasm | Bin 0 -> 2118 bytes test/functional/assets/pooltester.json | 216 ++++++++++++ test/functional/assets/pooltester.wasm | Bin 0 -> 6455 bytes test/functional/code.wasm | Bin 2378 -> 0 bytes .../functional/feature_smart_contract_test.py | 318 +++++++++++++++--- test/functional/test_runner.py | 4 +- 7 files changed, 502 insertions(+), 96 deletions(-) rename test/functional/{metadata.json => assets/c2c_tester.json} (56%) create mode 100644 test/functional/assets/c2c_tester.wasm create mode 100644 test/functional/assets/pooltester.json create mode 100644 test/functional/assets/pooltester.wasm delete mode 100644 test/functional/code.wasm diff --git a/test/functional/metadata.json b/test/functional/assets/c2c_tester.json similarity index 56% rename from test/functional/metadata.json rename to test/functional/assets/c2c_tester.json index cf24b11..bde799b 100644 --- a/test/functional/metadata.json +++ b/test/functional/assets/c2c_tester.json @@ -1,46 +1,22 @@ { "metadataVersion": "0.1.0", "source": { - "hash": "0xd2d4276c5864e736fe51fd70c2e76ae600520dbead60bf3b8054f6d3d13e3dd7", + "hash": "0x241e86037e1803891112031bb03b7816d4b00d50effc920dae39c2004efb2ca4", "language": "ink! 3.0.0-rc4", "compiler": "rustc 1.56.0-nightly" }, "contract": { - "name": "pooltest", + "name": "c2c_tester", "version": "0.1.0", "authors": [ - "[your_name] <[your_email]>" + "RBB S.r.l" ] }, "spec": { "constructors": [ - { - "args": [ - { - "name": "init_value", - "type": { - "displayName": [ - "bool" - ], - "type": 1 - } - } - ], - "docs": [ - "Constructor that initializes the `bool` value to the given `init_value`." - ], - "name": [ - "new" - ], - "selector": "0x9bae9d5e" - }, { "args": [], - "docs": [ - "Constructor that initializes the `bool` value to `false`.", - "", - "Constructors can delegate to other constructors." - ], + "docs": [], "name": [ "default" ], @@ -51,25 +27,29 @@ "events": [], "messages": [ { - "args": [], - "docs": [ - " A message that can be called on instantiated contracts.", - " This one flips the value of the stored `bool` from `true`", - " to `false` and vice versa." + "args": [ + { + "name": "value", + "type": { + "displayName": [ + "u32" + ], + "type": 1 + } + } ], + "docs": [], "mutates": true, "name": [ - "flip" + "set_value" ], "payable": false, "returnType": null, - "selector": "0x633aa551" + "selector": "0xc6298215" }, { "args": [], - "docs": [ - " Simply returns the current value of our `bool`." - ], + "docs": [], "mutates": false, "name": [ "get" @@ -77,7 +57,7 @@ "payable": false, "returnType": { "displayName": [ - "bool" + "u32" ], "type": 1 }, @@ -103,7 +83,7 @@ "types": [ { "def": { - "primitive": "bool" + "primitive": "u32" } } ] diff --git a/test/functional/assets/c2c_tester.wasm b/test/functional/assets/c2c_tester.wasm new file mode 100644 index 0000000000000000000000000000000000000000..764fe2b6aca67ae3c36aab67dccf395ffcb9b6cd GIT binary patch literal 2118 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 literal 0 HcmV?d00001 diff --git a/test/functional/code.wasm b/test/functional/code.wasm deleted file mode 100644 index d3c74df855ead838d7d7b7045c66aba336fb3423..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2378 zcma)8&x>3|5U#53_vR&UCfgB}Wf!sEbA=7KIAX{ydvSVTckv*45IklVGX~%O%FfOP z0Gs_KRIP7j%g_`PCR zrc>JGDNlFJRF~AQ@M3H&gh}CC9&?_Zj`j}vPJeIT9ZhbZPL9WWcSmGjJ~(~pK+?gR z(c=Ez!P)5cWW0BD`qgMW9^D~I$5-R=*GKoxCNohPMdQ)rYn1njHVPmtr0EBRHVav{S$?uHr9UMQPYxc{%g9CC`e{t7-W#;KS6b4PrE95!& z##2~qZWvlOx?j}2yWBh77xVoPYO9_-MS}tcMdNb3E-v_%+{EI?3wulYGs%YTvDs)G zReFEF@w_SubYizQiK+_ zzCtuZUN_$3{uo#--_#I5SGPrNAeYadM`4kKt`-PQD|AV?*y^pDMNj1iDN}C07Pu7v z#WAf4E?pJtdax1Pz7ByPV=Gh+Sv0q>NV$atIe=QDD~f%MiV9w%Zjiu10EV}h18!W# z9elD}es{@F@(O;Gl$IY+e)NX<9{eZ=vwh+RURr)MZ15u>AW@{jMv87?LCTb10aj2S zUKAaEtV60L4NB=K$Y`HP+L!U_b3i ztk;%cM0m0l)j+y{lQnAHa8pT_%QA2!fv^gMDlkL;_*oe{LQBWXb5GBQ#iNJi5(4z* z6WoPGLM=e8aNnRnZb5q@$V9umK{m^ua>){eek?qVTVS2SiGA6f<+yFw*Wf~ttQ!|h z8-(lD`Li0Vo%F=#!x#GR?QiQ`1BFUHT*O;X9NrcVOepTh{xsnS)&@151=n?py8{=|8Kc!0kKTyau zB8*-lER@uV9R!9KF8>pqZP6LnW;1kIcwfO~;cp#Z|rH{0le-iSf6@jv;qzt*Io-BER9}cUHmLOr}=c~opN{?Iu227`EbtM#< zyjL@AjW?-CMNc|YAuP~oT>$KAQLPbGjB~$V$8(IG+4!K;t*=8B-HXYogHb;jp54`e zyH?RA_~|-JrB+vj7tD4oJz7$zUY0Ls`YWK--ns+|{$2$rB8l`PH)_z=`TUf{q)wUR zitV8TCXOIPpI|!OM^p|&TH!d?(M3B}^^;k1;)xO`^E?EHsU!A$h^Yg8SE1ZtR(kXB zvn?-U^d$xprWE*Hp;$?m-K=f?1!q%rX>04{<2utu#+K^5>KF_pZ{p|Bn+CJC(|{`- zXz1LgGCFAxCk!z{fdJY$h=up8{BZt3i=U_HB65zqk)m$n3=A;NyTGc+L(0l`SyN)m Vx0ne3&mb~*ES?Ncj;DjC_!s7WchUd= diff --git a/test/functional/feature_smart_contract_test.py b/test/functional/feature_smart_contract_test.py index d155788..487c841 100755 --- a/test/functional/feature_smart_contract_test.py +++ b/test/functional/feature_smart_contract_test.py @@ -3,9 +3,9 @@ # Copyright (c) 2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Smart contract test -""" +""" Smart contract test """ + # Imports should be in PEP8 ordering (std library first, then third party # libraries then local imports). from collections import defaultdict @@ -23,6 +23,23 @@ import os +# helper function to reduce code duplication +def submit_pp_tx(client, input_utxo, alice, value, output): + tx = utxo.Transaction( + client, + inputs=[ + utxo.Input(input_utxo.outpoint(0)), + ], + outputs=[ + utxo.Output( + value=value, + destination=utxo.DestPubkey(alice.public_key), + data=None, + ), + output + ] + ).sign(alice, [input_utxo.outputs[0]], [0]) + return tx, client.submit(alice, tx) class ExampleTest(MintlayerTestFramework): # Each functional test is a subclass of the MintlayerTestFramework class. @@ -56,97 +73,290 @@ def setup_network(self): # sync_all() should not include node2, since we're not expecting it to # sync. connect_nodes(self.nodes[0], self.nodes[1]) - # self.sync_all([self.nodes[0:1]]) def run_test(self): """Main test logic""" client = self.nodes[0].rpc_client - substrate = client.substrate - alice = Keypair.create_from_uri('//Alice') + bob = Keypair.create_from_uri('//Erin') - # Find a suitable UTXO initial_utxo = [x for x in client.utxos_for(alice) if x[1].value >= 50][0] + value = initial_utxo[1].json()["value"] + + self.log.error(initial_utxo) - tx0 = utxo.Transaction( + tx = utxo.Transaction( client, inputs=[ utxo.Input(initial_utxo[0]), ], outputs=[ utxo.Output( - value=50, + value=value, destination=utxo.DestPubkey(alice.public_key), - data=None + data=None, ), - utxo.Output( - value=10, - destination=utxo.DestCreatePP( - code=os.path.join(os.path.dirname(__file__), "code.wasm"), - data=[0xed, 0x4b, 0x9d, 0x1b], # default() constructor selector - ), - data=None - ), - # This output prevent reward overflow - utxo.Output( - value=3981553255926290448385, # = genesis amount - u64::MAX - destination=utxo.DestPubkey(alice.public_key), - data=None - ) ] ).sign(alice, [initial_utxo[1]]) + client.submit(alice, tx) - # submit transaction and get the extrinsic and block hashes - (ext, blk,_) = client.submit(alice, tx0) + # invalid bytecode + value -= 1 - # each new smart contract instantiation creates a new account - # fetch this SS58-formatted account address and return it - # and the hex-encoded account id - (ss58, acc_id) = contract.getContractAddresses(substrate, blk) + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCreatePP( + code=[0x00], + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + assert_equal(contract.getContractAddresses(substrate, blk), None) + + # invalid value + (invalid_tx, res) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=0, + destination=utxo.DestCreatePP( + code=os.path.join(os.path.dirname(__file__), "assets/pooltester.wasm"), + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + assert_equal(res, None) - # create new contract instance which can be used to interact - # with the instantiated contract + # valid data + value -= 1 + + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCreatePP( + code=os.path.join(os.path.dirname(__file__), "assets/pooltester.wasm"), + data=[0xed, 0x4b, 0x9d, 0x1b], + ), + data=None, + )) + + (ss58, acc_id) = contract.getContractAddresses(substrate, blk) contractInstance = contract.ContractInstance( ss58, - os.path.join(os.path.dirname(__file__), "metadata.json"), + os.path.join(os.path.dirname(__file__), "assets/pooltester.json"), substrate ) - # read the value of the flipper contract + # verify the initial state of the smart contract result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, False) + assert_equal(result.contract_result_data.value, 1337) + # valid contract call + value -= 1 msg_data = contractInstance.generate_message_data("flip", {}) - self.log.info("Contract msg_data: {}, {}, {}".format(ss58, acc_id, msg_data)) - tx1 = utxo.Transaction( + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=1, + destination=utxo.DestCallPP( + dest_account=acc_id, + fund=False, + input_data=bytes.fromhex(msg_data.to_hex()[2:]), + ), + data=None, + )) + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + # invalid `value` given + msg_data = contractInstance.generate_message_data("flip", {}) + + (invalid_tx, res) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value=0, + destination=utxo.DestCallPP( + dest_account=alice.public_key, + fund=False, + input_data=bytes.fromhex(msg_data.to_hex()[2:]), + ), + data=None, + )) + assert_equal(res, None) + + # test contract-to-p2k transfer from alice to bob + # + # `send_to_pubkey()` first funds the smart contract from alice's funds + # and when the wasm code is executed, the funds are transferred to bob + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": bob.public_key }) + value -= 555 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 555, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that bob actually received the utxo + bobs_utxos = [x for x in client.utxos_for(bob)] + assert_equal(len(bobs_utxos), 1) + assert_equal(bobs_utxos[0][1].json()['value'], 555) + + # test contract-to-p2pk again but this time don't fund the contract + # meaning that after the TX, bob only has the UTXO he received in the previous test case + # and the contract has a UTXO with value 666 + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": bob.public_key }) + value -= 666 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 666, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = False, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that bob still has the same amount of UTXOs + utxos = [x for x in client.utxos_for(bob)] + assert_equal(len(utxos), 1) + + # verify that the contract has one utxo with value 666 + utxos = [x for x in client.utxos_for(acc_id[2:])] + assert_equal(len(utxos), 1) + assert_equal(utxos[0][1].json()["value"], 666) + + # try to call a contract that doesn't exist (alice's public key + # doesn't point to a valid smart contract) + # + # TODO: because we don't have gas refunding, the money is still + # spent, i.e., if the UTXO set is queried, you'll find a UTXO + # with value 888 meaning user just lost his money which is + # not the correct behavior but the implementation is still under way + msg_data = contractInstance.generate_message_data("fund", {}) + value -= 888 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 888, + destination = utxo.DestCallPP( + dest_account = alice.public_key, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + # Test cross-contract calls + # + # First instantiate another smart contract and verify it has + # been created correctly by querying its value. + # + # Then call the `set_value()` method of newly instantiated contract + # indirectly by creating a UTXO that calls the pooltester's + # `call_contract()` method which dispatches the call to `set_value()` + # + # When all that's done, query the value again and verify that it has been updated + value -= 111 + + (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 111, + destination = utxo.DestCreatePP( + code = os.path.join(os.path.dirname(__file__), "assets/c2c_tester.wasm"), + data = [0xed, 0x4b, 0x9d, 0x1b], + ), + data = None, + )) + + (ss58_c2c, acc_id_c2c) = contract.getContractAddresses(substrate, blk) + c2cInstance = contract.ContractInstance( + ss58_c2c, + os.path.join(os.path.dirname(__file__), "assets/c2c_tester.json"), + substrate + ) + + # verify the initial state of the smart contract + result = c2cInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 555) + + msg_data = contractInstance.generate_message_data("call_contract", { + "dest": acc_id_c2c, + "selector": "0xc6298215", + "value": 999, + }) + value -= 222 + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 222, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + # verify that the call succeeded + result = c2cInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 999) + + # Try to spend the funds of a contract + # + # First fund the contract with some amount of UTXO, + # verify that the fund worked (updated state variable) + # and then try to spend those funds and verify that the + # spend is rejected by the local PP node because the + # smart contract has not spent them and thus the outpoint + # hash is not in the local storage + # + # NOTE: spending the DestCallPP UTXOs doesn't require signatures + # but instead the outpoint hash of the UTXO. This is queried + # from the runtime storage as the smart contract has not transferred + # these funds, the outpoint hash is **not** found from the storage + # and this TX is rejected as invalid + msg_data = contractInstance.generate_message_data("fund", {}) + value -= 555 + + self.log.info("here") + self.log.error(tx) + + (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + value = 555, + destination = utxo.DestCallPP( + dest_account = acc_id, + fund = True, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ), + data = None, + )) + + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, 1338) + + utxos = [x for x in client.utxos_for(acc_id[2:])] + assert_equal(len(utxos), 1) + assert_equal(utxos[0][1].json()["value"], 555) + + invalid_tx = utxo.Transaction( client, - inputs=[ - utxo.Input(tx0.outpoint(0)), + inputs = [ + utxo.Input(utxos[0][0]), ], outputs=[ utxo.Output( - value=49, + value=555, destination=utxo.DestPubkey(alice.public_key), - data=None - ), - utxo.Output( - value=1, - destination=utxo.DestCallPP( - dest_account=acc_id, - fund=False, - input_data=bytes.fromhex(msg_data.to_hex()[2:]), - ), - data=None + data=None, ), ] - ).sign(alice, [tx0.outputs[0]], [0]) - (ext_hash, blk_hash,_) = client.submit(alice, tx1) - - result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, True) + ) + # size of the outpoint (32 bytes, 0x10) + the outpoint itself + # the outpoint in the witness field is valid but because the + # smart contract has not spent the funds, the TX is rejected + tx.inputs[0].witness = bytearray.fromhex("10" + str(utxos[0][0])[2:]) + assert_equal(client.submit(alice, invalid_tx), None) if __name__ == '__main__': ExampleTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c4dd2ed..ad07c29 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -64,9 +64,9 @@ 'feature_staking_diff_addresses.py', 'feature_staking_unlock_not_validator.py', 'feature_staking_withdraw_no_unlock.py', - 'feature_staking_withdraw_not_validator.py' + 'feature_staking_withdraw_not_validator.py', + 'feature_smart_contract_test.py', # 'feature_staking_unlock_and_withdraw.py' ## should be ran on 20 secs - # 'feature_smart_contract_test.py', # Don't append tests at the end to avoid merge conflicts # Put them in a random line within the section that fits their approximate run-time ]