|
|
@ -430,6 +430,8 @@ bool AsyncRPCOperation_mergetoaddress::main_impl() |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
UniValue obj(UniValue::VOBJ); |
|
|
|
|
|
|
|
/**
|
|
|
|
* SCENARIO #1 |
|
|
|
* |
|
|
@ -438,7 +440,6 @@ bool AsyncRPCOperation_mergetoaddress::main_impl() |
|
|
|
* There are no zaddrs or joinsplits involved. |
|
|
|
*/ |
|
|
|
if (isPureTaddrOnlyTx) { |
|
|
|
UniValue obj(UniValue::VOBJ); |
|
|
|
obj.push_back(Pair("rawtxn", EncodeHexTx(tx_))); |
|
|
|
sign_send_raw_transaction(obj); |
|
|
|
return true; |
|
|
@ -447,335 +448,6 @@ bool AsyncRPCOperation_mergetoaddress::main_impl() |
|
|
|
* END SCENARIO #1 |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
// Prepare raw transaction to handle JoinSplits
|
|
|
|
CMutableTransaction mtx(tx_); |
|
|
|
crypto_sign_keypair(joinSplitPubKey_.begin(), joinSplitPrivKey_); |
|
|
|
mtx.joinSplitPubKey = joinSplitPubKey_; |
|
|
|
tx_ = CTransaction(mtx); |
|
|
|
std::string hexMemo = std::get<1>(recipient_); |
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* SCENARIO #2 |
|
|
|
* |
|
|
|
* taddrs -> zaddr |
|
|
|
* |
|
|
|
* We only need a single JoinSplit. |
|
|
|
*/ |
|
|
|
if (sproutNoteInputs_.empty() && isToZaddr_) { |
|
|
|
// Create JoinSplit to target z-addr.
|
|
|
|
MergeToAddressJSInfo info; |
|
|
|
info.vpub_old = sendAmount; |
|
|
|
info.vpub_new = 0; |
|
|
|
|
|
|
|
JSOutput jso = JSOutput(boost::get<libzcash::SproutPaymentAddress>(toPaymentAddress_), sendAmount); |
|
|
|
if (hexMemo.size() > 0) { |
|
|
|
jso.memo = get_memo_from_hex_string(hexMemo); |
|
|
|
} |
|
|
|
info.vjsout.push_back(jso); |
|
|
|
|
|
|
|
UniValue obj(UniValue::VOBJ); |
|
|
|
obj = perform_joinsplit(info); |
|
|
|
sign_send_raw_transaction(obj); |
|
|
|
return true; |
|
|
|
} |
|
|
|
/**
|
|
|
|
* END SCENARIO #2 |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
// Copy zinputs to more flexible containers
|
|
|
|
std::deque<MergeToAddressInputSproutNote> zInputsDeque; |
|
|
|
for (const auto& o : sproutNoteInputs_) { |
|
|
|
zInputsDeque.push_back(o); |
|
|
|
} |
|
|
|
|
|
|
|
// When spending notes, take a snapshot of note witnesses and anchors as the treestate will
|
|
|
|
// change upon arrival of new blocks which contain joinsplit transactions. This is likely
|
|
|
|
// to happen as creating a chained joinsplit transaction can take longer than the block interval.
|
|
|
|
{ |
|
|
|
LOCK2(cs_main, pwalletMain->cs_wallet); |
|
|
|
for (auto t : sproutNoteInputs_) { |
|
|
|
JSOutPoint jso = std::get<0>(t); |
|
|
|
std::vector<JSOutPoint> vOutPoints = {jso}; |
|
|
|
uint256 inputAnchor; |
|
|
|
std::vector<boost::optional<SproutWitness>> vInputWitnesses; |
|
|
|
pwalletMain->GetSproutNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor); |
|
|
|
jsopWitnessAnchorMap[jso.ToString()] = MergeToAddressWitnessAnchorData{vInputWitnesses[0], inputAnchor}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/**
|
|
|
|
* SCENARIO #3 |
|
|
|
* |
|
|
|
* zaddrs -> zaddr |
|
|
|
* taddrs -> |
|
|
|
* |
|
|
|
* zaddrs -> |
|
|
|
* taddrs -> taddr |
|
|
|
* |
|
|
|
* Send to zaddr by chaining JoinSplits together and immediately consuming any change |
|
|
|
* Send to taddr by creating dummy z outputs and accumulating value in a change note |
|
|
|
* which is used to set vpub_new in the last chained joinsplit. |
|
|
|
*/ |
|
|
|
UniValue obj(UniValue::VOBJ); |
|
|
|
CAmount jsChange = 0; // this is updated after each joinsplit
|
|
|
|
int changeOutputIndex = -1; // this is updated after each joinsplit if jsChange > 0
|
|
|
|
bool vpubOldProcessed = false; // updated when vpub_old for taddr inputs is set in first joinsplit
|
|
|
|
bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit
|
|
|
|
|
|
|
|
// At this point, we are guaranteed to have at least one input note.
|
|
|
|
// Use address of first input note as the temporary change address.
|
|
|
|
SproutSpendingKey changeKey = std::get<3>(zInputsDeque.front()); |
|
|
|
SproutPaymentAddress changeAddress = changeKey.address(); |
|
|
|
|
|
|
|
CAmount vpubOldTarget = 0; |
|
|
|
CAmount vpubNewTarget = 0; |
|
|
|
if (isToTaddr_) { |
|
|
|
vpubNewTarget = z_inputs_total; |
|
|
|
} else { |
|
|
|
if (utxoInputs_.empty()) { |
|
|
|
vpubNewTarget = minersFee; |
|
|
|
} else { |
|
|
|
vpubOldTarget = t_inputs_total - minersFee; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Keep track of treestate within this transaction
|
|
|
|
boost::unordered_map<uint256, SproutMerkleTree, CCoinsKeyHasher> intermediates; |
|
|
|
std::vector<uint256> previousCommitments; |
|
|
|
|
|
|
|
while (!vpubNewProcessed) { |
|
|
|
MergeToAddressJSInfo info; |
|
|
|
info.vpub_old = 0; |
|
|
|
info.vpub_new = 0; |
|
|
|
|
|
|
|
// Set vpub_old in the first joinsplit
|
|
|
|
if (!vpubOldProcessed) { |
|
|
|
if (t_inputs_total < vpubOldTarget) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, |
|
|
|
strprintf("Insufficient transparent funds for vpub_old %s (miners fee %s, taddr inputs %s)", |
|
|
|
FormatMoney(vpubOldTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); |
|
|
|
} |
|
|
|
info.vpub_old += vpubOldTarget; // funds flowing from public pool
|
|
|
|
vpubOldProcessed = true; |
|
|
|
} |
|
|
|
|
|
|
|
CAmount jsInputValue = 0; |
|
|
|
uint256 jsAnchor; |
|
|
|
std::vector<boost::optional<SproutWitness>> witnesses; |
|
|
|
|
|
|
|
JSDescription prevJoinSplit; |
|
|
|
|
|
|
|
// Keep track of previous JoinSplit and its commitments
|
|
|
|
if (tx_.vjoinsplit.size() > 0) { |
|
|
|
prevJoinSplit = tx_.vjoinsplit.back(); |
|
|
|
} |
|
|
|
|
|
|
|
// If there is no change, the chain has terminated so we can reset the tracked treestate.
|
|
|
|
if (jsChange == 0 && tx_.vjoinsplit.size() > 0) { |
|
|
|
intermediates.clear(); |
|
|
|
previousCommitments.clear(); |
|
|
|
} |
|
|
|
|
|
|
|
//
|
|
|
|
// Consume change as the first input of the JoinSplit.
|
|
|
|
//
|
|
|
|
if (jsChange > 0) { |
|
|
|
LOCK2(cs_main, pwalletMain->cs_wallet); |
|
|
|
|
|
|
|
// Update tree state with previous joinsplit
|
|
|
|
SproutMerkleTree tree; |
|
|
|
auto it = intermediates.find(prevJoinSplit.anchor); |
|
|
|
if (it != intermediates.end()) { |
|
|
|
tree = it->second; |
|
|
|
} else if (!pcoinsTip->GetSproutAnchorAt(prevJoinSplit.anchor, tree)) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor"); |
|
|
|
} |
|
|
|
|
|
|
|
assert(changeOutputIndex != -1); |
|
|
|
boost::optional<SproutWitness> changeWitness; |
|
|
|
int n = 0; |
|
|
|
for (const uint256& commitment : prevJoinSplit.commitments) { |
|
|
|
tree.append(commitment); |
|
|
|
previousCommitments.push_back(commitment); |
|
|
|
if (!changeWitness && changeOutputIndex == n++) { |
|
|
|
changeWitness = tree.witness(); |
|
|
|
} else if (changeWitness) { |
|
|
|
changeWitness.get().append(commitment); |
|
|
|
} |
|
|
|
} |
|
|
|
if (changeWitness) { |
|
|
|
witnesses.push_back(changeWitness); |
|
|
|
} |
|
|
|
jsAnchor = tree.root(); |
|
|
|
intermediates.insert(std::make_pair(tree.root(), tree)); // chained js are interstitial (found in between block boundaries)
|
|
|
|
|
|
|
|
// Decrypt the change note's ciphertext to retrieve some data we need
|
|
|
|
ZCNoteDecryption decryptor(changeKey.receiving_key()); |
|
|
|
auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey); |
|
|
|
try { |
|
|
|
SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt( |
|
|
|
decryptor, |
|
|
|
prevJoinSplit.ciphertexts[changeOutputIndex], |
|
|
|
prevJoinSplit.ephemeralKey, |
|
|
|
hSig, |
|
|
|
(unsigned char)changeOutputIndex); |
|
|
|
|
|
|
|
SproutNote note = plaintext.note(changeAddress); |
|
|
|
info.notes.push_back(note); |
|
|
|
info.zkeys.push_back(changeKey); |
|
|
|
|
|
|
|
jsInputValue += plaintext.value(); |
|
|
|
|
|
|
|
LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n", |
|
|
|
getId(), |
|
|
|
FormatMoney(plaintext.value())); |
|
|
|
|
|
|
|
} catch (const std::exception& e) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what())); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
// Consume spendable non-change notes
|
|
|
|
//
|
|
|
|
std::vector<SproutNote> vInputNotes; |
|
|
|
std::vector<SproutSpendingKey> vInputZKeys; |
|
|
|
std::vector<JSOutPoint> vOutPoints; |
|
|
|
std::vector<boost::optional<SproutWitness>> vInputWitnesses; |
|
|
|
uint256 inputAnchor; |
|
|
|
int numInputsNeeded = (jsChange > 0) ? 1 : 0; |
|
|
|
while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) { |
|
|
|
MergeToAddressInputSproutNote t = zInputsDeque.front(); |
|
|
|
JSOutPoint jso = std::get<0>(t); |
|
|
|
SproutNote note = std::get<1>(t); |
|
|
|
CAmount noteFunds = std::get<2>(t); |
|
|
|
SproutSpendingKey zkey = std::get<3>(t); |
|
|
|
zInputsDeque.pop_front(); |
|
|
|
|
|
|
|
MergeToAddressWitnessAnchorData wad = jsopWitnessAnchorMap[jso.ToString()]; |
|
|
|
vInputWitnesses.push_back(wad.witness); |
|
|
|
if (inputAnchor.IsNull()) { |
|
|
|
inputAnchor = wad.anchor; |
|
|
|
} else if (inputAnchor != wad.anchor) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Selected input notes do not share the same anchor"); |
|
|
|
} |
|
|
|
|
|
|
|
vOutPoints.push_back(jso); |
|
|
|
vInputNotes.push_back(note); |
|
|
|
vInputZKeys.push_back(zkey); |
|
|
|
|
|
|
|
jsInputValue += noteFunds; |
|
|
|
|
|
|
|
int wtxHeight = -1; |
|
|
|
int wtxDepth = -1; |
|
|
|
{ |
|
|
|
LOCK2(cs_main, pwalletMain->cs_wallet); |
|
|
|
const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash]; |
|
|
|
// Zero confirmation notes belong to transactions which have not yet been mined
|
|
|
|
if (mapBlockIndex.find(wtx.hashBlock) == mapBlockIndex.end()) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); |
|
|
|
} |
|
|
|
wtxHeight = komodo_blockheight(wtx.hashBlock); |
|
|
|
wtxDepth = wtx.GetDepthInMainChain(); |
|
|
|
} |
|
|
|
LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)\n", |
|
|
|
getId(), |
|
|
|
jso.hash.ToString().substr(0, 10), |
|
|
|
jso.js, |
|
|
|
int(jso.n), // uint8_t
|
|
|
|
FormatMoney(noteFunds), |
|
|
|
wtxHeight, |
|
|
|
wtxDepth); |
|
|
|
} |
|
|
|
|
|
|
|
// Add history of previous commitments to witness
|
|
|
|
if (vInputNotes.size() > 0) { |
|
|
|
if (vInputWitnesses.size() == 0) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment"); |
|
|
|
} |
|
|
|
|
|
|
|
for (auto& optionalWitness : vInputWitnesses) { |
|
|
|
if (!optionalWitness) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null"); |
|
|
|
} |
|
|
|
SproutWitness w = *optionalWitness; // could use .get();
|
|
|
|
if (jsChange > 0) { |
|
|
|
for (const uint256& commitment : previousCommitments) { |
|
|
|
w.append(commitment); |
|
|
|
} |
|
|
|
if (jsAnchor != w.root()) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, "Witness for spendable note does not have same anchor as change input"); |
|
|
|
} |
|
|
|
} |
|
|
|
witnesses.push_back(w); |
|
|
|
} |
|
|
|
|
|
|
|
// The jsAnchor is null if this JoinSplit is at the start of a new chain
|
|
|
|
if (jsAnchor.IsNull()) { |
|
|
|
jsAnchor = inputAnchor; |
|
|
|
} |
|
|
|
|
|
|
|
// Add spendable notes as inputs
|
|
|
|
std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes)); |
|
|
|
std::copy(vInputZKeys.begin(), vInputZKeys.end(), std::back_inserter(info.zkeys)); |
|
|
|
} |
|
|
|
|
|
|
|
// Accumulate change
|
|
|
|
jsChange = jsInputValue + info.vpub_old; |
|
|
|
|
|
|
|
// Set vpub_new in the last joinsplit (when there are no more notes to spend)
|
|
|
|
if (zInputsDeque.empty()) { |
|
|
|
assert(!vpubNewProcessed); |
|
|
|
if (jsInputValue < vpubNewTarget) { |
|
|
|
throw JSONRPCError(RPC_WALLET_ERROR, |
|
|
|
strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr inputs %s)", |
|
|
|
FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); |
|
|
|
} |
|
|
|
info.vpub_new += vpubNewTarget; // funds flowing back to public pool
|
|
|
|
vpubNewProcessed = true; |
|
|
|
jsChange -= vpubNewTarget; |
|
|
|
// If we are merging to a t-addr, there should be no change
|
|
|
|
if (isToTaddr_) assert(jsChange == 0); |
|
|
|
} |
|
|
|
|
|
|
|
// create dummy output
|
|
|
|
info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new
|
|
|
|
|
|
|
|
// create output for any change
|
|
|
|
if (jsChange > 0) { |
|
|
|
std::string outputType = "change"; |
|
|
|
auto jso = JSOutput(changeAddress, jsChange); |
|
|
|
// If this is the final output, set the target and memo
|
|
|
|
if (isToZaddr_ && vpubNewProcessed) { |
|
|
|
outputType = "target"; |
|
|
|
jso.addr = boost::get<libzcash::SproutPaymentAddress>(toPaymentAddress_); |
|
|
|
if (!hexMemo.empty()) { |
|
|
|
jso.memo = get_memo_from_hex_string(hexMemo); |
|
|
|
} |
|
|
|
} |
|
|
|
info.vjsout.push_back(jso); |
|
|
|
|
|
|
|
LogPrint("zrpcunsafe", "%s: generating note for %s (amount=%s)\n", |
|
|
|
getId(), |
|
|
|
outputType, |
|
|
|
FormatMoney(jsChange)); |
|
|
|
} |
|
|
|
|
|
|
|
obj = perform_joinsplit(info, witnesses, jsAnchor); |
|
|
|
|
|
|
|
if (jsChange > 0) { |
|
|
|
changeOutputIndex = mta_find_output(obj, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Sanity check in case changes to code block above exits loop by invoking 'break'
|
|
|
|
assert(zInputsDeque.size() == 0); |
|
|
|
assert(vpubNewProcessed); |
|
|
|
|
|
|
|
sign_send_raw_transaction(obj); |
|
|
|
return true; |
|
|
|
} |
|
|
@ -866,10 +538,6 @@ UniValue AsyncRPCOperation_mergetoaddress::perform_joinsplit(MergeToAddressJSInf |
|
|
|
{ |
|
|
|
std::vector<boost::optional<SproutWitness>> witnesses; |
|
|
|
uint256 anchor; |
|
|
|
{ |
|
|
|
LOCK(cs_main); |
|
|
|
pwalletMain->GetSproutNoteWitnesses(outPoints, witnesses, anchor); |
|
|
|
} |
|
|
|
return perform_joinsplit(info, witnesses, anchor); |
|
|
|
} |
|
|
|
|
|
|
|