From d7266ebb43023115fb9b7a76d0615f4501bb2dec Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Thu, 29 Nov 2018 13:29:44 -0500 Subject: [PATCH 01/17] Fix blinding logic for issuances on input 1 or higher --- src/blind.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blind.cpp b/src/blind.cpp index e3e957ffb..6dfe828bb 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -251,7 +251,7 @@ int BlindTransaction(std::vector& input_blinding_factors, const std::v // New Issuance if (issuance.assetBlindingNonce.IsNull()) { bool assetToBlind = (vBlindIssuanceAsset.size() > i && vBlindIssuanceAsset[i].IsValid()) ? true : false; - GenerateAssetEntropy(entropy, tx.vin[0].prevout, issuance.assetEntropy); + GenerateAssetEntropy(entropy, tx.vin[i].prevout, issuance.assetEntropy); CalculateAsset(asset, entropy); CalculateReissuanceToken(token, entropy, assetToBlind); } else { From 5e561625aca9c30329f66e87b9a992e6002cfadf Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Thu, 29 Nov 2018 16:02:57 -0500 Subject: [PATCH 02/17] fixup BlindTransaction issuance counting assertion --- src/blind.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/blind.cpp b/src/blind.cpp index 6dfe828bb..a650b67fb 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -175,12 +175,28 @@ void CreateValueCommitment(CConfidentialValue& confValue, secp256k1_pedersen_com assert(confValue.IsValid()); } +size_t GetNumIssuances(const CMutableTransaction& tx) +{ + unsigned int numIssuances = 0; + for (unsigned int i = 0; i < tx.vin.size(); i++) { + if (!tx.vin[i].assetIssuance.IsNull()) { + if (!tx.vin[i].assetIssuance.nAmount.IsNull()) { + numIssuances++; + } + if (!tx.vin[i].assetIssuance.nInflationKeys.IsNull()) { + numIssuances++; + } + } + } + return numIssuances; +} + int BlindTransaction(std::vector& input_blinding_factors, const std::vector& input_asset_blinding_factors, const std::vector& input_assets, const std::vector& input_amounts, std::vector& output_blinding_factors, std::vector& output_asset_blinding_factors, const std::vector& output_pubkeys, const std::vector& vBlindIssuanceAsset, const std::vector& vBlindIssuanceToken, CMutableTransaction& tx, std::vector >* auxiliary_generators) { // Sanity check input data and output_pubkey size, clear other output data assert(tx.vout.size() >= output_pubkeys.size()); - assert(tx.vin.size() >= vBlindIssuanceAsset.size()); - assert(tx.vin.size() >= vBlindIssuanceToken.size()); + assert(tx.vin.size()+GetNumIssuances(tx) >= vBlindIssuanceAsset.size()); + assert(tx.vin.size()+GetNumIssuances(tx) >= vBlindIssuanceToken.size()); output_blinding_factors.clear(); output_blinding_factors.resize(tx.vout.size()); output_asset_blinding_factors.clear(); From 3aaf7780e5d6a441d99ca75943318c77f086cc07 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Tue, 27 Nov 2018 13:30:21 -0500 Subject: [PATCH 03/17] Break out issuance-specific testing to its own functional test --- qa/pull-tester/rpc-tests.py | 1 + qa/rpc-tests/confidential_transactions.py | 103 +------------- qa/rpc-tests/feature_issuance.py | 164 ++++++++++++++++++++++ 3 files changed, 166 insertions(+), 102 deletions(-) create mode 100755 qa/rpc-tests/feature_issuance.py diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 6269a6bb5..0ee8518a0 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -99,6 +99,7 @@ raise testScripts = [ + 'feature_issuance.py', # longest test should go first, to favor running tests in parallel 'wallet-hd.py', #'walletbackup.py', diff --git a/qa/rpc-tests/confidential_transactions.py b/qa/rpc-tests/confidential_transactions.py index d7c99a586..509a8acb6 100755 --- a/qa/rpc-tests/confidential_transactions.py +++ b/qa/rpc-tests/confidential_transactions.py @@ -22,9 +22,7 @@ def setup_network(self, split=False): self.sync_all() def run_test(self): - print("Mining blocks...") - self.nodes[0].generate(101) - self.sync_all() + print("General Confidential tests") #Running balances node0 = self.nodes[0].getbalance()["bitcoin"] node1 = 0 @@ -217,12 +215,8 @@ def run_test(self): # Unblinded issuance of asset issued = self.nodes[0].issueasset(1, 1, False) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["asset"]], 1) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) - # Quick unblinded reissuance check, making 2*COIN total self.nodes[0].reissueasset(issued["asset"], 1) - testAssetHex = issued["asset"] self.nodes[0].generate(1) self.sync_all() @@ -269,46 +263,6 @@ def run_test(self): self.nodes[2].generate(101) self.sync_all() - # Destroy assets - pre_destroy_btc_balance = self.nodes[2].getwalletinfo()['balance']['bitcoin'] - self.nodes[2].destroyamount('bitcoin', 2) # Destroy 2 BTC - self.nodes[2].generate(1) - self.sync_all() - - issuedamount = self.nodes[0].getwalletinfo()['balance'][issued["token"]] - assert_equal(issuedamount, Decimal('1.0')) - self.nodes[0].destroyamount(issued["token"], issuedamount) # Destroy all reissuance tokens of one type - - self.nodes[0].generate(1) - self.sync_all() - assert(issued["token"] not in self.nodes[0].getinfo()['balance']) - - # Test various issuance and auditing paths - - issuancedata = self.nodes[0].issueasset(Decimal('0.00000002'), Decimal('0.00000001')) #2 of asset, 1 reissuance token - self.nodes[1].generate(1) - self.sync_all() - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000002')) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) - self.nodes[0].reissueasset(issuancedata["asset"], Decimal('0.00000001')) - self.sync_all() - assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000003')) - # Can't reissue an issuance token (yet) - try: - self.nodes[0].reissueasset(issuancedata["token"], Decimal('0.00000001')) - raise AssertionError("You shouldn't be able to reissue a token yet") - except JSONRPCException: - pass - - - issuancedata = self.nodes[2].issueasset(Decimal('0.00000005'), 0) #5 of asset, 0 reissuance token - # No reissuance tokens - try: - self.nodes[2].reissueasset(issuancedata["token"], 5) - raise AssertionError("You shouldn't be able to reissue without a token") - except JSONRPCException: - pass - issuancedata = self.nodes[2].issueasset(0, Decimal('0.00000006')) #0 of asset, 6 reissuance token # Node 2 will send node 1 a reissuance token, both will generate assets @@ -319,68 +273,13 @@ def run_test(self): self.nodes[2].generate(1) self.sync_all() - assert_equal(self.nodes[2].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000005')) - assert_equal(self.nodes[1].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) redata1 = self.nodes[1].reissueasset(issuancedata["asset"], Decimal('0.05')) redata2 = self.nodes[2].reissueasset(issuancedata["asset"], Decimal('0.025')) - - self.sync_all() - # Watch-only issuances won't show up in wallet until confirmed self.nodes[1].generate(1) self.sync_all() - # Now have node 0 audit these issuances - blindingkey1 = self.nodes[1].dumpissuanceblindingkey(redata1["txid"], redata1["vin"]) - blindingkey2 = self.nodes[2].dumpissuanceblindingkey(redata2["txid"], redata2["vin"]) - blindingkey3 = self.nodes[2].dumpissuanceblindingkey(issuancedata["txid"], issuancedata["vin"]) - - # Need addr to get transactions in wallet. TODO: importissuances? - txdet1 = self.nodes[1].gettransaction(redata1["txid"])["details"] - txdet2 = self.nodes[2].gettransaction(redata2["txid"])["details"] - txdet3 = self.nodes[2].gettransaction(issuancedata["txid"])["details"] - - # Receive addresses added last - addr1 = txdet1[len(txdet1)-1]["address"] - addr2 = txdet2[len(txdet2)-1]["address"] - addr3 = txdet3[len(txdet3)-1]["address"] - - assert_equal(len(self.nodes[0].listissuances()), 6); - self.nodes[0].importaddress(addr1) - self.nodes[0].importaddress(addr2) - self.nodes[0].importaddress(addr3) - - issuances = self.nodes[0].listissuances() - assert_equal(len(issuances), 9) - - for issue in issuances: - if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: - assert_equal(issue['assetamount'], Decimal('-1')) - assert_equal(issue['tokenamount'], Decimal('-1')) - - self.nodes[0].importissuanceblindingkey(redata1["txid"], redata1["vin"], blindingkey1) - self.nodes[0].importissuanceblindingkey(redata2["txid"], redata2["vin"], blindingkey2) - self.nodes[0].importissuanceblindingkey(issuancedata["txid"], issuancedata["vin"], blindingkey3) - - issuances = self.nodes[0].listissuances() - - for issue in issuances: - if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: - assert_equal(issue['assetamount'], Decimal('0.05')) - if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: - assert_equal(issue['assetamount'], Decimal('0.025')) - if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: - assert_equal(issue['assetamount'], Decimal('0')) - assert_equal(issue['tokenamount'], Decimal('0.00000006')) - # Check for value accounting when asset issuance is null but token not, ie unblinded issued = self.nodes[0].issueasset(0, 1, False) - assert(issued["asset"] not in self.nodes[0].getwalletinfo()["balance"]) - assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) - # Check for value when receiving defferent assets by same address. self.nodes[0].sendtoaddress(unconfidential_address2, Decimal('0.00000001'), "", "", False, test_asset) diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py new file mode 100755 index 000000000..77e3ab746 --- /dev/null +++ b/qa/rpc-tests/feature_issuance.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016 The Bitcoin Core developers +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +" Tests issued assets functionality including (re)issuance, and de-issuance " + +class IssuanceTest (BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.num_nodes = 3 + self.setup_clean_chain = True + + def setup_network(self, split=False): + self.nodes = start_nodes(self.num_nodes, self.options.tmpdir) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + self.is_network_split=False + self.sync_all() + + def run_test(self): + + # Unblinded issuance of asset + issued = self.nodes[0].issueasset(1, 1, False) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["asset"]], 1) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) + # Quick unblinded reissuance check, making 2*COIN total + self.nodes[0].reissueasset(issued["asset"], 1) + + self.nodes[0].generate(1) + self.sync_all() + + issued2 = self.nodes[0].issueasset(2, 1) + test_asset = issued2["asset"] + assert_equal(self.nodes[0].getwalletinfo(test_asset)['balance'], Decimal(2)) + + assert_equal(self.nodes[1].getwalletinfo(test_asset)['balance'], Decimal(0)) + + # Send some bitcoin to other nodes + self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 3) + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 3) + self.nodes[0].generate(1) + self.sync_all() + + # Destroy assets + pre_destroy_btc_balance = self.nodes[2].getwalletinfo()['balance']['bitcoin'] + self.nodes[2].destroyamount('bitcoin', 2) # Destroy 2 BTC + self.nodes[2].generate(1) + self.sync_all() + + issuedamount = self.nodes[0].getwalletinfo()['balance'][issued["token"]] + assert_equal(issuedamount, Decimal('1.0')) + self.nodes[0].destroyamount(issued["token"], issuedamount) # Destroy all reissuance tokens of one type + + self.nodes[0].generate(1) + self.sync_all() + assert(issued["token"] not in self.nodes[0].getinfo()['balance']) + + # Test various issuance and auditing paths + + issuancedata = self.nodes[0].issueasset(Decimal('0.00000002'), Decimal('0.00000001')) #2 of asset, 1 reissuance token + self.nodes[1].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000002')) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) + self.nodes[0].reissueasset(issuancedata["asset"], Decimal('0.00000001')) + self.sync_all() + assert_equal(self.nodes[0].getwalletinfo()["balance"][issuancedata["asset"]], Decimal('0.00000003')) + # Can't reissue an issuance token (yet) + try: + self.nodes[0].reissueasset(issuancedata["token"], Decimal('0.00000001')) + raise AssertionError("You shouldn't be able to reissue a token yet") + except JSONRPCException: + pass + + + issuancedata = self.nodes[2].issueasset(Decimal('0.00000005'), 0) #5 of asset, 0 reissuance token + # No reissuance tokens + try: + self.nodes[2].reissueasset(issuancedata["token"], 5) + raise AssertionError("You shouldn't be able to reissue without a token") + except JSONRPCException: + pass + + issuancedata = self.nodes[2].issueasset(0, Decimal('0.00000006')) #0 of asset, 6 reissuance token + + # Node 2 will send node 1 a reissuance token, both will generate assets + self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), Decimal('0.00000001'), "", "", False, issuancedata["token"]) + # node 1 needs to know about a (re)issuance to reissue itself + self.nodes[1].importaddress(self.nodes[2].gettransaction(issuancedata["txid"])["details"][0]["address"]) + # also send some bitcoin + self.nodes[2].generate(1) + self.sync_all() + + assert_equal(self.nodes[2].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000005')) + assert_equal(self.nodes[1].getwalletinfo()["balance"][issuancedata["token"]], Decimal('0.00000001')) + redata1 = self.nodes[1].reissueasset(issuancedata["asset"], Decimal('0.05')) + redata2 = self.nodes[2].reissueasset(issuancedata["asset"], Decimal('0.025')) + + self.sync_all() + # Watch-only issuances won't show up in wallet until confirmed + self.nodes[1].generate(1) + self.sync_all() + + # Now have node 0 audit these issuances + blindingkey1 = self.nodes[1].dumpissuanceblindingkey(redata1["txid"], redata1["vin"]) + blindingkey2 = self.nodes[2].dumpissuanceblindingkey(redata2["txid"], redata2["vin"]) + blindingkey3 = self.nodes[2].dumpissuanceblindingkey(issuancedata["txid"], issuancedata["vin"]) + + # Need addr to get transactions in wallet. TODO: importissuances? + txdet1 = self.nodes[1].gettransaction(redata1["txid"])["details"] + txdet2 = self.nodes[2].gettransaction(redata2["txid"])["details"] + txdet3 = self.nodes[2].gettransaction(issuancedata["txid"])["details"] + + # Receive addresses added last + addr1 = txdet1[len(txdet1)-1]["address"] + addr2 = txdet2[len(txdet2)-1]["address"] + addr3 = txdet3[len(txdet3)-1]["address"] + + assert_equal(len(self.nodes[0].listissuances()), 6); + self.nodes[0].importaddress(addr1) + self.nodes[0].importaddress(addr2) + self.nodes[0].importaddress(addr3) + + issuances = self.nodes[0].listissuances() + assert_equal(len(issuances), 9) + + for issue in issuances: + if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: + assert_equal(issue['assetamount'], Decimal('-1')) + assert_equal(issue['tokenamount'], Decimal('-1')) + + # Test that importing the issuance blinding keys then reveals the issuance amounts + self.nodes[0].importissuanceblindingkey(redata1["txid"], redata1["vin"], blindingkey1) + self.nodes[0].importissuanceblindingkey(redata2["txid"], redata2["vin"], blindingkey2) + self.nodes[0].importissuanceblindingkey(issuancedata["txid"], issuancedata["vin"], blindingkey3) + + issuances = self.nodes[0].listissuances() + + for issue in issuances: + if issue['txid'] == redata1["txid"] and issue['vin'] == redata1["vin"]: + assert_equal(issue['assetamount'], Decimal('0.05')) + if issue['txid'] == redata2["txid"] and issue['vin'] == redata2["vin"]: + assert_equal(issue['assetamount'], Decimal('0.025')) + if issue['txid'] == issuancedata["txid"] and issue['vin'] == issuancedata["vin"]: + assert_equal(issue['assetamount'], Decimal('0')) + assert_equal(issue['tokenamount'], Decimal('0.00000006')) + + # Check for value accounting when asset issuance is null but token not, ie unblinded + issued = self.nodes[0].issueasset(0, 1, False) + assert(issued["asset"] not in self.nodes[0].getwalletinfo()["balance"]) + assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) + + +if __name__ == '__main__': + IssuanceTest ().main () From 1225923e071086e70e121781f4e80c7e98f3b786 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Thu, 29 Nov 2018 16:01:35 -0500 Subject: [PATCH 04/17] Fixup (raw)blindrawtransaction --- src/rpc/rawtransaction.cpp | 61 ++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index b9490377c..9fbdb5875 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -712,14 +712,16 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector& CScript blindingScript = CScript() << OP_RETURN << std::vector(tx.vin[nIn].prevout.hash.begin(), tx.vin[nIn].prevout.hash.end()) << tx.vin[nIn].prevout.n; for (size_t nPseudo = 0; nPseudo < 2; nPseudo++) { - CConfidentialValue& confValue = (nPseudo == 0) ? issuance.nAmount : issuance.nInflationKeys; + bool issuance_asset = (nPseudo == 0); + std::vector& issuance_blinding_keys = issuance_asset ? asset_keys : token_keys; + CConfidentialValue& confValue = issuance_asset ? issuance.nAmount : issuance.nInflationKeys; if (confValue.IsCommitment()) { // Rangeproof must exist if (tx.wit.vtxinwit.size() <= nIn) { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Transaction issuance is already blinded but has no attached rangeproof.")); } CTxInWitness& txinwit = tx.wit.vtxinwit[nIn]; - std::vector& vchRangeproof = (nPseudo == 0) ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof; + std::vector& vchRangeproof = issuance_asset ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof; uint256 blinding_factor; uint256 asset_blinding_factor; CAmount amount; @@ -728,29 +730,29 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector& // Wipe out confidential info from issuance vchRangeproof.clear(); confValue = CConfidentialValue(amount); - // One key both blinded values, single key needed for issuance reveal - asset_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); - token_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); + // One key blinds both values, single key needed for issuance reveal + issuance_blinding_keys.push_back(pwalletMain->GetBlindingKey(&blindingScript)); continue; } #endif // If no wallet, or unable to unblind, leave it alone in next blinding step - asset_keys.push_back(CKey()); - token_keys.push_back(CKey()); - } else { + issuance_blinding_keys.push_back(CKey()); + + } else if (confValue.IsExplicit()) { // Without wallet, nothing to be done. - asset_keys.push_back(CKey()); - token_keys.push_back(CKey()); + issuance_blinding_keys.push_back(CKey()); #ifdef ENABLE_WALLET // Use wallet to generate blindingkey used directly as nonce // as user is not "sending" to anyone. // Always assumed we want to blind here. // TODO Signal intent for all blinding via API including replacing nonce commitment if (fUseWallet) { - asset_keys[asset_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); - token_keys[token_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); + issuance_blinding_keys[issuance_blinding_keys.size()-1] = pwalletMain->GetBlindingKey(&blindingScript); } #endif + } else { + // Null or invalid, don't try anything but append an empty key + issuance_blinding_keys.push_back(CKey()); } } } @@ -766,6 +768,7 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) "The input raw transaction cannot have already-blinded outputs.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" "If an additional blinded output is required to make a balanced blinding, a 0-value unspendable output will be added. Since there is no access to the wallet the blinding pubkey from the last output with blinding key will be repeated.\n" + "You can not blind issuances with this call.\n" "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" @@ -903,9 +906,9 @@ UniValue rawblindrawtransaction(const JSONRPCRequest& request) #ifdef ENABLE_WALLET UniValue blindrawtransaction(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.size() < 1 || request.params.size() > 4)) + if (request.fHelp || (request.params.size() < 1 || request.params.size() > 5)) throw runtime_error( - "blindrawtransaction \"hexstring\" ( ignoreblindfail [\"assetcommitment,...\"] totalblinder )\n" + "blindrawtransaction \"hexstring\" ( ignoreblindfail [\"assetcommitment,...\"] blind_issuances \"totalblinder\" )\n" "\nConvert one or more outputs of a raw transaction into confidential ones using only wallet inputs.\n" "Returns the hex-encoded raw transaction.\n" "The output keys used can be specified by using a confidential address in createrawtransaction.\n" @@ -914,11 +917,13 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) "\nArguments:\n" "1. \"hexstring\", (string, required) A hex-encoded raw transaction.\n" "2. \"ignoreblindfail\"\" (bool, optional, default=true) Return a transaction even when a blinding attempt fails due to number of blinded inputs/outputs.\n" - "3. [ (array, optional) An array of input asset generators. If provided, this list must be empty, or match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" + "3. \"asset_commitments\" \n" + " [ (array, optional) An array of input asset generators. If provided, this list must be empty, or match the final input commitment list, including ordering, to make a valid surjection proof. This list does not include generators for issuances, as these assets are inherently unblinded.\n" " \"assetcommitment\" (string, optional) A hex-encoded asset commitment, one for each input.\n" " Null commitments must be \"\".\n" " ],\n" - "4. \"totalblinder\" (string, optional) Ignored for now.\n" + "4. \"blind_issuances\" (bool, optional, default=true) Blind the issuances found in the raw transaction or not. All issuances will be blinded if true. \n" + "5. \"totalblinder\" (string, optional) Ignored for now.\n" "\nResult:\n" "\"transaction\" (string) hex string of the transaction\n" @@ -930,8 +935,10 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)); } else if (request.params.size() == 3){ RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)); + } else if (request.params.size() == 4){ + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VBOOL)); } else { - RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VSTR)); + RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VSTR)(UniValue::VBOOL)(UniValue::VARR)(UniValue::VBOOL)(UniValue::VSTR)); } vector txData(ParseHexV(request.params[0], "argument 1")); @@ -966,8 +973,11 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) } } + bool blind_issuances = request.params[3].isNull() || request.params[3].get_bool(); + LOCK(pwalletMain->cs_wallet); + std::vector input_blinds; std::vector input_asset_blinds; std::vector input_assets; @@ -976,6 +986,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) std::vector output_asset_blinds; std::vector output_assets; std::vector output_pubkeys; + std::vector blind_issuance_asset; int n_blinded_ins = 0; for (size_t nIn = 0; nIn < tx.vin.size(); nIn++) { @@ -1014,8 +1025,14 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) std::vector asset_keys; std::vector token_keys; + // This fills out issuance blinding data for you from the wallet itself FillBlinds(tx, true, output_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys); + if (!blind_issuances) { + asset_keys.clear(); + token_keys.clear(); + } + // How many are we trying to blind? int numPubKeys = 0; unsigned int keyIndex = 0; @@ -1026,6 +1043,12 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) keyIndex = i; } } + for (const auto& key : asset_keys) { + if (key.IsValid()) numPubKeys++; + } + for (const auto& key : token_keys) { + if (key.IsValid()) numPubKeys++; + } if (numPubKeys == 0 && n_blinded_ins == 0) { // Vacuous, just return the transaction @@ -1046,7 +1069,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request) } } - if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, std::vector(), std::vector(), tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { + if (BlindTransaction(input_blinds, input_asset_blinds, input_assets, input_amounts, output_blinds, output_asset_blinds, output_pubkeys, asset_keys, token_keys, tx, (auxiliary_generators.size() ? &auxiliary_generators : NULL)) != numPubKeys) { // TODO Have more rich return values, communicating to user what has been blinded // User may be ok not blinding something that for instance has no corresponding type on input throw JSONRPCError(RPC_INVALID_PARAMETER, string("Unable to blind transaction: Are you sure each asset type to blind is represented in the inputs?")); @@ -1556,7 +1579,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "rawtransactions", "rawblindrawtransaction", &rawblindrawtransaction, false, {}}, #ifdef ENABLE_WALLET - { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {}}, + { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {"hexstring", "ignoreblindfail", "asset_commitments", "blind_issuances", "totalblinder"}}, #endif { "blockchain", "gettxoutproof", &gettxoutproof, true, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", &verifytxoutproof, true, {"proof"} }, From f44ec9f1fd96e647cac64888590bf8dec10994ab Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Sat, 1 Dec 2018 14:51:53 -0500 Subject: [PATCH 05/17] Fixup issueasset help results information --- src/wallet/rpcwallet.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 84efc32b1..706e81c80 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -4329,8 +4329,8 @@ UniValue issueasset(const JSONRPCRequest& request) "{ (json object)\n" " \"txid\":\"\", (string) Transaction id for issuance.\n" " \"vin\":\"n\", (numeric) The input position of the issuance in the transaction.\n" - " \"entropy\":\"\" (string) Entropy of the asset type.\n" - " \"asset\":\"\", (string) Asset type for issuance if known.\n" + " \"entropy\":\"\", (string) Entropy of the asset type.\n" + " \"asset\":\"\", (string) Asset type for issuance.\n" " \"token\":\"\", (string) Token type for issuance.\n" "}\n" "\nExamples:\n" From 4c2300f3c7ec722a17ee4b354ff4a40e29f58731 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Mon, 26 Nov 2018 15:56:39 -0500 Subject: [PATCH 06/17] rawissueasset RPC call --- src/rpc/client.cpp | 1 + src/rpc/rawtransaction.cpp | 193 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 9f3e83d2d..b27145333 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -142,6 +142,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtomainchain", 1, "subtractfeefromamount"}, { "getnewblockhex", 0, "required_age"}, { "initpegoutwallet", 1, "bip32_counter"}, + { "rawissueasset", 1, "issuances"}, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 9fbdb5875..59b63f404 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1568,6 +1568,198 @@ UniValue sendrawtransaction(const JSONRPCRequest& request) return hashTx.GetHex(); } +struct IssuanceDetails +{ + int input_index; + uint256 entropy; + CAsset asset; + CAsset token; +}; + +// Appends a single issuance to the first input that doesn't have one, and includes +// a single output per asset type in shuffled positions. +void issueasset_base(CMutableTransaction& mtx, IssuanceDetails& issuance_details, const CAmount asset_amount, const CAmount token_amount, const std::string& asset_address_str, const std::string& token_address_str, const bool blind_issuance, const uint256& contract_hash) +{ + + CBitcoinAddress asset_address(asset_address_str); + CBitcoinAddress token_address(token_address_str); + CScript asset_destination = GetScriptForDestination(asset_address.Get()); + CScript token_destination = GetScriptForDestination(token_address.Get()); + + // Find an input with no issuance field + size_t issuance_input_index = 0; + for (; issuance_input_index < mtx.vin.size(); issuance_input_index++) { + if (mtx.vin[issuance_input_index].assetIssuance.IsNull()) { + break; + } + } + // Can't add another one, exit + if (issuance_input_index == mtx.vin.size()) { + issuance_details.input_index = -1; + return; + } + + uint256 entropy; + CAsset asset; + CAsset token; + GenerateAssetEntropy(entropy, mtx.vin[issuance_input_index].prevout, contract_hash); + CalculateAsset(asset, entropy); + CalculateReissuanceToken(token, entropy, blind_issuance); + + issuance_details.input_index = issuance_input_index; + issuance_details.entropy = entropy; + issuance_details.asset = asset; + issuance_details.token = token; + + mtx.vin[issuance_input_index].assetIssuance.assetEntropy = contract_hash; + + // Place assets into randomly placed output slots, just insert in place + // -1 due to fee output being at the end no matter what. + int asset_place = GetRandInt(mtx.vout.size()-1); + int token_place = GetRandInt(mtx.vout.size()); // Don't bias insertion + + CTxOut asset_out(asset, asset_amount, asset_destination); + // If blinded address, insert the pubkey into the nonce field for later substitution by blinding + if (asset_address.IsBlinded()) { + CPubKey asset_blind = asset_address.GetBlindingKey(); + asset_out.nNonce.vchCommitment = std::vector(asset_blind.begin(), asset_blind.end()); + } + // Don't issue stuff or set values unless non-zero (both are against consensus) + if (asset_amount > 0) { + mtx.vout.insert(mtx.vout.begin()+asset_place, asset_out); + mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; + } + + CTxOut token_out(token, token_amount, token_destination); + // If blinded address, insert the pubkey into the nonce field for later substitution by blinding + if (token_address.IsBlinded()) { + CPubKey token_blind = token_address.GetBlindingKey(); + token_out.nNonce.vchCommitment = std::vector(token_blind.begin(), token_blind.end()); + } + // Don't issue stuff or set values unless non-zero (both are against consensus) + if (token_amount > 0) { + mtx.vout.insert(mtx.vout.begin()+token_place, token_out); + mtx.vin[issuance_input_index].assetIssuance.nInflationKeys = token_amount; + } +} + +UniValue rawissueasset(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw runtime_error( + "rawissueasset transaction [{\"asset_amount\":x.xxx, \"asset_address\":\"address\", \"token_amount\":x.xxx, \"token_address\":\"address\", \"blind\":bool, ( \"contract_hash\":\"hash\" )}, ...]\n" + "\nCreate an asset by attaching issuances to transaction inputs. Returns the transaction hex. There must be as many inputs as issuances requested. The final transaction hex is the final version of the transaction appended to the last object in the array.\n" + "\nArguments:\n" + "1. \"transaction\" (string, required) Transaction in hex in which to include a peg-in input.\n" + "2. \"issuances\" (list, required) List of issuances to create. Each issuance must have one non-zero amount. \n" + "[\n" + " {\n" + " \"asset_amount\":x.xxx (numeric or string, optional) Amount of asset to generate, if any.\n" + " \"asset_address\":addr (string, optional) Destination address of generated asset. Required if `asset_amount` given.\n" + " \"token_amount\":x.xxx (numeric or string, optional) Amount of reissuance token to generate, if any.\n" + " \"token_address\":addr (string, optional) Destination address of generated reissuance tokens. Required if `token_amount` given.\n" + " \"blind\":bool (bool, optional, default=true) Whether to mark the issuance input for blinding or not. Only affects issuances with re-issuance tokens." + " \"contract_hash\":str (string, optional, default=00..00) Contract hash that is put into issuance definition. Must be 32 bytes worth in hex string form. This will affect the asset id." + " }\n" + " ...\n" + "]\n" + "\nResult:\n" + "[ (json array) Results of issuances, in the order of `issuances` argument\n" + " { (json object)\n" + " \"hex\":, (string) The transaction with issuances appended. Only appended to final index in returned array.\n" + " \"vin\":\"n\", (numeric) The input position of the issuance in the transaction.\n" + " \"entropy\":\"\" (string) Entropy of the asset type.\n" + " \"asset\":\"\", (string) Asset type for issuance if known.\n" + " \"token\":\"\", (string) Token type for issuance.\n" + " },\n" + " ...\n" + "]" + ); + + CMutableTransaction mtx; + + if (!DecodeHexTx(mtx, request.params[0].get_str())) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + UniValue issuances = request.params[1].get_array(); + + std::string asset_address_str = ""; + std::string token_address_str = ""; + + UniValue ret(UniValue::VARR); + + // Count issuances, only append hex to final one + unsigned int issuances_til_now = 0; + + for (unsigned int idx = 0; idx < issuances.size(); idx++) { + const UniValue& issuance = issuances[idx]; + const UniValue& issuance_o = issuance.get_obj(); + + CAmount asset_amount = 0; + const UniValue& asset_amount_uni = issuance_o["asset_amount"]; + if (asset_amount_uni.isNum()) { + asset_amount = AmountFromValue(asset_amount_uni); + if (asset_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, asset_amount must be positive"); + } + const UniValue& asset_address_uni = issuance_o["asset_address"]; + if (!asset_address_uni.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing corresponding asset_address"); + } + asset_address_str = asset_address_uni.get_str(); + } + + CAmount token_amount = 0; + const UniValue& token_amount_uni = issuance_o["token_amount"]; + if (token_amount_uni.isNum()) { + token_amount = AmountFromValue(token_amount_uni); + if (token_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, token_amount must be positive"); + } + const UniValue& token_address_uni = issuance_o["token_address"]; + if (!token_address_uni.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing corresponding token_address"); + } + token_address_str = token_address_uni.get_str(); + } + if (asset_amount == 0 && token_amount == 0) { + throw JSONRPCError(RPC_TYPE_ERROR, "Issuance must have one non-zero component"); + } + + // If we have issuances, check if reissuance tokens will be generated via blinding path + const UniValue blind_uni = issuance_o["blind"]; + const bool blind_issuance = !blind_uni.isBool() || blind_uni.get_bool(); + + // Check for optional contract to hash into definition + uint256 contract_hash; + if (!issuance_o["contract_hash"].isNull()) { + contract_hash = ParseHashV(issuance_o["contract_hash"], "contract_hash"); + } + + IssuanceDetails details; + + issueasset_base(mtx, details, asset_amount, token_amount, asset_address_str, token_address_str, blind_issuance, contract_hash); + if (details.input_index == -1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to find enough blank inputs for listed issuances."); + } + + issuances_til_now++; + + UniValue obj(UniValue::VOBJ); + if (issuances_til_now == issuances.size()) { + obj.pushKV("hex", EncodeHexTx(mtx, RPCSerializationFlags())); + } + obj.pushKV("vin", details.input_index); + obj.pushKV("entropy", details.entropy.GetHex()); + obj.pushKV("asset", details.asset.GetHex()); + obj.pushKV("token", details.token.GetHex()); + + ret.push_back(obj); + } + + return ret; +} + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- @@ -1578,6 +1770,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "sendrawtransaction", &sendrawtransaction, false, {"hexstring","allowhighfees"} }, { "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "rawtransactions", "rawblindrawtransaction", &rawblindrawtransaction, false, {}}, + { "rawtransactions", "rawissueasset", &rawissueasset, false, {"transaction", "issuances"}}, #ifdef ENABLE_WALLET { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {"hexstring", "ignoreblindfail", "asset_commitments", "blind_issuances", "totalblinder"}}, #endif From e6ff6abbfebdc74355385ddd34a78d323003c92f Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Tue, 27 Nov 2018 15:49:29 -0500 Subject: [PATCH 07/17] add functional testing for raw issuance call --- qa/rpc-tests/feature_issuance.py | 151 +++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py index 77e3ab746..ea3284cfa 100755 --- a/qa/rpc-tests/feature_issuance.py +++ b/qa/rpc-tests/feature_issuance.py @@ -8,6 +8,82 @@ " Tests issued assets functionality including (re)issuance, and de-issuance " +# Creates a raw issuance transaction based on the passed in list, checking important details after +def process_raw_issuance(node, issuance_list): + if len(issuance_list) > 5: + raise Exception('Issuance list too long') + # Make enough outputs for any subsequent spend + next_destinations = {} + output_values = (node.getbalance()['bitcoin']-1)/5 + for i in range(5): + next_destinations[node.getnewaddress()] = output_values + + raw_tx = node.createrawtransaction([], next_destinations) + funded_tx = node.fundrawtransaction(raw_tx)['hex'] + issued_call_details = node.rawissueasset(funded_tx, issuance_list) + issued_tx = issued_call_details[-1]["hex"] # Get hex from end + # don't accept blinding fail, and blind all issuances or none at all + blind_tx = node.blindrawtransaction(issued_tx, False, [], issuance_list[0]["blind"]) + signed_tx = node.signrawtransaction(blind_tx) + tx_id = node.sendrawtransaction(signed_tx['hex']) + node.generate(1) + assert_equal(node.gettransaction(tx_id)["confirmations"], 1) + num_issuance = 0 + decoded_tx = node.decoderawtransaction(signed_tx['hex']) + decoded_unblind_tx = node.decoderawtransaction(issued_tx) + for i, (issuance_req, tx_input, issuance_result) in enumerate(zip(issuance_list, decoded_tx["vin"], issued_call_details)): + if "issuance" not in tx_input: + break + + num_issuance += 1 + issuance_details = tx_input["issuance"] + if "blind" not in issuance_req or issuance_req["blind"] == True: + + assert("assetamount" not in issuance_details) + assert("tokenamount" not in issuance_details) + assert_equal(issuance_details["assetBlindingNonce"], "00"*32) + if "asset_amount" in issuance_req: + assert("assetamountcommitment" in issuance_details) + if "token_amount" in issuance_req: + assert("tokenamountcommitment" in issuance_details) + else: + if "asset_amount" in issuance_req: + assert_equal(issuance_details["assetamount"], issuance_req["asset_amount"]) + if "token_amount" in issuance_req: + assert_equal(issuance_details["tokenamount"], issuance_req["token_amount"]) + + # Cross-check RPC call result details with raw details + assert_equal(issuance_result["vin"], i) + assert_equal(issuance_result["entropy"], issuance_details["assetEntropy"]) + if "asset" in issuance_details: + assert_equal(issuance_result["asset"], issuance_details["asset"]) + if "token" in issuance_details: + assert_equal(issuance_result["token"], issuance_details["token"]) + + # Look for outputs assets where we expect them, or not, initial issuance first then token + for issuance_type in ["asset", "token"]: + blind_dest = issuance_type+"_address" not in issuance_req or node.validateaddress(issuance_req[issuance_type+"_address"])["confidential_key"] != "" + if blind_dest: + # We should not find any the issuances we made since the addresses confidential + for output in decoded_tx["vout"]: + if "asset" in output and output["asset"] == issuance_details[issuance_type]: + raise Exception("Found asset in plaintext that should be confidential!") + + # Now scan unblinded version of issuance outputs + asset_found = False + for output in decoded_unblind_tx["vout"]: + if "asset" in output and output["asset"] == issuance_details[issuance_type]: + if issuance_type+"_address" not in issuance_req: + raise Exception("Found asset type not requested") + if "value" in output and \ + output["value"] == issuance_req[issuance_type+"_amount"]: + asset_found = True + + # Find the asset type if it was created + assert(asset_found if issuance_type+"_address" in issuance_req else True) + + assert_equal(num_issuance, len(issuance_list)) + class IssuanceTest (BitcoinTestFramework): def __init__(self): @@ -160,5 +236,80 @@ def run_test(self): assert_equal(self.nodes[0].getwalletinfo()["balance"][issued["token"]], 1) + print("Raw issuance tests") + # Addresses to send to to check proper blinding + blind_addr = self.nodes[0].getnewaddress() + nonblind_addr = self.nodes[0].validateaddress(blind_addr)['unconfidential'] + + # Fail making non-witness issuance sourcing a single unblinded output. + # See: https://github.com/ElementsProject/elements/issues/473 + total_amount = self.nodes[0].getbalance()['bitcoin'] + self.nodes[0].sendtoaddress(nonblind_addr, total_amount, "", "", True) + self.nodes[1].generate(1) + raw_tx = self.nodes[0].createrawtransaction([], {nonblind_addr:self.nodes[0].getbalance()['bitcoin']-1}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr}])[0]["hex"] + blind_tx = self.nodes[0].blindrawtransaction(issued_tx) + signed_tx = self.nodes[0].signrawtransaction(blind_tx) + assert_raises_jsonrpc(-26, "", self.nodes[0].sendrawtransaction, signed_tx['hex']) + + # Make single blinded output to ensure we work around above issue + total_amount = self.nodes[0].getbalance()['bitcoin'] + self.nodes[0].sendtoaddress(blind_addr, total_amount, "", "", True) + self.nodes[1].generate(1) + + # Start with single issuance input, unblinded (makes 5 outputs for later larger issuances) + process_raw_issuance(self.nodes[0], [{"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":2, "asset_address":nonblind_addr, "blind":True}]) + process_raw_issuance(self.nodes[0], [{"token_amount":5, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}]) + # Now do multiple with some issuance outputs blind, some unblinded + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":blind_addr, "token_amount":2, "token_address":nonblind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + # Up to 5 issuances since we're making 5 outputs each time + process_raw_issuance(self.nodes[0], [{"asset_amount":7, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}, {"asset_amount":2, "asset_address":nonblind_addr, "blind":False}]) + process_raw_issuance(self.nodes[0], [{"asset_amount":1, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":False}, {"asset_amount":3, "asset_address":nonblind_addr, "blind":False}, {"asset_amount":4, "asset_address":nonblind_addr, "token_amount":5, "token_address":blind_addr, "blind":False}, {"asset_amount":6, "asset_address":nonblind_addr, "token_amount":7, "token_address":blind_addr, "blind":False}, {"asset_amount":8, "asset_address":nonblind_addr, "token_amount":9, "token_address":blind_addr, "blind":False}]) + # Default "blind" value is true, ommitting explicit argument for last + process_raw_issuance(self.nodes[0], [{"asset_amount":1, "asset_address":nonblind_addr, "token_amount":2, "token_address":blind_addr, "blind":True}, {"asset_amount":3, "asset_address":nonblind_addr, "blind":True}, {"asset_amount":4, "asset_address":nonblind_addr, "token_amount":5, "token_address":blind_addr, "blind":True}, {"asset_amount":6, "asset_address":nonblind_addr, "token_amount":7, "token_address":blind_addr, "blind":True}, {"asset_amount":8, "asset_address":nonblind_addr, "token_amount":9, "token_address":blind_addr}]) + + # Make sure contract hash is being interpreted as expected, resulting in different asset ids + raw_tx = self.nodes[0].createrawtransaction([], {nonblind_addr:self.nodes[0].getbalance()['bitcoin']-1}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] + id_set = set() + + # First issue an asset with no argument + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + + # Again with 00..00 argument, which match the no-argument case + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"00"*32}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 1) + + # Random contract string should again differ + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"deadbeef"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 2) + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract_hash":"deadbeee"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 3) + + # Finally, append an issuance on top of an already-"issued" raw tx + # Same contract, different utxo being spent results in new asset type + issued_tx = self.nodes[2].rawissueasset(issued_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract":"deadbeee"*8}])[0]["hex"] + decode_tx = self.nodes[0].decoderawtransaction(issued_tx) + id_set.add(decode_tx["vin"][1]["issuance"]["asset"]) + assert_equal(len(id_set), 4) + # This issuance should not have changed + id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) + assert_equal(len(id_set), 4) + if __name__ == '__main__': IssuanceTest ().main () From 703abb8868e42afb17cef7a996ac7ffc1b0b666a Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Mon, 3 Dec 2018 16:49:33 +0100 Subject: [PATCH 08/17] add confidential_key explanation in validateaddress help --- src/rpc/misc.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index c304d3115..76aea0cc6 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -184,6 +184,7 @@ UniValue validateaddress(const JSONRPCRequest& request) " \"timestamp\" : timestamp, (number, optional) The creation time of the key if available in seconds since epoch (Jan 1 1970 GMT)\n" " \"unconfidential\" : \"address\" (string) The address without confidentiality key\n" " \"confidential\" : \"address\" (string) Confidential version of the address, only if it is yours and unconfidential\n" + " \"confidential_key\" : \"publickeyhex\" (string) The hex value of the raw blinding public key for that address, if any.\n" " \"hdkeypath\" : \"keypath\" (string, optional) The HD keypath if the key is HD and available\n" " \"hdmasterkeyid\" : \"\" (string, optional) The Hash160 of the HD master pubkey\n" "}\n" From 55e3ca8e6647eec94ca48e0698d662ec5799da19 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 12 Dec 2018 16:11:49 -0500 Subject: [PATCH 09/17] blindrawtransaction: Have blind_issuances parse correctly from command line --- src/rpc/client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index b27145333..d63420563 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -93,6 +93,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "rawblindrawtransaction", 6, "ignoreblindfail" }, { "blindrawtransaction", 1, "assetcommitments" }, { "blindrawtransaction", 2, "ignoreblindfail" }, + { "blindrawtransaction", 3, "blind_issuances" }, { "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 1, "outputs" }, { "dumpissuanceblindingkey", 1, "vin" }, From bfe1be3e7587d7887457acaba4b742d7ab9d788d Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 12 Dec 2018 16:12:30 -0500 Subject: [PATCH 10/17] Correct how wallet blinding logic detects which token id to use based on blinding keys --- src/blind.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/blind.cpp b/src/blind.cpp index a650b67fb..0c77d0c85 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -266,10 +266,10 @@ int BlindTransaction(std::vector& input_blinding_factors, const std::v } // New Issuance if (issuance.assetBlindingNonce.IsNull()) { - bool assetToBlind = (vBlindIssuanceAsset.size() > i && vBlindIssuanceAsset[i].IsValid()) ? true : false; + bool blind_issuance = (vBlindIssuanceToken.size() > i && vBlindIssuanceToken[i].IsValid()) ? true : false; GenerateAssetEntropy(entropy, tx.vin[i].prevout, issuance.assetEntropy); CalculateAsset(asset, entropy); - CalculateReissuanceToken(token, entropy, assetToBlind); + CalculateReissuanceToken(token, entropy, blind_issuance); } else { CalculateAsset(asset, issuance.assetEntropy); } @@ -396,8 +396,8 @@ int BlindTransaction(std::vector& input_blinding_factors, const std::v if (nPseudo == 0) { CalculateAsset(asset, entropy); } else { - bool assetToBlind = (vBlindIssuanceAsset.size() > nIn && vBlindIssuanceAsset[nIn].IsValid()) ? true : false; - CalculateReissuanceToken(asset, entropy, assetToBlind); + bool blind_issuance = (vBlindIssuanceToken.size() > nIn && vBlindIssuanceToken[nIn].IsValid()) ? true : false; + CalculateReissuanceToken(asset, entropy, blind_issuance); } } else { if (nPseudo == 0) { From 7c11d37fbf449fcbca2a84d51a3e06d57371e9f4 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 12 Dec 2018 16:13:55 -0500 Subject: [PATCH 11/17] issueasset_base: Push 0-value value to issuance field if blinding --- src/rpc/rawtransaction.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 59b63f404..3325f02b4 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1624,10 +1624,13 @@ void issueasset_base(CMutableTransaction& mtx, IssuanceDetails& issuance_details CPubKey asset_blind = asset_address.GetBlindingKey(); asset_out.nNonce.vchCommitment = std::vector(asset_blind.begin(), asset_blind.end()); } - // Don't issue stuff or set values unless non-zero (both are against consensus) + // Explicit 0 is represented by a null value, don't set to non-null in that case + if (blind_issuance || asset_amount != 0) { + mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; + } + // Don't make zero value output(impossible by consensus) if (asset_amount > 0) { mtx.vout.insert(mtx.vout.begin()+asset_place, asset_out); - mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; } CTxOut token_out(token, token_amount, token_destination); @@ -1636,10 +1639,13 @@ void issueasset_base(CMutableTransaction& mtx, IssuanceDetails& issuance_details CPubKey token_blind = token_address.GetBlindingKey(); token_out.nNonce.vchCommitment = std::vector(token_blind.begin(), token_blind.end()); } - // Don't issue stuff or set values unless non-zero (both are against consensus) + // Explicit 0 is represented by a null value, don't set to non-null in that case + if (blind_issuance || token_amount != 0) { + mtx.vin[issuance_input_index].assetIssuance.nInflationKeys = token_amount; + } + // Don't make zero value output(impossible by consensus) if (token_amount > 0) { mtx.vout.insert(mtx.vout.begin()+token_place, token_out); - mtx.vin[issuance_input_index].assetIssuance.nInflationKeys = token_amount; } } From 62b7d3702ac74a9d48c442f7fac4bc1207a007b7 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Sat, 1 Dec 2018 16:02:40 -0500 Subject: [PATCH 12/17] rawreissuance rpc --- src/rpc/client.cpp | 1 + src/rpc/rawtransaction.cpp | 127 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index d63420563..8dda79aa3 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -144,6 +144,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getnewblockhex", 0, "required_age"}, { "initpegoutwallet", 1, "bip32_counter"}, { "rawissueasset", 1, "issuances"}, + { "rawreissueasset", 1, "reissuances"}, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 3325f02b4..fe46523c3 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1649,6 +1649,42 @@ void issueasset_base(CMutableTransaction& mtx, IssuanceDetails& issuance_details } } +// Appends a single reissuance to the specified input if none exists, +// and the corresponding output in a shuffled position. Errors otherwise. +void reissueasset_base(CMutableTransaction& mtx, int& issuance_input_index, const CAmount asset_amount, const std::string& asset_address_str, const uint256& asset_blinder, const uint256& entropy) +{ + + CBitcoinAddress asset_address(asset_address_str); + CScript asset_destination = GetScriptForDestination(asset_address.Get()); + + // Check if issuance already exists, error if already exists + if ((size_t)issuance_input_index >= mtx.vin.size() || !mtx.vin[issuance_input_index].assetIssuance.IsNull()) { + issuance_input_index = -1; + return; + } + + CAsset asset; + CalculateAsset(asset, entropy); + + mtx.vin[issuance_input_index].assetIssuance.assetEntropy = entropy; + mtx.vin[issuance_input_index].assetIssuance.assetBlindingNonce = asset_blinder; + mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; + + // Place assets into randomly placed output slots, before change output, inserted in place + assert(mtx.vout.size() >= 1); + int asset_place = GetRandInt(mtx.vout.size()-1); + + CTxOut asset_out(asset, asset_amount, asset_destination); + // If blinded address, insert the pubkey into the nonce field for later substitution by blinding + if (asset_address.IsBlinded()) { + CPubKey asset_blind = asset_address.GetBlindingKey(); + asset_out.nNonce.vchCommitment = std::vector(asset_blind.begin(), asset_blind.end()); + } + assert(asset_amount > 0); + mtx.vout.insert(mtx.vout.begin()+asset_place, asset_out); + mtx.vin[issuance_input_index].assetIssuance.nAmount = asset_amount; +} + UniValue rawissueasset(const JSONRPCRequest& request) { if (request.fHelp || request.params.size() != 2) @@ -1766,6 +1802,96 @@ UniValue rawissueasset(const JSONRPCRequest& request) return ret; } +UniValue rawreissueasset(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw runtime_error( + "rawreissueasset transaction {\"vin\":\"n\", \"asset_amount\":x.xxx, \"asset_address\":\"address\", \"asset_blinder\":, \"entropy\":, ( \"contract_hash\": )}\n" + "\nRe-issue an asset by attaching pseudo-inputs to transaction inputs, revealing the underlying reissuance token of the input. Returns the transaction hex.\n" + "\nArguments:\n" + "1. \"transaction\" (string, required) Transaction in hex in which to include an issuance input.\n" + "2. \"reissuances\" (list, required) List of re-issuances to create. Each issuance must have one non-zero amount.\n" + "[\n" + " {\n" + " \"input_index\":\"n\", (numeric, required) The input position of the reissuance in the transaction.\n" + " \"asset_amount\":x.xxx, (numeric or string, required) Amount of asset to generate, if any.\n" + " \"asset_address\":addr, (string, required) Destination address of generated asset. Required if `asset_amount` given.\n" + " \"asset_blinder\":, (string, required) The blinding factor of the reissuance token output being spent.\n" + " \"entropy\":, (string, required) The `entropy` returned during initial issuance for the asset being reissued." + " }\n" + "\nResult:\n" + "{ (json object)\n" + " \"hex\":, (string) The transaction with reissuances appended.\n" + "}\n" + ); + + CMutableTransaction mtx; + + if (!DecodeHexTx(mtx, request.params[0].get_str())) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + if (mtx.vout.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction must have at least one output."); + } + + UniValue issuances = request.params[1].get_array(); + + unsigned int num_issuances = 0; + + for (unsigned int idx = 0; idx < issuances.size(); idx++) { + const UniValue& issuance = issuances[idx]; + const UniValue& issuance_o = issuance.get_obj(); + + CAmount asset_amount = 0; + const UniValue& asset_amount_uni = issuance_o["asset_amount"]; + if (asset_amount_uni.isNum()) { + asset_amount = AmountFromValue(asset_amount_uni); + if (asset_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, asset_amount must be positive"); + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Asset amount must be given for each reissuance."); + } + + const UniValue& asset_address_uni = issuance_o["asset_address"]; + if (!asset_address_uni.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Reissuance missing asset_address"); + } + std::string asset_address_str = asset_address_uni.get_str(); + + int input_index = -1; + const UniValue& input_index_o = issuance_o["input_index"]; + if (input_index_o.isNum()) { + input_index = input_index_o.get_int(); + if (input_index < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Input index must be non-negative."); + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Input indexes for all reissuances are required."); + } + + uint256 asset_blinder = ParseHashV(issuance_o["asset_blinder"], "asset_blinder"); + + uint256 entropy = ParseHashV(issuance_o["entropy"], "entropy"); + + reissueasset_base(mtx, input_index, asset_amount, asset_address_str, asset_blinder, entropy); + if (input_index == -1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Selected transaction input already has issuance data."); + } + + num_issuances++; + } + + if (num_issuances != issuances.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to find enough blank inputs for listed issuances."); + } + + UniValue ret(UniValue::VOBJ); + ret.pushKV("hex", EncodeHexTx(mtx, RPCSerializationFlags())); + return ret; +} + + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- @@ -1777,6 +1903,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "rawtransactions", "rawblindrawtransaction", &rawblindrawtransaction, false, {}}, { "rawtransactions", "rawissueasset", &rawissueasset, false, {"transaction", "issuances"}}, + { "rawtransactions", "rawreissueasset", &rawreissueasset, false, {"transaction", "reissuances"}}, #ifdef ENABLE_WALLET { "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {"hexstring", "ignoreblindfail", "asset_commitments", "blind_issuances", "totalblinder"}}, #endif From bc26f6be3b321e37abf4426bbd5e6d4874b89a2a Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Sun, 2 Dec 2018 17:18:05 +0100 Subject: [PATCH 13/17] rawreissueasset functional tests --- qa/rpc-tests/feature_issuance.py | 120 ++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py index ea3284cfa..57d4836d5 100755 --- a/qa/rpc-tests/feature_issuance.py +++ b/qa/rpc-tests/feature_issuance.py @@ -248,8 +248,8 @@ def run_test(self): self.nodes[1].generate(1) raw_tx = self.nodes[0].createrawtransaction([], {nonblind_addr:self.nodes[0].getbalance()['bitcoin']-1}) funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] - issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr}])[0]["hex"] - blind_tx = self.nodes[0].blindrawtransaction(issued_tx) + issued_tx = self.nodes[2].rawissueasset(funded_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "blind":False}])[0]["hex"] + blind_tx = self.nodes[0].blindrawtransaction(issued_tx) # This is a no-op signed_tx = self.nodes[0].signrawtransaction(blind_tx) assert_raises_jsonrpc(-26, "", self.nodes[0].sendrawtransaction, signed_tx['hex']) @@ -303,13 +303,127 @@ def run_test(self): # Finally, append an issuance on top of an already-"issued" raw tx # Same contract, different utxo being spent results in new asset type - issued_tx = self.nodes[2].rawissueasset(issued_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "contract":"deadbeee"*8}])[0]["hex"] + # We also create a reissuance token to test reissuance with contract hash + issued_tx = self.nodes[2].rawissueasset(issued_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "token_address":nonblind_addr, "contract":"deadbeee"*8}])[0]["hex"] decode_tx = self.nodes[0].decoderawtransaction(issued_tx) id_set.add(decode_tx["vin"][1]["issuance"]["asset"]) + non_null_contract_token = decode_tx["vin"][1]["issuance"]["token"] assert_equal(len(id_set), 4) # This issuance should not have changed id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) assert_equal(len(id_set), 4) + print("Raw reissuance tests") + issued_asset = self.nodes[0].issueasset(0, 1) + self.nodes[0].generate(1) + utxo_info = None + # Find info about the token output using wallet + for utxo in self.nodes[0].listunspent(): + if utxo["asset"] == issued_asset["token"]: + utxo_info = utxo + break + assert(utxo_info is not None) + + issued_address = self.nodes[0].getnewaddress() + # Create transaction spending the reissuance token + raw_tx = self.nodes[0].createrawtransaction([], {issued_address:Decimal('0.00000001')}, 0, {issued_address:issued_asset["token"]}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)['hex'] + # Find the reissuance input + reissuance_index = -1 + for i, tx_input in enumerate(self.nodes[0].decoderawtransaction(funded_tx)["vin"]): + if tx_input["txid"] == utxo_info["txid"] and tx_input["vout"] == utxo_info["vout"]: + reissuance_index = i + break + assert(reissuance_index != -1) + reissued_tx = self.nodes[0].rawreissueasset(funded_tx, [{"asset_amount":3, "asset_address":self.nodes[0].getnewaddress(), "input_index":reissuance_index, "asset_blinder":utxo_info["assetblinder"], "entropy":issued_asset["entropy"]}]) + blind_tx = self.nodes[0].blindrawtransaction(reissued_tx["hex"]) + signed_tx = self.nodes[0].signrawtransaction(blind_tx) + tx_id = self.nodes[0].sendrawtransaction(signed_tx["hex"]) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].gettransaction(tx_id)["confirmations"], 1) + + # Now send reissuance token to blinded multisig, then reissue + addrs = [] + for i in range(3): + addrs.append(self.nodes[0].validateaddress(self.nodes[0].getnewaddress())["pubkey"]) + + + multisig_addr = self.nodes[0].createmultisig(2,addrs) + blinded_addr = self.nodes[0].getnewaddress() + blinding_pubkey = self.nodes[0].validateaddress(blinded_addr)["confidential_key"] + blinding_privkey = self.nodes[0].dumpblindingkey(blinded_addr) + blinded_multisig = self.nodes[0].createblindedaddress(multisig_addr["address"], blinding_pubkey) + # Import address so we consider the reissuance tokens ours + self.nodes[0].importaddress(blinded_multisig) + # Import blinding key to be able to decrypt values sent to it + self.nodes[0].importblindingkey(blinded_multisig, blinding_privkey) + + self.nodes[0].sendtoaddress(blinded_multisig, self.nodes[0].getbalance()[issued_asset["asset"]], "", "", False, issued_asset["asset"]) + self.nodes[0].generate(1) + + # Get that multisig output + utxo_info = None + # Find info about the token output using wallet + for utxo in self.nodes[0].listunspent(): + if utxo["asset"] == issued_asset["token"]: + utxo_info = utxo + break + assert(utxo_info is not None) + + # Now make transaction spending that input + raw_tx = self.nodes[0].createrawtransaction([], {issued_address:1}, 0, {issued_address:issued_asset["token"]}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)["hex"] + # Find the reissuance input + reissuance_index = -1 + for i, tx_input in enumerate(self.nodes[0].decoderawtransaction(funded_tx)["vin"]): + if tx_input["txid"] == utxo_info["txid"] and tx_input["vout"] == utxo_info["vout"]: + reissuance_index = i + break + assert(reissuance_index != -1) + reissued_tx = self.nodes[0].rawreissueasset(funded_tx, [{"asset_amount":3, "asset_address":self.nodes[0].getnewaddress(), "input_index":reissuance_index, "asset_blinder":utxo_info["assetblinder"], "entropy":issued_asset["entropy"]}]) + + blind_tx = self.nodes[0].blindrawtransaction(reissued_tx["hex"]) + signed_tx = self.nodes[0].signrawtransaction(blind_tx) + tx_id = self.nodes[0].sendrawtransaction(signed_tx["hex"]) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].gettransaction(tx_id)["confirmations"], 1) + + # Now make transaction spending a token that had non-null contract_hash + contract_hash = "deadbeee"*8 + raw_tx = self.nodes[0].createrawtransaction([], {self.nodes[0].getnewaddress():1}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)["hex"] + issued_tx = self.nodes[0].rawissueasset(funded_tx, [{"token_amount":1, "token_address":self.nodes[0].getnewaddress(), "contract_hash":contract_hash}])[0] + blinded_tx = self.nodes[0].blindrawtransaction(issued_tx["hex"]) + signed_tx = self.nodes[0].signrawtransaction(blinded_tx) + tx_id = self.nodes[0].sendrawtransaction(signed_tx["hex"]) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].gettransaction(tx_id)["confirmations"], 1) + + utxo_info = None + # Find info about the token output using wallet + for utxo in self.nodes[0].listunspent(): + if utxo["asset"] == issued_tx["token"]: + utxo_info = utxo + break + assert(utxo_info is not None) + + # Now spend the token, and create reissuance + raw_tx = self.nodes[0].createrawtransaction([], {issued_address:1}, 0, {issued_address:issued_tx["token"]}) + funded_tx = self.nodes[0].fundrawtransaction(raw_tx)["hex"] + # Find the reissuance input + reissuance_index = -1 + for i, tx_input in enumerate(self.nodes[0].decoderawtransaction(funded_tx)["vin"]): + if tx_input["txid"] == utxo_info["txid"] and tx_input["vout"] == utxo_info["vout"]: + reissuance_index = i + break + assert(reissuance_index != -1) + reissued_tx = self.nodes[0].rawreissueasset(funded_tx, [{"asset_amount":3, "asset_address":self.nodes[0].getnewaddress(), "input_index":reissuance_index, "asset_blinder":utxo_info["assetblinder"], "entropy":issued_tx["entropy"]}]) + + blind_tx = self.nodes[0].blindrawtransaction(reissued_tx["hex"], False) + signed_tx = self.nodes[0].signrawtransaction(blind_tx) + tx_id = self.nodes[0].sendrawtransaction(signed_tx["hex"]) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].gettransaction(tx_id)["confirmations"], 1) + if __name__ == '__main__': IssuanceTest ().main () From 19fe1ac4ed1d6e19b50be35314f52477cdb0a3e6 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 12 Dec 2018 16:27:16 -0500 Subject: [PATCH 14/17] fixup rawissueasset help comment --- src/rpc/rawtransaction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index fe46523c3..1d143dde9 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1692,7 +1692,7 @@ UniValue rawissueasset(const JSONRPCRequest& request) "rawissueasset transaction [{\"asset_amount\":x.xxx, \"asset_address\":\"address\", \"token_amount\":x.xxx, \"token_address\":\"address\", \"blind\":bool, ( \"contract_hash\":\"hash\" )}, ...]\n" "\nCreate an asset by attaching issuances to transaction inputs. Returns the transaction hex. There must be as many inputs as issuances requested. The final transaction hex is the final version of the transaction appended to the last object in the array.\n" "\nArguments:\n" - "1. \"transaction\" (string, required) Transaction in hex in which to include a peg-in input.\n" + "1. \"transaction\" (string, required) Transaction in hex in which to include an issuance input.\n" "2. \"issuances\" (list, required) List of issuances to create. Each issuance must have one non-zero amount. \n" "[\n" " {\n" From 169545d71763350750ec1aca2428241e7ed11eea Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Mon, 17 Dec 2018 15:52:59 -0500 Subject: [PATCH 15/17] f'rawreissueasset functional tests' --- qa/rpc-tests/feature_issuance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py index 57d4836d5..8c7aeaf80 100755 --- a/qa/rpc-tests/feature_issuance.py +++ b/qa/rpc-tests/feature_issuance.py @@ -307,7 +307,6 @@ def run_test(self): issued_tx = self.nodes[2].rawissueasset(issued_tx, [{"asset_amount":1, "asset_address":nonblind_addr, "token_address":nonblind_addr, "contract":"deadbeee"*8}])[0]["hex"] decode_tx = self.nodes[0].decoderawtransaction(issued_tx) id_set.add(decode_tx["vin"][1]["issuance"]["asset"]) - non_null_contract_token = decode_tx["vin"][1]["issuance"]["token"] assert_equal(len(id_set), 4) # This issuance should not have changed id_set.add(decode_tx["vin"][0]["issuance"]["asset"]) From db81539b08ed21a1e2ab2fe0814c659dd809e4f4 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Thu, 8 Nov 2018 10:09:24 -0500 Subject: [PATCH 16/17] fix endianness of getrawtransaction issuance entropy, add basic test --- qa/rpc-tests/confidential_transactions.py | 6 ++++++ src/rpc/rawtransaction.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qa/rpc-tests/confidential_transactions.py b/qa/rpc-tests/confidential_transactions.py index 509a8acb6..7f323b51c 100755 --- a/qa/rpc-tests/confidential_transactions.py +++ b/qa/rpc-tests/confidential_transactions.py @@ -217,6 +217,12 @@ def run_test(self): issued = self.nodes[0].issueasset(1, 1, False) self.nodes[0].reissueasset(issued["asset"], 1) + # Compare resulting fields with getrawtransaction + raw_details = self.nodes[0].getrawtransaction(issued["txid"], 1) + assert_equal(issued["entropy"], raw_details["vin"][issued["vin"]]["issuance"]["assetEntropy"]) + assert_equal(issued["asset"], raw_details["vin"][issued["vin"]]["issuance"]["asset"]) + assert_equal(issued["token"], raw_details["vin"][issued["vin"]]["issuance"]["token"]) + self.nodes[0].generate(1) self.sync_all() diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 1d143dde9..901d872bb 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -155,7 +155,7 @@ void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry) uint256 entropy; if (issuance.assetBlindingNonce.IsNull()) { GenerateAssetEntropy(entropy, txin.prevout, issuance.assetEntropy); - issue.push_back(Pair("assetEntropy", HexStr(entropy))); + issue.push_back(Pair("assetEntropy", entropy.GetHex())); CalculateAsset(asset, entropy); CalculateReissuanceToken(token, entropy, issuance.nAmount.IsCommitment()); issue.push_back(Pair("isreissuance", false)); From 0ab580f098300d54b10d866270cd9f7bf1f31084 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Tue, 18 Dec 2018 12:59:30 -0500 Subject: [PATCH 17/17] fixup backported tests --- qa/rpc-tests/confidential_transactions.py | 2 +- qa/rpc-tests/feature_issuance.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qa/rpc-tests/confidential_transactions.py b/qa/rpc-tests/confidential_transactions.py index 7f323b51c..b596dafc7 100755 --- a/qa/rpc-tests/confidential_transactions.py +++ b/qa/rpc-tests/confidential_transactions.py @@ -331,7 +331,7 @@ def run_test(self): txid1 = self.nodes[0].sendtoaddress(blinded_addr, 1) txid2 = self.nodes[0].sendtoaddress(blinded_addr, 3) unspent = self.nodes[0].listunspent(0, 0) - assert_equal(len(unspent), 4) + assert_equal(len(unspent), 3) rawtx = self.nodes[0].createrawtransaction([{"txid":unspent[0]["txid"], "vout":unspent[0]["vout"]}, {"txid":unspent[1]["txid"], "vout":unspent[1]["vout"]}], {addr:unspent[0]["amount"]+unspent[1]["amount"]-Decimal("0.2"), "fee":Decimal("0.2")}) # Blinding will fail with 2 blinded inputs and 0 blinded outputs # since it has no notion of a wallet to fill in a 0-value OP_RETURN output diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py index 8c7aeaf80..ad79fedf4 100755 --- a/qa/rpc-tests/feature_issuance.py +++ b/qa/rpc-tests/feature_issuance.py @@ -14,7 +14,7 @@ def process_raw_issuance(node, issuance_list): raise Exception('Issuance list too long') # Make enough outputs for any subsequent spend next_destinations = {} - output_values = (node.getbalance()['bitcoin']-1)/5 + output_values = round(float((node.getbalance()['bitcoin']-1)/5), 8) for i in range(5): next_destinations[node.getnewaddress()] = output_values @@ -197,7 +197,7 @@ def run_test(self): addr2 = txdet2[len(txdet2)-1]["address"] addr3 = txdet3[len(txdet3)-1]["address"] - assert_equal(len(self.nodes[0].listissuances()), 6); + assert_equal(len(self.nodes[0].listissuances()), 7); self.nodes[0].importaddress(addr1) self.nodes[0].importaddress(addr2) self.nodes[0].importaddress(addr3)