diff --git a/test/functional/assets/pooltester.json b/test/functional/assets/pooltester.json index 6501429..d3f6d8e 100644 --- a/test/functional/assets/pooltester.json +++ b/test/functional/assets/pooltester.json @@ -1,44 +1,26 @@ { "metadataVersion": "0.1.0", "source": { - "hash": "0x2d1e1ed093dfb95c258342e80e7e1800c8c87cf4459837291a35245d23d15038", - "language": "ink! 3.0.0-rc4", + "hash": "0xc8d99645ca13ba2acbf5cc7942aa22fa3c94d9b5e4f0b6dc0f023494ceaf8893", + "language": "ink! 3.0.0-rc3", "compiler": "rustc 1.56.0-nightly" }, "contract": { "name": "pooltester", "version": "0.1.0", "authors": [ - "RBB S.r.l" + "[your_name] <[your_email]>" ] }, "spec": { "constructors": [ { - "args": [ - { - "name": "value", - "type": { - "displayName": [ - "i64" - ], - "type": 1 - } - } - ], + "args": [], "docs": [], "name": [ "new" ], "selector": "0x9bae9d5e" - }, - { - "args": [], - "docs": [], - "name": [ - "default" - ], - "selector": "0xed4b9d1b" } ], "docs": [], @@ -47,7 +29,7 @@ { "args": [], "docs": [], - "mutates": true, + "mutates": false, "name": [ "get" ], @@ -81,6 +63,15 @@ ], "type": 2 } + }, + { + "name": "value", + "type": { + "displayName": [ + "u128" + ], + "type": 5 + } } ], "docs": [], @@ -89,31 +80,14 @@ "send_to_pubkey" ], "payable": false, - "returnType": null, + "returnType": { + "displayName": [ + "Result" + ], + "type": 6 + }, "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": [ { @@ -129,16 +103,16 @@ "name": "selector", "type": { "displayName": [], - "type": 5 + "type": 9 } }, { "name": "value", "type": { "displayName": [ - "i64" + "u128" ], - "type": 1 + "type": 5 } } ], @@ -148,7 +122,12 @@ "call_contract" ], "payable": false, - "returnType": null, + "returnType": { + "displayName": [ + "Result" + ], + "type": 6 + }, "selector": "0xc7c0b7ca" } ] @@ -204,6 +183,65 @@ "primitive": "u8" } }, + { + "def": { + "primitive": "u128" + } + }, + { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 7, + "typeName": "T" + } + ], + "name": "Ok" + }, + { + "fields": [ + { + "type": 8, + "typeName": "E" + } + ], + "name": "Err" + } + ] + } + }, + "params": [ + 7, + 8 + ], + "path": [ + "Result" + ] + }, + { + "def": { + "tuple": [] + } + }, + { + "def": { + "variant": { + "variants": [ + { + "discriminant": 0, + "name": "FailGetRandomSource" + } + ] + } + }, + "path": [ + "pooltester", + "TransferError" + ] + }, { "def": { "array": { diff --git a/test/functional/assets/pooltester.wasm b/test/functional/assets/pooltester.wasm index 515b1d4..948b10a 100644 Binary files a/test/functional/assets/pooltester.wasm and b/test/functional/assets/pooltester.wasm differ diff --git a/test/functional/feature_smart_contract_test.py b/test/functional/feature_smart_contract_test.py index 487c841..450e2cd 100755 --- a/test/functional/feature_smart_contract_test.py +++ b/test/functional/feature_smart_contract_test.py @@ -24,20 +24,18 @@ import os # helper function to reduce code duplication -def submit_pp_tx(client, input_utxo, alice, value, output): +def submit_pp_tx(client, input_utxo, alice, value, outputs): + outputs.insert(0, utxo.Output( + value=value, + data=None, + destination=utxo.DestPubkey(alice.public_key) + )) 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 - ] + outputs = outputs, ).sign(alice, [input_utxo.outputs[0]], [0]) return tx, client.submit(alice, tx) @@ -79,7 +77,7 @@ def run_test(self): client = self.nodes[0].rpc_client substrate = client.substrate alice = Keypair.create_from_uri('//Alice') - bob = Keypair.create_from_uri('//Erin') + erin = Keypair.create_from_uri('//Erin') initial_utxo = [x for x in client.utxos_for(alice) if x[1].value >= 50][0] value = initial_utxo[1].json()["value"] @@ -101,41 +99,47 @@ def run_test(self): ).sign(alice, [initial_utxo[1]]) client.submit(alice, tx) - # invalid bytecode + """ + Invalid bytecode + """ value -= 1 - (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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( + """ + 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) - # valid data + """ + Valid data + """ value -= 1 - (tx, (_, blk, _)) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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=[0x9b, 0xae, 0x9d, 0x5e], ), data=None, - )) + )]) (ss58, acc_id) = contract.getContractAddresses(substrate, blk) contractInstance = contract.ContractInstance( @@ -148,126 +152,256 @@ def run_test(self): result = contractInstance.read(alice, "get") assert_equal(result.contract_result_data.value, 1337) - # valid contract call + """ + Valid contract call + """ value -= 1 msg_data = contractInstance.generate_message_data("flip", {}) - (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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 + """ + Invalid `value` given + """ msg_data = contractInstance.generate_message_data("flip", {}) - (invalid_tx, res) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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 + # query the initial value of the contract # - # `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 }) + # each successful tranfser will update the value by one + # and each call that fails doesn't change the value + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + """ + Try to fund a contract that doesn't exist + """ + value -= 400 + + (tx, _) = submit_pp_tx(client, tx, alice, value, [utxo.Output( + value = 400, + data = None, + destination = utxo.DestFundPP(alice.public_key) + )]) + + """ + Try to call contract without funding it + """ + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": erin.public_key, "value": 555 }) value -= 555 - (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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) + # call failed, the value is not updated + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) - # 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 + """ + Fund the contract (but not enough) and call it + """ + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": erin.public_key, "value": 500 }) + value -= 500 - (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:]), + (tx, _) = submit_pp_tx(client, tx, alice, value, [ + utxo.Output( + value = 400, + data = None, + destination = utxo.DestFundPP(acc_id) ), - data = None, - )) + utxo.Output( + value = 100, + data = None, + destination = utxo.DestCallPP( + dest_account = acc_id, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ) + ), + ]) - # verify that bob still has the same amount of UTXOs - utxos = [x for x in client.utxos_for(bob)] - assert_equal(len(utxos), 1) + # call failed, the value is not updated + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1337) + + """ + Fund the contract and call it + """ + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": erin.public_key, "value": 500 }) + value -= 200 + + (tx, _) = submit_pp_tx(client, tx, alice, value, [ + utxo.Output( + value = 100, + data = None, + destination = utxo.DestFundPP(acc_id) + ), + utxo.Output( + value = 100, + data = None, + destination = utxo.DestCallPP( + dest_account = acc_id, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ) + ), + ]) + + # call succeeded, the value is updated + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1336) + + # verify that Erin has 1 UTXO with value 500 + erins = [x for x in client.utxos_for(erin.public_key)] + assert_equal(len(erins), 1) + assert_equal(erins[0][1].json()["value"], 500) + + # verify that the contract only has CallPP UTXOs + contract_utxos = [x for x in client.utxos_for(acc_id[2:])] + callpp_utxos = [x for x in contract_utxos if list(x[1].json()["destination"])[0] == "CallPP"] + assert_equal(len(contract_utxos), len(callpp_utxos)) + + """ + Fund the contract and call it but don't transfer all of the funds + """ + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": erin.public_key, "value": 200 }) + value -= 600 + + (tx, _) = submit_pp_tx(client, tx, alice, value, [ + utxo.Output( + value = 500, + data = None, + destination = utxo.DestFundPP(acc_id) + ), + utxo.Output( + value = 100, + data = None, + destination = utxo.DestCallPP( + dest_account = acc_id, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ) + ), + ]) + + # call succeeded, the value is updated + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1335) + + # verify that erin has two UTXOs and that their total value is 700 + erins = [x for x in client.utxos_for(erin.public_key)] + total_value = sum([x[1].json()["value"] for x in erins]) + assert_equal(len(erins), 2) + assert_equal(total_value, 700) - # verify that the contract has one utxo with value 666 + # verify that the contract has one FundPP UTXO with value 300 + fundpps = [x for x in client.utxos_for(acc_id[2:]) if list(x[1].json()["destination"])[0] == "FundPP"] + assert_equal(len(fundpps), 1) + assert_equal(fundpps[0][1].json()["value"], 300) + + """ + Try to transfer all of the funds of a contract + + The contract has a UTXO with a value 300 from the previous test, try to spend it entirely + and check the state after the TX. + + Erin should have X and the contract should only have CallPP UTXOs + """ + msg_data = contractInstance.generate_message_data("send_to_pubkey", { "dest": erin.public_key, "value": 300 }) + value -= 100 + + (tx, _) = submit_pp_tx(client, tx, alice, value, [ + utxo.Output( + value = 100, + data = None, + destination = utxo.DestCallPP( + dest_account = acc_id, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ) + ), + ]) + + # call succeeded, the value is updated + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1334) + + # verify that erin has two UTXOs and that their total value is 700 + erins = [x for x in client.utxos_for(erin.public_key)] + total_value = sum([x[1].json()["value"] for x in erins]) + assert_equal(len(erins), 3) + assert_equal(total_value, 1000) + + # verify that the contract doesn't have FundPP UTXOs but only CallPP UTXOs utxos = [x for x in client.utxos_for(acc_id[2:])] - assert_equal(len(utxos), 1) - assert_equal(utxos[0][1].json()["value"], 666) + fundpps = [x for x in utxos if list(x[1].json()["destination"])[0] == "FundPP"] + assert_equal(len(utxos), 6) + assert_equal(len(fundpps), 0) - # 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", {}) + """ + 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 + """ value -= 888 - (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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:]), + input_data = [0x00, 0x01, 0x02, 0x03], ), data = None, - )) + )]) + # call failed, the value is not updated result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, -1337) + assert_equal(result.contract_result_data.value, -1334) - # 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 + """ + 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( + (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( @@ -285,56 +419,57 @@ def run_test(self): "selector": "0xc6298215", "value": 999, }) - value -= 222 + value -= 600 - (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:]), + (tx, _) = submit_pp_tx(client, tx, alice, value, [ + utxo.Output( + value = 500, + data = None, + destination = utxo.DestFundPP(acc_id) ), - data = None, - )) + utxo.Output( + value = 100, + data = None, + destination = utxo.DestCallPP( + dest_account = acc_id, + input_data = bytes.fromhex(msg_data.to_hex()[2:]), + ) + ) + ]) # 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", {}) + result = contractInstance.read(alice, "get") + assert_equal(result.contract_result_data.value, -1333) + + """ + 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 + """ value -= 555 - self.log.info("here") - self.log.error(tx) - - (tx, _) = submit_pp_tx(client, tx, alice, value, utxo.Output( + (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, - )) + destination = utxo.DestFundPP(acc_id) + )]) - result = contractInstance.read(alice, "get") - assert_equal(result.contract_result_data.value, 1338) - - utxos = [x for x in client.utxos_for(acc_id[2:])] + # fetch the FundPP UTXO that was just sent + utxos = [x for x in client.utxos_for(acc_id[2:]) if list(x[1].json()["destination"])[0] == "FundPP"] assert_equal(len(utxos), 1) assert_equal(utxos[0][1].json()["value"], 555) @@ -345,9 +480,9 @@ def run_test(self): ], outputs=[ utxo.Output( - value=555, - destination=utxo.DestPubkey(alice.public_key), - data=None, + value = 555, + data = None, + destination = utxo.DestPubkey(alice.public_key) ), ] ) diff --git a/test/functional/test_framework/mintlayer/utxo.py b/test/functional/test_framework/mintlayer/utxo.py index d95e5b3..4b95230 100644 --- a/test/functional/test_framework/mintlayer/utxo.py +++ b/test/functional/test_framework/mintlayer/utxo.py @@ -30,6 +30,10 @@ def __init__(self, url="ws://127.0.0.1", port=9944): def encode_obj(self, ty, obj): return self.substrate.encode_scale(ty, obj) + """ SCALE-decode given object """ + def decode_obj(self, ty, obj): + return self.substrate.decode_scale(ty, obj) + """ SCALE-encode given object """ def encode(self, obj): return self.encode_obj(obj.type_string(), obj.json()) @@ -155,6 +159,8 @@ def load(obj): return DestCreatePP.load(obj['CreatePP']) if 'CallPP' in obj: return DestCallPP.load(obj['CallPP']) + if 'FundPP' in obj: + return DestFundPP.load(obj['FundPP']) if 'LockForStaking' in obj: return DestLockForStaking.load(obj['LockForStaking']) if 'LockExtraForStaking' in obj: @@ -209,17 +215,30 @@ def json(self): return { 'CreatePP': { 'code': self.code, 'data': self.data } } class DestCallPP(Destination): - def __init__(self, dest_account, fund, input_data): + def __init__(self, dest_account, input_data): self.acct = dest_account - self.fund = fund self.data = input_data @staticmethod def load(obj): - return DestCallPP(ss58_decode(obj['dest_account']), obj['fund'], obj['input_data']) + return DestCallPP(ss58_decode(obj['dest_account']), obj['input_data']) + + def json(self): + return { 'CallPP': { 'dest_account': self.acct, 'input_data': self.data } } + + def get_pubkey(self): + return str(self.acct) + +class DestFundPP(Destination): + def __init__(self, dest_account): + self.acct = dest_account + + @staticmethod + def load(obj): + return DestFundPP(ss58_decode(obj['dest_account'])) def json(self): - return { 'CallPP': { 'dest_account': self.acct, 'fund': self.fund, 'input_data': self.data } } + return { 'FundPP': { 'dest_account': self.acct } } def get_pubkey(self): return str(self.acct)