diff --git a/test/functional/feature_confidential_transactions.py b/test/functional/feature_confidential_transactions.py index 5e76b75576..b7f62b0e3f 100755 --- a/test/functional/feature_confidential_transactions.py +++ b/test/functional/feature_confidential_transactions.py @@ -3,10 +3,28 @@ # Distributed under the MIT/X11 software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +import io + +from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import connect_nodes_bi, assert_equal from test_framework.authproxy import JSONRPCException -from decimal import Decimal +from test_framework.messages import ( + COIN, + CTransaction, + CTxOut, + CTxOutAsset, + CTxOutValue, + CTxInWitness, + CTxOutWitness, +) +from test_framework.util import ( + connect_nodes_bi, + assert_equal, + hex_str_to_bytes, + bytes_to_hex_str, + BITCOIN_ASSET_OUT, + assert_raises_rpc_error, +) import os import re @@ -378,7 +396,8 @@ def run_test(self): assert_equal(multi_asset_amount['bitcoin'], value1 + value3) assert_equal(multi_asset_amount[test_asset], Decimal('0.00000003')) - # Check blinded multisig functionality + # Check blinded multisig functionality and partial blinding functionality + # Get two pubkeys blinded_addr = self.nodes[0].getnewaddress() pubkey = self.nodes[0].getaddressinfo(blinded_addr)["pubkey"] @@ -396,15 +415,143 @@ def run_test(self): # Create blinded address from p2sh address and import corresponding privkey blinded_multisig_addr = self.nodes[0].createblindedaddress(unconfidential_addr, blinding_pubkey) self.nodes[0].importblindingkey(blinded_multisig_addr, blinding_key) - self.nodes[1].importblindingkey(blinded_multisig_addr, blinding_key) - # Send coins to blinded multisig address and check that they were received - self.nodes[2].sendtoaddress(blinded_multisig_addr, 1) + + # Issue new asset, to use different assets in one transaction when doing + # partial blinding. Just to make these tests a bit more elaborate :-) + issued3 = self.nodes[2].issueasset(1, 0) + self.nodes[2].generate(1) self.sync_all() - assert_equal(len(self.nodes[0].listunspent(0, 0, [unconfidential_addr])), 1) - assert_equal(len(self.nodes[1].listunspent(0, 0, [unconfidential_addr])), 1) + node2_balance = self.nodes[2].getbalance() + assert(issued3['asset'] in node2_balance) + assert_equal(node2_balance[issued3['asset']], Decimal(1)) - self.nodes[0].generate(1) + # Send asset to blinded multisig address and check that it was received + self.nodes[2].sendtoaddress(address=blinded_multisig_addr, amount=1, assetlabel=issued3['asset']) self.sync_all() + # We will use this multisig UTXO in our partially-blinded transaction, + # and will also check that multisig UTXO can be successfully spent + # after the transaction is signed by node1 and node0 in succession. + unspent_asset = self.nodes[0].listunspent(0, 0, [unconfidential_addr], True, {"asset":issued3['asset']}) + assert_equal(len(unspent_asset), 1) + assert(issued3['asset'] not in self.nodes[2].getbalance()) + + # Create new UTXO on node0 to be used in our partially-blinded transaction + blinded_addr = self.nodes[0].getnewaddress() + addr = self.nodes[0].validateaddress(blinded_addr)["unconfidential"] + self.nodes[0].sendtoaddress(blinded_addr, 0.1) + unspent = self.nodes[0].listunspent(0, 0, [addr]) + assert_equal(len(unspent), 1) + + # Create new UTXO on node1 to be used in our partially-blinded transaction + blinded_addr2 = self.nodes[1].getnewaddress() + addr2 = self.nodes[1].validateaddress(blinded_addr2)["unconfidential"] + self.nodes[1].sendtoaddress(blinded_addr2, 0.11) + unspent2 = self.nodes[1].listunspent(0, 0, [addr2]) + assert_equal(len(unspent2), 1) + + # The transaction will have three non-fee outputs + dst_addr = self.nodes[0].getnewaddress() + dst_addr2 = self.nodes[1].getnewaddress() + dst_addr3 = self.nodes[2].getnewaddress() + + # Inputs are selected up front + inputs = [{"txid": unspent2[0]["txid"], "vout": unspent2[0]["vout"]}, {"txid": unspent[0]["txid"], "vout": unspent[0]["vout"]}, {"txid": unspent_asset[0]["txid"], "vout": unspent_asset[0]["vout"]}] + + # Create one part of the transaction to partially blind + rawtx = self.nodes[0].createrawtransaction( + inputs, {dst_addr2: Decimal("0.01")}) + + # Create another part of the transaction to partially blind + rawtx2 = self.nodes[0].createrawtransaction( + inputs, + {dst_addr: Decimal("0.1"), dst_addr3: Decimal("1.0")}, + 0, + False, + {dst_addr: unspent[0]['asset'], dst_addr3: unspent_asset[0]['asset']}) + + sum_i = unspent2[0]["amount"] + unspent[0]["amount"] + sum_o = 0.01 + 0.10 + 0.1 + assert_equal(int(round(sum_i*COIN)), int(round(sum_o*COIN))) + + # Blind the first part of the transaction - we need to supply the + # assetcommmitments for all of the inputs, for the surjectionproof + # to be valid after we combine the transactions + blindtx = self.nodes[1].blindrawtransaction( + rawtx, True, [ + unspent2[0]['assetcommitment'], + unspent[0]['assetcommitment'], + unspent_asset[0]['assetcommitment'] + ]) + + # Combine the transactions + + # Blinded, but incomplete transaction. + # 3 inputs and 1 output, but no fee output, and + # it was blinded with 3 asset commitments, that means + # the final transaction should have 3 inputs. + btx = CTransaction() + btx.deserialize(io.BytesIO(hex_str_to_bytes(blindtx))) + + # Unblinded transaction, with 3 inputs and 2 outputs. + # We will add them to the other transaction to make it complete. + ubtx = CTransaction() + ubtx.deserialize(io.BytesIO(hex_str_to_bytes(rawtx2))) + + # We will add outputs of unblinded transaction + # on top of inputs and outputs of the blinded, but incomplete transaction. + # We also append empty witness instances to make witness arrays match + # vin/vout arrays + btx.wit.vtxinwit.append(CTxInWitness()) + btx.vout.append(ubtx.vout[0]) + btx.wit.vtxoutwit.append(CTxOutWitness()) + btx.wit.vtxinwit.append(CTxInWitness()) + btx.vout.append(ubtx.vout[1]) + btx.wit.vtxoutwit.append(CTxOutWitness()) + # Add explicit fee output + btx.vout.append(CTxOut(nValue=CTxOutValue(10000000), + nAsset=CTxOutAsset(BITCOIN_ASSET_OUT))) + btx.wit.vtxoutwit.append(CTxOutWitness()) + + # Input 0 is bitcoin asset (already blinded) + # Input 1 is also bitcoin asset + # Input 2 is our new asset + + # Blind with wrong order of assetcommitments - such transaction should be rejected + blindtx = self.nodes[0].blindrawtransaction( + bytes_to_hex_str(btx.serialize()), True, [ + unspent_asset[0]['assetcommitment'], + unspent[0]['assetcommitment'], + unspent2[0]['assetcommitment'] + ]) + + stx2 = self.nodes[1].signrawtransactionwithwallet(blindtx) + stx = self.nodes[0].signrawtransactionwithwallet(stx2['hex']) + self.sync_all() + + assert_raises_rpc_error(-26, "bad-txns-in-ne-out", self.nodes[2].sendrawtransaction, stx['hex']) + + # Blind with correct order of assetcommitments + blindtx = self.nodes[0].blindrawtransaction( + bytes_to_hex_str(btx.serialize()), True, [ + unspent2[0]['assetcommitment'], + unspent[0]['assetcommitment'], + unspent_asset[0]['assetcommitment'] + ]) + + stx2 = self.nodes[1].signrawtransactionwithwallet(blindtx) + stx = self.nodes[0].signrawtransactionwithwallet(stx2['hex']) + txid = self.nodes[2].sendrawtransaction(stx['hex']) + self.nodes[2].generate(1) + assert self.nodes[2].getrawtransaction(txid, 1)['confirmations'] == 1 + self.sync_all() + + # Check that the sent asset has reached its destination + unconfidential_dst_addr3 = self.nodes[2].validateaddress(dst_addr3)["unconfidential"] + unspent_asset2 = self.nodes[2].listunspent(1, 1, [unconfidential_dst_addr3], True, {"asset":issued3['asset']}) + assert_equal(len(unspent_asset2), 1) + assert_equal(unspent_asset2[0]['amount'], Decimal(1)) + # And that the balance was correctly updated + assert_equal(self.nodes[2].getbalance()[issued3['asset']], Decimal(1)) # Basic checks of rawblindrawtransaction functionality blinded_addr = self.nodes[0].getnewaddress()