Auto merge of #3888 - Eirik0:3873-sapling-migration, r=Eirik0
Sapling migration RPC Closes #3873
This commit is contained in:
commit
20821614a8
|
@ -71,6 +71,7 @@ testScripts=(
|
||||||
'p2p_node_bloom.py'
|
'p2p_node_bloom.py'
|
||||||
'regtest_signrawtransaction.py'
|
'regtest_signrawtransaction.py'
|
||||||
'finalsaplingroot.py'
|
'finalsaplingroot.py'
|
||||||
|
'sprout_sapling_migration.py'
|
||||||
'turnstile.py'
|
'turnstile.py'
|
||||||
);
|
);
|
||||||
testScriptsExt=(
|
testScriptsExt=(
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) 2019 The Zcash developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import sys; assert sys.version_info < (3,), ur"This script does not run under Python 3. Please use Python 2.7.x."
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import assert_equal, assert_true, get_coinbase_address, \
|
||||||
|
initialize_chain_clean, start_nodes, wait_and_assert_operationid_status, \
|
||||||
|
wait_and_assert_operationid_status_result
|
||||||
|
|
||||||
|
|
||||||
|
class SproutSaplingMigration(BitcoinTestFramework):
|
||||||
|
def setup_nodes(self):
|
||||||
|
return start_nodes(4, self.options.tmpdir, [[
|
||||||
|
'-nuparams=5ba81b19:100', # Overwinter
|
||||||
|
'-nuparams=76b809bb:100', # Sapling
|
||||||
|
]] * 4)
|
||||||
|
|
||||||
|
def setup_chain(self):
|
||||||
|
print("Initializing test directory " + self.options.tmpdir)
|
||||||
|
initialize_chain_clean(self.options.tmpdir, 4)
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
print "Mining blocks..."
|
||||||
|
self.nodes[0].generate(101)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# Send some ZEC to a Sprout address
|
||||||
|
tAddr = get_coinbase_address(self.nodes[0])
|
||||||
|
sproutAddr = self.nodes[0].z_getnewaddress('sprout')
|
||||||
|
saplingAddr = self.nodes[0].z_getnewaddress('sapling')
|
||||||
|
|
||||||
|
opid = self.nodes[0].z_sendmany(tAddr, [{"address": sproutAddr, "amount": Decimal('10')}], 1, 0)
|
||||||
|
wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||||
|
self.nodes[0].generate(1)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('10'))
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('0'))
|
||||||
|
|
||||||
|
# Migrate
|
||||||
|
self.nodes[0].z_setmigration(True)
|
||||||
|
print "Mining to block 494..."
|
||||||
|
self.nodes[0].generate(392) # 102 -> 494
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# At 494 we should have no async operations
|
||||||
|
assert_equal(0, len(self.nodes[0].z_getoperationstatus()), "num async operations at 494")
|
||||||
|
|
||||||
|
self.nodes[0].generate(1)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# At 495 we should have an async operation
|
||||||
|
operationstatus = self.nodes[0].z_getoperationstatus()
|
||||||
|
print "migration operation: {}".format(operationstatus)
|
||||||
|
assert_equal(1, len(operationstatus), "num async operations at 495")
|
||||||
|
assert_equal('saplingmigration', operationstatus[0]['method'])
|
||||||
|
assert_equal(500, operationstatus[0]['target_height'])
|
||||||
|
|
||||||
|
result = wait_and_assert_operationid_status_result(self.nodes[0], operationstatus[0]['id'])
|
||||||
|
print "result: {}".format(result)
|
||||||
|
assert_equal('saplingmigration', result['method'])
|
||||||
|
assert_equal(500, result['target_height'])
|
||||||
|
assert_equal(1, result['result']['num_tx_created'])
|
||||||
|
|
||||||
|
assert_equal(0, len(self.nodes[0].getrawmempool()), "mempool size at 495")
|
||||||
|
|
||||||
|
self.nodes[0].generate(3)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# At 498 the mempool will be empty and no funds will have moved
|
||||||
|
assert_equal(0, len(self.nodes[0].getrawmempool()), "mempool size at 498")
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('10'))
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('0'))
|
||||||
|
|
||||||
|
self.nodes[0].generate(1)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# At 499 there will be a transaction in the mempool and the note will be locked
|
||||||
|
assert_equal(1, len(self.nodes[0].getrawmempool()), "mempool size at 499")
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('0'))
|
||||||
|
assert_equal(self.nodes[0].z_getbalance(saplingAddr), Decimal('0'))
|
||||||
|
assert_true(self.nodes[0].z_getbalance(saplingAddr, 0) > Decimal('0'), "Unconfirmed sapling")
|
||||||
|
|
||||||
|
self.nodes[0].generate(1)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
# At 500 funds will have moved
|
||||||
|
sprout_balance = self.nodes[0].z_getbalance(sproutAddr)
|
||||||
|
sapling_balance = self.nodes[0].z_getbalance(saplingAddr)
|
||||||
|
print "sprout balance: {}, sapling balance: {}".format(sprout_balance, sapling_balance)
|
||||||
|
assert_true(sprout_balance < Decimal('10'), "Should have less Sprout funds")
|
||||||
|
assert_true(sapling_balance > Decimal('0'), "Should have more Sapling funds")
|
||||||
|
assert_true(sprout_balance + sapling_balance, Decimal('9.9999'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
SproutSaplingMigration().main()
|
|
@ -385,8 +385,9 @@ def assert_raises(exc, fun, *args, **kwds):
|
||||||
def fail(message=""):
|
def fail(message=""):
|
||||||
raise AssertionError(message)
|
raise AssertionError(message)
|
||||||
|
|
||||||
# Returns txid if operation was a success or None
|
|
||||||
def wait_and_assert_operationid_status(node, myopid, in_status='success', in_errormsg=None, timeout=300):
|
# Returns an async operation result
|
||||||
|
def wait_and_assert_operationid_status_result(node, myopid, in_status='success', in_errormsg=None, timeout=300):
|
||||||
print('waiting for async operation {}'.format(myopid))
|
print('waiting for async operation {}'.format(myopid))
|
||||||
result = None
|
result = None
|
||||||
for _ in xrange(1, timeout):
|
for _ in xrange(1, timeout):
|
||||||
|
@ -399,26 +400,29 @@ def wait_and_assert_operationid_status(node, myopid, in_status='success', in_err
|
||||||
assert_true(result is not None, "timeout occured")
|
assert_true(result is not None, "timeout occured")
|
||||||
status = result['status']
|
status = result['status']
|
||||||
|
|
||||||
txid = None
|
debug = os.getenv("PYTHON_DEBUG", "")
|
||||||
|
if debug:
|
||||||
|
print('...returned status: {}'.format(status))
|
||||||
|
|
||||||
errormsg = None
|
errormsg = None
|
||||||
if status == "failed":
|
if status == "failed":
|
||||||
errormsg = result['error']['message']
|
errormsg = result['error']['message']
|
||||||
elif status == "success":
|
if debug:
|
||||||
txid = result['result']['txid']
|
|
||||||
|
|
||||||
if os.getenv("PYTHON_DEBUG", ""):
|
|
||||||
print('...returned status: {}'.format(status))
|
|
||||||
if errormsg is not None:
|
|
||||||
print('...returned error: {}'.format(errormsg))
|
print('...returned error: {}'.format(errormsg))
|
||||||
|
assert_equal(in_errormsg, errormsg)
|
||||||
|
|
||||||
assert_equal(in_status, status, "Operation returned mismatched status. Error Message: {}".format(errormsg))
|
assert_equal(in_status, status, "Operation returned mismatched status. Error Message: {}".format(errormsg))
|
||||||
|
|
||||||
if errormsg is not None:
|
return result
|
||||||
assert_true(in_errormsg is not None, "No error retured. Expected: {}".format(errormsg))
|
|
||||||
assert_true(in_errormsg in errormsg, "Error returned: {}. Error expected: {}".format(errormsg, in_errormsg))
|
|
||||||
return result # if there was an error return the result
|
# Returns txid if operation was a success or None
|
||||||
|
def wait_and_assert_operationid_status(node, myopid, in_status='success', in_errormsg=None, timeout=300):
|
||||||
|
result = wait_and_assert_operationid_status_result(node, myopid, in_status, in_errormsg, timeout)
|
||||||
|
if result['status'] == "success":
|
||||||
|
return result['result']['txid']
|
||||||
else:
|
else:
|
||||||
return txid # otherwise return the txid
|
return None
|
||||||
|
|
||||||
# Find a coinbase address on the node, filtering by the number of UTXOs it has.
|
# Find a coinbase address on the node, filtering by the number of UTXOs it has.
|
||||||
# If no filter is provided, returns the coinbase address on the node containing
|
# If no filter is provided, returns the coinbase address on the node containing
|
||||||
|
|
|
@ -10,7 +10,7 @@ from test_framework.authproxy import JSONRPCException
|
||||||
from test_framework.mininode import COIN
|
from test_framework.mininode import COIN
|
||||||
from test_framework.util import assert_equal, initialize_chain_clean, \
|
from test_framework.util import assert_equal, initialize_chain_clean, \
|
||||||
start_nodes, connect_nodes_bi, wait_and_assert_operationid_status, \
|
start_nodes, connect_nodes_bi, wait_and_assert_operationid_status, \
|
||||||
get_coinbase_address
|
wait_and_assert_operationid_status_result, get_coinbase_address
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import timeit
|
import timeit
|
||||||
|
@ -84,7 +84,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
||||||
recipients= [{"address":myzaddr, "amount": Decimal('1')}]
|
recipients= [{"address":myzaddr, "amount": Decimal('1')}]
|
||||||
myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
|
myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
|
||||||
|
|
||||||
wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "no UTXOs found for taddr from address", 10)
|
wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "Insufficient funds, no UTXOs found for taddr from address.", 10)
|
||||||
|
|
||||||
# This send will fail because our wallet does not allow any change when protecting a coinbase utxo,
|
# This send will fail because our wallet does not allow any change when protecting a coinbase utxo,
|
||||||
# as it's currently not possible to specify a change address in z_sendmany.
|
# as it's currently not possible to specify a change address in z_sendmany.
|
||||||
|
@ -92,7 +92,9 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
||||||
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
|
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
|
||||||
|
|
||||||
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
|
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
|
||||||
error_result = wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "wallet does not allow any change", 10)
|
error_result = wait_and_assert_operationid_status_result(self.nodes[0], myopid, "failed", ("Change 8.76533211 not allowed. "
|
||||||
|
"When shielding coinbase funds, the wallet does not allow any change "
|
||||||
|
"as there is currently no way to specify a change address in z_sendmany."), 10)
|
||||||
|
|
||||||
# Test that the returned status object contains a params field with the operation's input parameters
|
# Test that the returned status object contains a params field with the operation's input parameters
|
||||||
assert_equal(error_result["method"], "z_sendmany")
|
assert_equal(error_result["method"], "z_sendmany")
|
||||||
|
|
|
@ -217,6 +217,7 @@ BITCOIN_CORE_H = \
|
||||||
validationinterface.h \
|
validationinterface.h \
|
||||||
version.h \
|
version.h \
|
||||||
wallet/asyncrpcoperation_mergetoaddress.h \
|
wallet/asyncrpcoperation_mergetoaddress.h \
|
||||||
|
wallet/asyncrpcoperation_saplingmigration.h \
|
||||||
wallet/asyncrpcoperation_sendmany.h \
|
wallet/asyncrpcoperation_sendmany.h \
|
||||||
wallet/asyncrpcoperation_shieldcoinbase.h \
|
wallet/asyncrpcoperation_shieldcoinbase.h \
|
||||||
wallet/crypter.h \
|
wallet/crypter.h \
|
||||||
|
@ -306,6 +307,7 @@ libbitcoin_wallet_a_SOURCES = \
|
||||||
zcbenchmarks.cpp \
|
zcbenchmarks.cpp \
|
||||||
zcbenchmarks.h \
|
zcbenchmarks.h \
|
||||||
wallet/asyncrpcoperation_mergetoaddress.cpp \
|
wallet/asyncrpcoperation_mergetoaddress.cpp \
|
||||||
|
wallet/asyncrpcoperation_saplingmigration.cpp \
|
||||||
wallet/asyncrpcoperation_sendmany.cpp \
|
wallet/asyncrpcoperation_sendmany.cpp \
|
||||||
wallet/asyncrpcoperation_shieldcoinbase.cpp \
|
wallet/asyncrpcoperation_shieldcoinbase.cpp \
|
||||||
wallet/crypter.cpp \
|
wallet/crypter.cpp \
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
#include "assert.h"
|
||||||
|
#include "boost/variant/static_visitor.hpp"
|
||||||
|
#include "asyncrpcoperation_saplingmigration.h"
|
||||||
|
#include "init.h"
|
||||||
|
#include "rpc/protocol.h"
|
||||||
|
#include "random.h"
|
||||||
|
#include "sync.h"
|
||||||
|
#include "tinyformat.h"
|
||||||
|
#include "transaction_builder.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "wallet.h"
|
||||||
|
|
||||||
|
const CAmount FEE = 10000;
|
||||||
|
|
||||||
|
AsyncRPCOperation_saplingmigration::AsyncRPCOperation_saplingmigration(int targetHeight) : targetHeight_(targetHeight) {}
|
||||||
|
|
||||||
|
AsyncRPCOperation_saplingmigration::~AsyncRPCOperation_saplingmigration() {}
|
||||||
|
|
||||||
|
void AsyncRPCOperation_saplingmigration::main() {
|
||||||
|
if (isCancelled())
|
||||||
|
return;
|
||||||
|
|
||||||
|
set_state(OperationStatus::EXECUTING);
|
||||||
|
start_execution_clock();
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
success = main_impl();
|
||||||
|
} catch (const UniValue& objError) {
|
||||||
|
int code = find_value(objError, "code").get_int();
|
||||||
|
std::string message = find_value(objError, "message").get_str();
|
||||||
|
set_error_code(code);
|
||||||
|
set_error_message(message);
|
||||||
|
} catch (const runtime_error& e) {
|
||||||
|
set_error_code(-1);
|
||||||
|
set_error_message("runtime error: " + string(e.what()));
|
||||||
|
} catch (const logic_error& e) {
|
||||||
|
set_error_code(-1);
|
||||||
|
set_error_message("logic error: " + string(e.what()));
|
||||||
|
} catch (const exception& e) {
|
||||||
|
set_error_code(-1);
|
||||||
|
set_error_message("general exception: " + string(e.what()));
|
||||||
|
} catch (...) {
|
||||||
|
set_error_code(-2);
|
||||||
|
set_error_message("unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_execution_clock();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
set_state(OperationStatus::SUCCESS);
|
||||||
|
} else {
|
||||||
|
set_state(OperationStatus::FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string s = strprintf("%s: Sprout->Sapling transactions created. (status=%s", getId(), getStateAsString());
|
||||||
|
if (success) {
|
||||||
|
s += strprintf(", success)\n");
|
||||||
|
} else {
|
||||||
|
s += strprintf(", error=%s)\n", getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
LogPrintf("%s", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncRPCOperation_saplingmigration::main_impl() {
|
||||||
|
std::vector<CSproutNotePlaintextEntry> sproutEntries;
|
||||||
|
std::vector<SaplingNoteEntry> saplingEntries;
|
||||||
|
{
|
||||||
|
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||||
|
// We set minDepth to 11 to avoid unconfirmed notes and in anticipation of specifying
|
||||||
|
// an anchor at height N-10 for each Sprout JoinSplit description
|
||||||
|
// Consider, should notes be sorted?
|
||||||
|
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, "", 11);
|
||||||
|
}
|
||||||
|
CAmount availableFunds = 0;
|
||||||
|
for (const CSproutNotePlaintextEntry& sproutEntry : sproutEntries) {
|
||||||
|
availableFunds = sproutEntry.plaintext.value();
|
||||||
|
}
|
||||||
|
// If the remaining amount to be migrated is less than 0.01 ZEC, end the migration.
|
||||||
|
if (availableFunds < CENT) {
|
||||||
|
setMigrationResult(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HDSeed seed;
|
||||||
|
if (!pwalletMain->GetHDSeed(seed)) {
|
||||||
|
throw JSONRPCError(
|
||||||
|
RPC_WALLET_ERROR,
|
||||||
|
"AsyncRPCOperation_AsyncRPCOperation_saplingmigration: HD seed not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
libzcash::SaplingPaymentAddress migrationDestAddress = getMigrationDestAddress(seed);
|
||||||
|
|
||||||
|
auto consensusParams = Params().GetConsensus();
|
||||||
|
|
||||||
|
// Up to the limit of 5, as many transactions are sent as are needed to migrate the remaining funds
|
||||||
|
int numTxCreated = 0;
|
||||||
|
int noteIndex = 0;
|
||||||
|
do {
|
||||||
|
CAmount amountToSend = chooseAmount(availableFunds);
|
||||||
|
auto builder = TransactionBuilder(consensusParams, targetHeight_, pwalletMain, pzcashParams);
|
||||||
|
std::vector<CSproutNotePlaintextEntry> fromNotes;
|
||||||
|
CAmount fromNoteAmount = 0;
|
||||||
|
while (fromNoteAmount < amountToSend) {
|
||||||
|
auto sproutEntry = sproutEntries[noteIndex++];
|
||||||
|
fromNotes.push_back(sproutEntry);
|
||||||
|
fromNoteAmount += sproutEntry.plaintext.value();
|
||||||
|
}
|
||||||
|
availableFunds -= fromNoteAmount;
|
||||||
|
for (const CSproutNotePlaintextEntry& sproutEntry : fromNotes) {
|
||||||
|
libzcash::SproutNote sproutNote = sproutEntry.plaintext.note(sproutEntry.address);
|
||||||
|
libzcash::SproutSpendingKey sproutSk;
|
||||||
|
pwalletMain->GetSproutSpendingKey(sproutEntry.address, sproutSk);
|
||||||
|
std::vector<JSOutPoint> vOutPoints = {sproutEntry.jsop};
|
||||||
|
// Each migration transaction SHOULD specify an anchor at height N-10
|
||||||
|
// for each Sprout JoinSplit description
|
||||||
|
// TODO: the above functionality (in comment) is not implemented in zcashd
|
||||||
|
uint256 inputAnchor;
|
||||||
|
std::vector<boost::optional<SproutWitness>> vInputWitnesses;
|
||||||
|
pwalletMain->GetSproutNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor);
|
||||||
|
builder.AddSproutInput(sproutSk, sproutNote, vInputWitnesses[0].get());
|
||||||
|
}
|
||||||
|
// The amount chosen *includes* the 0.0001 ZEC fee for this transaction, i.e.
|
||||||
|
// the value of the Sapling output will be 0.0001 ZEC less.
|
||||||
|
builder.SetFee(FEE);
|
||||||
|
builder.AddSaplingOutput(ovkForShieldingFromTaddr(seed), migrationDestAddress, amountToSend - FEE);
|
||||||
|
CTransaction tx = builder.Build().GetTxOrThrow();
|
||||||
|
if (isCancelled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pwalletMain->AddPendingSaplingMigrationTx(tx);
|
||||||
|
++numTxCreated;
|
||||||
|
} while (numTxCreated < 5 && availableFunds > CENT);
|
||||||
|
|
||||||
|
setMigrationResult(numTxCreated);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsyncRPCOperation_saplingmigration::setMigrationResult(int numTxCreated) {
|
||||||
|
UniValue res(UniValue::VOBJ);
|
||||||
|
res.push_back(Pair("num_tx_created", numTxCreated));
|
||||||
|
set_result(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
CAmount AsyncRPCOperation_saplingmigration::chooseAmount(const CAmount& availableFunds) {
|
||||||
|
CAmount amount = 0;
|
||||||
|
do {
|
||||||
|
// 1. Choose an integer exponent uniformly in the range 6 to 8 inclusive.
|
||||||
|
int exponent = GetRand(3) + 6;
|
||||||
|
// 2. Choose an integer mantissa uniformly in the range 1 to 99 inclusive.
|
||||||
|
uint64_t mantissa = GetRand(99) + 1;
|
||||||
|
// 3. Calculate amount := (mantissa * 10^exponent) zatoshi.
|
||||||
|
int pow = std::pow(10, exponent);
|
||||||
|
amount = mantissa * pow;
|
||||||
|
// 4. If amount is greater than the amount remaining to send, repeat from step 1.
|
||||||
|
} while (amount > availableFunds);
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unless otherwise specified, the migration destination address is the address for Sapling account 0
|
||||||
|
libzcash::SaplingPaymentAddress AsyncRPCOperation_saplingmigration::getMigrationDestAddress(const HDSeed& seed) {
|
||||||
|
// Derive the address for Sapling account 0
|
||||||
|
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
|
||||||
|
uint32_t bip44CoinType = Params().BIP44CoinType();
|
||||||
|
|
||||||
|
// We use a fixed keypath scheme of m/32'/coin_type'/account'
|
||||||
|
// Derive m/32'
|
||||||
|
auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT);
|
||||||
|
// Derive m/32'/coin_type'
|
||||||
|
auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT);
|
||||||
|
|
||||||
|
// Derive m/32'/coin_type'/0'
|
||||||
|
libzcash::SaplingExtendedSpendingKey xsk = m_32h_cth.Derive(0 | ZIP32_HARDENED_KEY_LIMIT);
|
||||||
|
|
||||||
|
libzcash::SaplingPaymentAddress toAddress = xsk.DefaultAddress();
|
||||||
|
|
||||||
|
// Refactor: this is similar logic as in the visitor HaveSpendingKeyForPaymentAddress and is used elsewhere
|
||||||
|
libzcash::SaplingIncomingViewingKey ivk;
|
||||||
|
libzcash::SaplingFullViewingKey fvk;
|
||||||
|
if (!(pwalletMain->GetSaplingIncomingViewingKey(toAddress, ivk) &&
|
||||||
|
pwalletMain->GetSaplingFullViewingKey(ivk, fvk) &&
|
||||||
|
pwalletMain->HaveSaplingSpendingKey(fvk))) {
|
||||||
|
// Sapling account 0 must be the first address returned by GenerateNewSaplingZKey
|
||||||
|
assert(pwalletMain->GenerateNewSaplingZKey() == toAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
UniValue AsyncRPCOperation_saplingmigration::getStatus() const {
|
||||||
|
UniValue v = AsyncRPCOperation::getStatus();
|
||||||
|
UniValue obj = v.get_obj();
|
||||||
|
obj.push_back(Pair("method", "saplingmigration"));
|
||||||
|
obj.push_back(Pair("target_height", targetHeight_));
|
||||||
|
return obj;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include "amount.h"
|
||||||
|
#include "asyncrpcoperation.h"
|
||||||
|
#include "univalue.h"
|
||||||
|
#include "zcash/Address.hpp"
|
||||||
|
#include "zcash/zip32.h"
|
||||||
|
|
||||||
|
class AsyncRPCOperation_saplingmigration : public AsyncRPCOperation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AsyncRPCOperation_saplingmigration(int targetHeight);
|
||||||
|
virtual ~AsyncRPCOperation_saplingmigration();
|
||||||
|
|
||||||
|
// We don't want to be copied or moved around
|
||||||
|
AsyncRPCOperation_saplingmigration(AsyncRPCOperation_saplingmigration const&) = delete; // Copy construct
|
||||||
|
AsyncRPCOperation_saplingmigration(AsyncRPCOperation_saplingmigration&&) = delete; // Move construct
|
||||||
|
AsyncRPCOperation_saplingmigration& operator=(AsyncRPCOperation_saplingmigration const&) = delete; // Copy assign
|
||||||
|
AsyncRPCOperation_saplingmigration& operator=(AsyncRPCOperation_saplingmigration&&) = delete; // Move assign
|
||||||
|
|
||||||
|
virtual void main();
|
||||||
|
|
||||||
|
virtual UniValue getStatus() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int targetHeight_;
|
||||||
|
|
||||||
|
bool main_impl();
|
||||||
|
|
||||||
|
void setMigrationResult(int numTxCreated);
|
||||||
|
|
||||||
|
CAmount chooseAmount(const CAmount& availableFunds);
|
||||||
|
|
||||||
|
libzcash::SaplingPaymentAddress getMigrationDestAddress(const HDSeed& seed);
|
||||||
|
};
|
|
@ -3906,6 +3906,28 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
||||||
return operationId;
|
return operationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UniValue z_setmigration(const UniValue& params, bool fHelp) {
|
||||||
|
if (!EnsureWalletIsAvailable(fHelp))
|
||||||
|
return NullUniValue;
|
||||||
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
|
"z_setmigration enabled\n"
|
||||||
|
"When enabled the Sprout to Sapling migration will attempt to migrate all funds from this wallet’s\n"
|
||||||
|
"Sprout addresses to either the address for Sapling account 0 or the address specified by the parameter\n"
|
||||||
|
"'-migrationdestaddress'.\n"
|
||||||
|
"\n"
|
||||||
|
"This migration is designed to minimize information leakage. As a result for wallets with a significant\n"
|
||||||
|
"Sprout balance, this process may take several weeks. The migration works by sending, up to 5, as many\n"
|
||||||
|
"transactions as possible whenever the blockchain reaches a height equal to 499 modulo 500. The transaction\n"
|
||||||
|
"amounts are picked according to the random distribution specified in ZIP 308. The migration will end once\n"
|
||||||
|
"the wallet’s Sprout balance is below" + strprintf("%s %s", FormatMoney(CENT), CURRENCY_UNIT) + ".\n"
|
||||||
|
"\nArguments:\n"
|
||||||
|
"1. enabled (boolean, required) 'true' or 'false' to enable or disable respectively.\n"
|
||||||
|
);
|
||||||
|
LOCK(pwalletMain->cs_wallet);
|
||||||
|
pwalletMain->fSaplingMigrationEnabled = params[0].get_bool();
|
||||||
|
return NullUniValue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
When estimating the number of coinbase utxos we can shield in a single transaction:
|
When estimating the number of coinbase utxos we can shield in a single transaction:
|
||||||
|
@ -4660,6 +4682,7 @@ static const CRPCCommand commands[] =
|
||||||
{ "wallet", "z_gettotalbalance", &z_gettotalbalance, false },
|
{ "wallet", "z_gettotalbalance", &z_gettotalbalance, false },
|
||||||
{ "wallet", "z_mergetoaddress", &z_mergetoaddress, false },
|
{ "wallet", "z_mergetoaddress", &z_mergetoaddress, false },
|
||||||
{ "wallet", "z_sendmany", &z_sendmany, false },
|
{ "wallet", "z_sendmany", &z_sendmany, false },
|
||||||
|
{ "wallet", "z_setmigration", &z_setmigration, false },
|
||||||
{ "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false },
|
{ "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false },
|
||||||
{ "wallet", "z_getoperationstatus", &z_getoperationstatus, true },
|
{ "wallet", "z_getoperationstatus", &z_getoperationstatus, true },
|
||||||
{ "wallet", "z_getoperationresult", &z_getoperationresult, true },
|
{ "wallet", "z_getoperationresult", &z_getoperationresult, true },
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
|
|
||||||
#include "wallet/wallet.h"
|
#include "wallet/wallet.h"
|
||||||
|
|
||||||
|
#include "asyncrpcqueue.h"
|
||||||
#include "checkpoints.h"
|
#include "checkpoints.h"
|
||||||
#include "coincontrol.h"
|
#include "coincontrol.h"
|
||||||
|
#include "core_io.h"
|
||||||
#include "consensus/upgrades.h"
|
#include "consensus/upgrades.h"
|
||||||
#include "consensus/validation.h"
|
#include "consensus/validation.h"
|
||||||
#include "consensus/consensus.h"
|
#include "consensus/consensus.h"
|
||||||
|
@ -15,12 +17,14 @@
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "net.h"
|
#include "net.h"
|
||||||
#include "rpc/protocol.h"
|
#include "rpc/protocol.h"
|
||||||
|
#include "rpc/server.h"
|
||||||
#include "script/script.h"
|
#include "script/script.h"
|
||||||
#include "script/sign.h"
|
#include "script/sign.h"
|
||||||
#include "timedata.h"
|
#include "timedata.h"
|
||||||
#include "utilmoneystr.h"
|
#include "utilmoneystr.h"
|
||||||
#include "zcash/Note.hpp"
|
#include "zcash/Note.hpp"
|
||||||
#include "crypter.h"
|
#include "crypter.h"
|
||||||
|
#include "wallet/asyncrpcoperation_saplingmigration.h"
|
||||||
#include "zcash/zip32.h"
|
#include "zcash/zip32.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
@ -32,6 +36,8 @@
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace libzcash;
|
using namespace libzcash;
|
||||||
|
|
||||||
|
extern UniValue sendrawtransaction(const UniValue& params, bool fHelp);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings
|
* Settings
|
||||||
*/
|
*/
|
||||||
|
@ -558,6 +564,15 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CWallet::ChainTipAdded(const CBlockIndex *pindex,
|
||||||
|
const CBlock *pblock,
|
||||||
|
SproutMerkleTree sproutTree,
|
||||||
|
SaplingMerkleTree saplingTree)
|
||||||
|
{
|
||||||
|
IncrementNoteWitnesses(pindex, pblock, sproutTree, saplingTree);
|
||||||
|
UpdateSaplingNullifierNoteMapForBlock(pblock);
|
||||||
|
}
|
||||||
|
|
||||||
void CWallet::ChainTip(const CBlockIndex *pindex,
|
void CWallet::ChainTip(const CBlockIndex *pindex,
|
||||||
const CBlock *pblock,
|
const CBlock *pblock,
|
||||||
SproutMerkleTree sproutTree,
|
SproutMerkleTree sproutTree,
|
||||||
|
@ -565,11 +580,60 @@ void CWallet::ChainTip(const CBlockIndex *pindex,
|
||||||
bool added)
|
bool added)
|
||||||
{
|
{
|
||||||
if (added) {
|
if (added) {
|
||||||
IncrementNoteWitnesses(pindex, pblock, sproutTree, saplingTree);
|
ChainTipAdded(pindex, pblock, sproutTree, saplingTree);
|
||||||
|
RunSaplingMigration(pindex->nHeight);
|
||||||
} else {
|
} else {
|
||||||
DecrementNoteWitnesses(pindex);
|
DecrementNoteWitnesses(pindex);
|
||||||
}
|
|
||||||
UpdateSaplingNullifierNoteMapForBlock(pblock);
|
UpdateSaplingNullifierNoteMapForBlock(pblock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWallet::RunSaplingMigration(int blockHeight) {
|
||||||
|
if (!NetworkUpgradeActive(blockHeight, Params().GetConsensus(), Consensus::UPGRADE_SAPLING)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOCK(cs_wallet);
|
||||||
|
if (!fSaplingMigrationEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The migration transactions to be sent in a particular batch can take
|
||||||
|
// significant time to generate, and this time depends on the speed of the user's
|
||||||
|
// computer. If they were generated only after a block is seen at the target
|
||||||
|
// height minus 1, then this could leak information. Therefore, for target
|
||||||
|
// height N, implementations SHOULD start generating the transactions at around
|
||||||
|
// height N-5
|
||||||
|
if (blockHeight % 500 == 495) {
|
||||||
|
if (saplingMigrationOperation != nullptr) {
|
||||||
|
saplingMigrationOperation->cancel();
|
||||||
|
}
|
||||||
|
pendingSaplingMigrationTxs.clear();
|
||||||
|
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||||
|
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_saplingmigration(blockHeight + 5));
|
||||||
|
saplingMigrationOperation = operation;
|
||||||
|
q->addOperation(operation);
|
||||||
|
} else if (blockHeight % 500 == 499) {
|
||||||
|
if (saplingMigrationOperation != nullptr) {
|
||||||
|
saplingMigrationOperation->cancel();
|
||||||
|
}
|
||||||
|
for (const CTransaction& transaction : pendingSaplingMigrationTxs) {
|
||||||
|
// The following is taken from z_sendmany/z_mergetoaddress
|
||||||
|
// Send the transaction
|
||||||
|
// TODO: Use CWallet::CommitTransaction instead of sendrawtransaction
|
||||||
|
auto signedtxn = EncodeHexTx(transaction);
|
||||||
|
UniValue params = UniValue(UniValue::VARR);
|
||||||
|
params.push_back(signedtxn);
|
||||||
|
UniValue sendResultValue = sendrawtransaction(params, false);
|
||||||
|
if (sendResultValue.isNull()) {
|
||||||
|
throw JSONRPCError(RPC_WALLET_ERROR, "sendrawtransaction did not return an error or a txid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingSaplingMigrationTxs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CWallet::AddPendingSaplingMigrationTx(const CTransaction& tx) {
|
||||||
|
LOCK(cs_wallet);
|
||||||
|
pendingSaplingMigrationTxs.push_back(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CWallet::SetBestChain(const CBlockLocator& loc)
|
void CWallet::SetBestChain(const CBlockLocator& loc)
|
||||||
|
@ -2450,7 +2514,7 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Increment note witness caches
|
// Increment note witness caches
|
||||||
ChainTip(pindex, &block, sproutTree, saplingTree, true);
|
ChainTipAdded(pindex, &block, sproutTree, saplingTree);
|
||||||
|
|
||||||
pindex = chainActive.Next(pindex);
|
pindex = chainActive.Next(pindex);
|
||||||
if (GetTime() >= nNow + 60) {
|
if (GetTime() >= nNow + 60) {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#define BITCOIN_WALLET_WALLET_H
|
#define BITCOIN_WALLET_WALLET_H
|
||||||
|
|
||||||
#include "amount.h"
|
#include "amount.h"
|
||||||
|
#include "asyncrpcoperation.h"
|
||||||
#include "coins.h"
|
#include "coins.h"
|
||||||
#include "key.h"
|
#include "key.h"
|
||||||
#include "keystore.h"
|
#include "keystore.h"
|
||||||
|
@ -755,6 +756,9 @@ private:
|
||||||
TxNullifiers mapTxSproutNullifiers;
|
TxNullifiers mapTxSproutNullifiers;
|
||||||
TxNullifiers mapTxSaplingNullifiers;
|
TxNullifiers mapTxSaplingNullifiers;
|
||||||
|
|
||||||
|
std::vector<CTransaction> pendingSaplingMigrationTxs;
|
||||||
|
std::shared_ptr<AsyncRPCOperation> saplingMigrationOperation = nullptr;
|
||||||
|
|
||||||
void AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid);
|
void AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid);
|
||||||
void AddToSproutSpends(const uint256& nullifier, const uint256& wtxid);
|
void AddToSproutSpends(const uint256& nullifier, const uint256& wtxid);
|
||||||
void AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid);
|
void AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid);
|
||||||
|
@ -767,6 +771,7 @@ public:
|
||||||
* incremental witness cache in any transaction in mapWallet.
|
* incremental witness cache in any transaction in mapWallet.
|
||||||
*/
|
*/
|
||||||
int64_t nWitnessCacheSize;
|
int64_t nWitnessCacheSize;
|
||||||
|
bool fSaplingMigrationEnabled = false;
|
||||||
|
|
||||||
void ClearNoteWitnessCache();
|
void ClearNoteWitnessCache();
|
||||||
|
|
||||||
|
@ -832,6 +837,7 @@ protected:
|
||||||
private:
|
private:
|
||||||
template <class T>
|
template <class T>
|
||||||
void SyncMetaData(std::pair<typename TxSpendMap<T>::iterator, typename TxSpendMap<T>::iterator>);
|
void SyncMetaData(std::pair<typename TxSpendMap<T>::iterator, typename TxSpendMap<T>::iterator>);
|
||||||
|
void ChainTipAdded(const CBlockIndex *pindex, const CBlock *pblock, SproutMerkleTree sproutTree, SaplingMerkleTree saplingTree);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx);
|
bool UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx);
|
||||||
|
@ -1183,6 +1189,8 @@ public:
|
||||||
CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const;
|
CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const;
|
||||||
CAmount GetChange(const CTransaction& tx) const;
|
CAmount GetChange(const CTransaction& tx) const;
|
||||||
void ChainTip(const CBlockIndex *pindex, const CBlock *pblock, SproutMerkleTree sproutTree, SaplingMerkleTree saplingTree, bool added);
|
void ChainTip(const CBlockIndex *pindex, const CBlock *pblock, SproutMerkleTree sproutTree, SaplingMerkleTree saplingTree, bool added);
|
||||||
|
void RunSaplingMigration(int blockHeight);
|
||||||
|
void AddPendingSaplingMigrationTx(const CTransaction& tx);
|
||||||
/** Saves witness caches and best block locator to disk. */
|
/** Saves witness caches and best block locator to disk. */
|
||||||
void SetBestChain(const CBlockLocator& loc);
|
void SetBestChain(const CBlockLocator& loc);
|
||||||
std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses);
|
std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses);
|
||||||
|
|
Loading…
Reference in New Issue