Merge #474: rawissueasset RPC call

ca383ac add functional testing for raw issuance call (Gregory Sanders)
a5e1af5 rawissueasset RPC call (Gregory Sanders)
f9f91ba Fixup issueasset help results information (Gregory Sanders)
69d8d15 Break out issuance-specific testing to its own functional test (Gregory Sanders)
2b22b6d Fixup (raw)blindrawtransaction (Gregory Sanders)
a5e9e0e Fix blinding logic for issuances on input 1 or higher (Gregory Sanders)
83df289 fixup BlindTransaction issuance counting assertion (Gregory Sanders)

Pull request description:

  Resolves the initial issuance portion of: https://github.com/ElementsProject/elements/issues/471

  The user follows this flow:
  `createrawtransaction->fundrawtransaction->rawissueasset->blindrawtransaction->signrawtransaction`

  `rawissueasset` is a new RPC call that allows the user to append N-many issuances, up to the amount of inputs the transaction has, at once. It allows the issuances to be "marked" for blinding later(only effects reissance token asset id), and for the destination outputs to be arbitrary addresses, blinded or otherwise.

  Previously `fundrawtransaction` would not blind issuances(pretty much on accident), so I have added an argument, default `true`, on whether to blind issuances, since there is no field in the transaction itself to mark as whether we want to blind it or not.

  Note: This will cause your transactions' fee-rate to go down, so the caller may want to over-shoot fees during the `fundrawtransaction` call as cushion.

  I also fixed a couple bugs on the way as I uncovered them.

Tree-SHA512: fa77158c390743f579797d426b9d9cb000a5d686a8b10aa818b1e9ad65fdc42ac139a36ad226e31676b3bd9c6643bc69074869d6a927309a7ec328c91e853bec
This commit is contained in:
Gregory Sanders 2018-12-12 11:12:20 -05:00
commit 71c69c3d4b
No known key found for this signature in database
GPG key ID: 6BE2CED14A9917BC
7 changed files with 574 additions and 126 deletions

View file

@ -107,6 +107,7 @@ testScripts = [
'feature_fedpeg.py',
'default_asset_name.py',
'assetdir.py',
'feature_issuance.py',
# Elements' specially adapted tests second
'blockchain.py',

View file

@ -22,9 +22,7 @@ class CTTest (BitcoinTestFramework):
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,9 +215,6 @@ class CTTest (BitcoinTestFramework):
# 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)
# Compare resulting fields with getrawtransaction
@ -228,7 +223,6 @@ class CTTest (BitcoinTestFramework):
assert_equal(issued["asset"], raw_details["vin"][issued["vin"]]["issuance"]["asset"])
assert_equal(issued["token"], raw_details["vin"][issued["vin"]]["issuance"]["token"])
testAssetHex = issued["asset"]
self.nodes[0].generate(1)
self.sync_all()
@ -275,46 +269,6 @@ class CTTest (BitcoinTestFramework):
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
@ -325,68 +279,13 @@ class CTTest (BitcoinTestFramework):
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)

315
qa/rpc-tests/feature_issuance.py Executable file
View file

@ -0,0 +1,315 @@
#!/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 "
# 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):
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)
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 ()

View file

@ -174,12 +174,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<uint256 >& input_blinding_factors, const std::vector<uint256 >& input_asset_blinding_factors, const std::vector<CAsset >& input_assets, const std::vector<CAmount >& input_amounts, std::vector<uint256 >& output_blinding_factors, std::vector<uint256 >& output_asset_blinding_factors, const std::vector<CPubKey>& output_pubkeys, const std::vector<CKey>& vBlindIssuanceAsset, const std::vector<CKey>& vBlindIssuanceToken, CMutableTransaction& tx, std::vector<std::vector<unsigned char> >* 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();
@ -250,7 +266,7 @@ int BlindTransaction(std::vector<uint256 >& 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 {

View file

@ -141,6 +141,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "testproposedblock", 1, "acceptnonstd" },
{ "sendtomainchain", 2, "subtractfeefromamount"},
{ "getnewblockhex", 0, "required_age"},
{ "rawissueasset", 1, "issuances"},
// Echo with conversion (For testing only)
{ "echojson", 0, "arg0" },
{ "echojson", 1, "arg1" },

View file

@ -712,14 +712,16 @@ void FillBlinds(CMutableTransaction& tx, bool fUseWallet, std::vector<uint256>&
CScript blindingScript = CScript() << OP_RETURN << std::vector<unsigned char>(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<CKey>& 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<unsigned char>& vchRangeproof = (nPseudo == 0) ? txinwit.vchIssuanceAmountRangeproof : txinwit.vchInflationKeysRangeproof;
std::vector<unsigned char>& 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<uint256>&
// 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<unsigned char> 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<uint256> input_blinds;
std::vector<uint256> input_asset_blinds;
std::vector<CAsset> input_assets;
@ -976,6 +986,7 @@ UniValue blindrawtransaction(const JSONRPCRequest& request)
std::vector<uint256> output_asset_blinds;
std::vector<CAsset> output_assets;
std::vector<CPubKey> output_pubkeys;
std::vector<CKey> 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<CKey> asset_keys;
std::vector<CKey> 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<CKey>(), std::vector<CKey>(), 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,6 +1579,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<unsigned char>(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<unsigned char>(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\":<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\":\"<entropy>\" (string) Entropy of the asset type.\n"
" \"asset\":\"<asset>\", (string) Asset type for issuance if known.\n"
" \"token\":\"<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
// --------------------- ------------------------ ----------------------- ----------
@ -1566,8 +1781,9 @@ 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, {}},
{ "rawtransactions", "blindrawtransaction", &blindrawtransaction, true, {"hexstring", "ignoreblindfail", "asset_commitments", "blind_issuances", "totalblinder"}},
#endif
{ "blockchain", "gettxoutproof", &gettxoutproof, true, {"txids", "blockhash"} },
{ "blockchain", "verifytxoutproof", &verifytxoutproof, true, {"proof"} },

View file

@ -3802,8 +3802,8 @@ UniValue issueasset(const JSONRPCRequest& request)
"{ (json object)\n"
" \"txid\":\"<txid>\", (string) Transaction id for issuance.\n"
" \"vin\":\"n\", (numeric) The input position of the issuance in the transaction.\n"
" \"entropy\":\"<entropy>\" (string) Entropy of the asset type.\n"
" \"asset\":\"<asset>\", (string) Asset type for issuance if known.\n"
" \"entropy\":\"<entropy>\", (string) Entropy of the asset type.\n"
" \"asset\":\"<asset>\", (string) Asset type for issuance.\n"
" \"token\":\"<token>\", (string) Token type for issuance.\n"
"}\n"
"\nExamples:\n"