diff --git a/qa/rpc-tests/feature_issuance.py b/qa/rpc-tests/feature_issuance.py index ea3284cfac3..8c7aeaf8073 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,7 +303,8 @@ 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"]) assert_equal(len(id_set), 4) @@ -311,5 +312,117 @@ def run_test(self): 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 () diff --git a/src/blind.cpp b/src/blind.cpp index ebc9cd2ed6a..ac4e5ff8865 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -265,10 +265,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); } @@ -395,8 +395,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) { diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index bf3b04efbec..8f139878ed0 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" }, @@ -142,6 +143,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtomainchain", 2, "subtractfeefromamount"}, { "getnewblockhex", 0, "required_age"}, { "rawissueasset", 1, "issuances"}, + { "rawreissueasset", 1, "reissuances"}, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index cb472a298a2..3a2b1c2f80e 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" diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index bf21a1f421e..4c4b064648b 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1635,10 +1635,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); @@ -1647,13 +1650,52 @@ 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; } } +// 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) @@ -1661,7 +1703,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" @@ -1771,6 +1813,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 // --------------------- ------------------------ ----------------------- ---------- @@ -1782,6 +1914,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