diff --git a/pallets/pp/src/lib.rs b/pallets/pp/src/lib.rs index 36055b8..c92495f 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, @@ -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::send_conscrit_p2pk(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::send_conscrit_c2c(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 @@ -267,10 +277,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 +299,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,18 +309,17 @@ 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")); } + 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(); diff --git a/pallets/utxo/src/lib.rs b/pallets/utxo/src/lib.rs index 1b16303..065216d 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")] @@ -561,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( @@ -1040,13 +1038,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(); @@ -1139,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)?; @@ -1371,10 +1378,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 +1393,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 +1443,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 +1452,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/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 0000000..764fe2b Binary files /dev/null and b/test/functional/assets/c2c_tester.wasm differ diff --git a/test/functional/assets/pooltester.json b/test/functional/assets/pooltester.json new file mode 100644 index 0000000..6501429 --- /dev/null +++ b/test/functional/assets/pooltester.json @@ -0,0 +1,216 @@ +{ + "metadataVersion": "0.1.0", + "source": { + "hash": "0x2d1e1ed093dfb95c258342e80e7e1800c8c87cf4459837291a35245d23d15038", + "language": "ink! 3.0.0-rc4", + "compiler": "rustc 1.56.0-nightly" + }, + "contract": { + "name": "pooltester", + "version": "0.1.0", + "authors": [ + "RBB S.r.l" + ] + }, + "spec": { + "constructors": [ + { + "args": [ + { + "name": "value", + "type": { + "displayName": [ + "i64" + ], + "type": 1 + } + } + ], + "docs": [], + "name": [ + "new" + ], + "selector": "0x9bae9d5e" + }, + { + "args": [], + "docs": [], + "name": [ + "default" + ], + "selector": "0xed4b9d1b" + } + ], + "docs": [], + "events": [], + "messages": [ + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "get" + ], + "payable": false, + "returnType": { + "displayName": [ + "i64" + ], + "type": 1 + }, + "selector": "0x2f865bd9" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "flip" + ], + "payable": false, + "returnType": null, + "selector": "0x633aa551" + }, + { + "args": [ + { + "name": "dest", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "send_to_pubkey" + ], + "payable": false, + "returnType": null, + "selector": "0xd10be299" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "fund" + ], + "payable": false, + "returnType": null, + "selector": "0x4aafa343" + }, + { + "args": [], + "docs": [], + "mutates": true, + "name": [ + "send_to_self" + ], + "payable": false, + "returnType": null, + "selector": "0xba6ee83a" + }, + { + "args": [ + { + "name": "dest", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + }, + { + "name": "selector", + "type": { + "displayName": [], + "type": 5 + } + }, + { + "name": "value", + "type": { + "displayName": [ + "i64" + ], + "type": 1 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "call_contract" + ], + "payable": false, + "returnType": null, + "selector": "0xc7c0b7ca" + } + ] + }, + "storage": { + "struct": { + "fields": [ + { + "layout": { + "cell": { + "key": "0x0000000000000000000000000000000000000000000000000000000000000000", + "ty": 1 + } + }, + "name": "value" + } + ] + } + }, + "types": [ + { + "def": { + "primitive": "i64" + } + }, + { + "def": { + "composite": { + "fields": [ + { + "type": 3, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_env", + "types", + "AccountId" + ] + }, + { + "def": { + "array": { + "len": 32, + "type": 4 + } + } + }, + { + "def": { + "primitive": "u8" + } + }, + { + "def": { + "array": { + "len": 4, + "type": 4 + } + } + } + ] +} \ No newline at end of file diff --git a/test/functional/assets/pooltester.wasm b/test/functional/assets/pooltester.wasm new file mode 100644 index 0000000..515b1d4 Binary files /dev/null and b/test/functional/assets/pooltester.wasm differ diff --git a/test/functional/code.wasm b/test/functional/code.wasm deleted file mode 100644 index d3c74df..0000000 Binary files a/test/functional/code.wasm and /dev/null differ 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_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 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 ] 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,