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_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 ]