Browse Source

Fix intermediate vpub_new leakage in multi joinsplit tx (#1360)

pull/40/head^2
Simon 7 years ago
parent
commit
c93a0872e8
  1. 466
      src/wallet/asyncrpcoperation_sendmany.cpp

466
src/wallet/asyncrpcoperation_sendmany.cpp

@ -342,9 +342,14 @@ bool AsyncRPCOperation_sendmany::main_impl() {
tx_ = CTransaction(mtx);
// Copy zinputs and zoutputs to more flexible containers
std::deque<SendManyInputJSOP> zInputsDeque;
std::deque<SendManyInputJSOP> zInputsDeque; // zInputsDeque stores minimum numbers of notes for target amount
CAmount tmp = 0;
for (auto o : z_inputs_) {
zInputsDeque.push_back(o);
tmp += std::get<2>(o);
if (tmp >= targetAmount) {
break;
}
}
std::deque<SendManyRecipient> zOutputsDeque;
for (auto o : z_outputs_) {
@ -442,283 +447,216 @@ bool AsyncRPCOperation_sendmany::main_impl() {
* zaddr -> taddrs
* -> zaddrs
*
* Processing order:
* Part 1: taddrs and miners fee
* Part 2: zaddrs
*/
/**
* SCENARIO #3
* Part 1: Add to the transparent value pool.
* Send to zaddrs by chaining JoinSplits together and immediately consuming any change
* Send to taddrs 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 minersFeeProcessed = false;
bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit
CAmount vpubNewTarget = minersFee;
if (t_outputs_total > 0) {
add_taddr_outputs_to_tx();
CAmount taddrTargetAmount = t_outputs_total + minersFee;
minersFeeProcessed = true;
while (zInputsDeque.size() > 0 && taddrTargetAmount > 0) {
AsyncJoinSplitInfo info;
info.vpub_old = 0;
info.vpub_new = 0;
std::vector<JSOutPoint> outPoints;
int n = 0;
while (n++ < ZC_NUM_JS_INPUTS && taddrTargetAmount > 0) {
SendManyInputJSOP o = zInputsDeque.front();
JSOutPoint outPoint = std::get<0>(o);
Note note = std::get<1>(o);
CAmount noteFunds = std::get<2>(o);
zInputsDeque.pop_front();
vpubNewTarget += t_outputs_total;
}
info.notes.push_back(note);
outPoints.push_back(outPoint);
int wtxHeight = -1;
int wtxDepth = -1;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
const CWalletTx& wtx = pwalletMain->mapWallet[outPoint.hash];
wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight;
wtxDepth = wtx.GetDepthInMainChain();
}
LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)\n",
getId(),
outPoint.hash.ToString().substr(0, 10),
outPoint.js,
int(outPoint.n), // uint8_t
FormatMoney(noteFunds),
wtxHeight,
wtxDepth
);
// Keep track of treestate within this transaction
boost::unordered_map<uint256, ZCIncrementalMerkleTree, CCoinsKeyHasher> intermediates;
std::vector<uint256> previousCommitments;
// Put value back into the value pool
if (noteFunds >= taddrTargetAmount) {
jsChange = noteFunds - taddrTargetAmount;
info.vpub_new += taddrTargetAmount;
} else {
info.vpub_new += noteFunds;
}
while (!vpubNewProcessed) {
AsyncJoinSplitInfo info;
info.vpub_old = 0;
info.vpub_new = 0;
taddrTargetAmount -= noteFunds;
if (taddrTargetAmount <= 0) {
break;
}
}
CAmount jsInputValue = 0;
uint256 jsAnchor;
std::vector<boost::optional<ZCIncrementalWitness>> witnesses;
if (jsChange > 0) {
info.vjsout.push_back(JSOutput());
info.vjsout.push_back(JSOutput(frompaymentaddress_, jsChange));
LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
getId(),
FormatMoney(jsChange)
);
}
JSDescription prevJoinSplit;
// Keep track of previous JoinSplit and its commitments
if (tx_.vjoinsplit.size() > 0) {
prevJoinSplit = tx_.vjoinsplit.back();
}
obj = perform_joinsplit(info, outPoints);
// 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();
}
if (jsChange > 0) {
changeOutputIndex = find_output(obj, 1);
//
// Consume change as the first input of the JoinSplit.
//
if (jsChange > 0) {
LOCK2(cs_main, pwalletMain->cs_wallet);
// Update tree state with previous joinsplit
ZCIncrementalMerkleTree tree;
auto it = intermediates.find(prevJoinSplit.anchor);
if (it != intermediates.end()) {
tree = it->second;
} else if (!pcoinsTip->GetAnchorAt(prevJoinSplit.anchor, tree)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor");
}
}
}
assert(changeOutputIndex != -1);
boost::optional<ZCIncrementalWitness> 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(spendingkey_.viewing_key());
auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey);
try {
NotePlaintext plaintext = NotePlaintext::decrypt(
decryptor,
prevJoinSplit.ciphertexts[changeOutputIndex],
prevJoinSplit.ephemeralKey,
hSig,
(unsigned char) changeOutputIndex);
Note note = plaintext.note(frompaymentaddress_);
info.notes.push_back(note);
/**
* SCENARIO #3
* Part 2: Send to zaddrs by chaining JoinSplits together and immediately consuming any change
*/
if (z_outputs_total>0) {
jsInputValue += plaintext.value;
// Keep track of treestate within this transaction
boost::unordered_map<uint256, ZCIncrementalMerkleTree, CCoinsKeyHasher> intermediates;
std::vector<uint256> previousCommitments;
while (zOutputsDeque.size() > 0) {
AsyncJoinSplitInfo info;
info.vpub_old = 0;
info.vpub_new = 0;
LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n",
getId(),
FormatMoney(plaintext.value)
);
CAmount jsInputValue = 0;
uint256 jsAnchor;
std::vector<boost::optional<ZCIncrementalWitness>> witnesses;
} catch (const std::exception& e) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what()));
}
}
JSDescription prevJoinSplit;
// Keep track of previous JoinSplit and its commitments
if (tx_.vjoinsplit.size() > 0) {
prevJoinSplit = tx_.vjoinsplit.back();
//
// Consume spendable non-change notes
//
std::vector<Note> vInputNotes;
std::vector<JSOutPoint> vOutPoints;
std::vector<boost::optional<ZCIncrementalWitness>> vInputWitnesses;
uint256 inputAnchor;
int numInputsNeeded = (jsChange>0) ? 1 : 0;
while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) {
SendManyInputJSOP t = zInputsDeque.front();
JSOutPoint jso = std::get<0>(t);
Note note = std::get<1>(t);
CAmount noteFunds = std::get<2>(t);
zInputsDeque.pop_front();
WitnessAnchorData 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);
// 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();
}
jsInputValue += noteFunds;
//
// Consume change as the first input of the JoinSplit.
//
if (jsChange > 0) {
int wtxHeight = -1;
int wtxDepth = -1;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
// Update tree state with previous joinsplit
ZCIncrementalMerkleTree tree;
auto it = intermediates.find(prevJoinSplit.anchor);
if (it != intermediates.end()) {
tree = it->second;
} else if (!pcoinsTip->GetAnchorAt(prevJoinSplit.anchor, tree)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor");
}
assert(changeOutputIndex != -1);
boost::optional<ZCIncrementalWitness> 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(spendingkey_.viewing_key());
auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey);
try {
NotePlaintext plaintext = NotePlaintext::decrypt(
decryptor,
prevJoinSplit.ciphertexts[changeOutputIndex],
prevJoinSplit.ephemeralKey,
hSig,
(unsigned char) changeOutputIndex);
Note note = plaintext.note(frompaymentaddress_);
info.notes.push_back(note);
jsInputValue += plaintext.value;
const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash];
wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight;
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
);
}
LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n",
getId(),
FormatMoney(plaintext.value)
);
// Add history of previous commitments to witness
if (vInputNotes.size() > 0) {
} catch (const std::exception& e) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what()));
}
if (vInputWitnesses.size()==0) {
throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment");
}
//
// Consume spendable non-change notes
//
std::vector<Note> vInputNotes;
std::vector<JSOutPoint> vOutPoints;
std::vector<boost::optional<ZCIncrementalWitness>> vInputWitnesses;
uint256 inputAnchor;
int numInputsNeeded = (jsChange>0) ? 1 : 0;
while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) {
SendManyInputJSOP t = zInputsDeque.front();
JSOutPoint jso = std::get<0>(t);
Note note = std::get<1>(t);
CAmount noteFunds = std::get<2>(t);
zInputsDeque.pop_front();
WitnessAnchorData 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");
for (auto & optionalWitness : vInputWitnesses) {
if (!optionalWitness) {
throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null");
}
vOutPoints.push_back(jso);
vInputNotes.push_back(note);
jsInputValue += noteFunds;
int wtxHeight = -1;
int wtxDepth = -1;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash];
wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight;
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");
ZCIncrementalWitness w = *optionalWitness; // could use .get();
if (jsChange > 0) {
for (const uint256& commitment : previousCommitments) {
w.append(commitment);
}
ZCIncrementalWitness 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");
}
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;
}
witnesses.push_back(w);
}
// Add spendable notes as inputs
std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes));
// The jsAnchor is null if this JoinSplit is at the start of a new chain
if (jsAnchor.IsNull()) {
jsAnchor = inputAnchor;
}
//
// Find recipient to transfer funds to
//
// Add spendable notes as inputs
std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes));
}
// Find recipient to transfer funds to
std::string address, hexMemo;
CAmount value = 0;
if (zOutputsDeque.size() > 0) {
SendManyRecipient smr = zOutputsDeque.front();
std::string address = std::get<0>(smr);
CAmount value = std::get<1>(smr);
std::string hexMemo = std::get<2>(smr);
address = std::get<0>(smr);
value = std::get<1>(smr);
hexMemo = std::get<2>(smr);
zOutputsDeque.pop_front();
}
// Will we have any change? Has the miners fee been processed yet?
jsChange = 0;
CAmount outAmount = value;
if (!minersFeeProcessed) {
if (jsInputValue < minersFee) {
throw JSONRPCError(RPC_WALLET_ERROR, "Not enough funds to pay miners fee");
}
outAmount += minersFee;
// Reset change
jsChange = 0;
CAmount outAmount = value;
// Set vpub_new in the last joinsplit (when there are no more notes to spend or zaddr outputs to satisfy)
if (zOutputsDeque.size() == 0 && zInputsDeque.size() == 0) {
assert(!vpubNewProcessed);
if (jsInputValue < vpubNewTarget) {
throw JSONRPCError(RPC_WALLET_ERROR,
strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr outputs %s)",
FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(t_outputs_total)));
}
outAmount += vpubNewTarget;
info.vpub_new += vpubNewTarget; // funds flowing back to public pool
vpubNewProcessed = true;
jsChange = jsInputValue - outAmount;
assert(jsChange >= 0);
}
else {
// This is not the last joinsplit, so compute change and any amount still due to the recipient
if (jsInputValue > outAmount) {
jsChange = jsInputValue - outAmount;
} else if (outAmount > jsInputValue) {
@ -729,42 +667,44 @@ bool AsyncRPCOperation_sendmany::main_impl() {
// reduce the amount being sent right now to the value of all inputs
value = jsInputValue;
if (!minersFeeProcessed) {
value -= minersFee;
}
}
if (!minersFeeProcessed) {
minersFeeProcessed = true;
info.vpub_new += minersFee; // funds flowing back to public pool
}
// create output for recipient
}
// create output for recipient
if (address.empty()) {
assert(value==0);
info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new
} else {
PaymentAddress pa = CZCPaymentAddress(address).Get();
JSOutput jso = JSOutput(pa, value);
if (hexMemo.size() > 0) {
jso.memo = get_memo_from_hex_string(hexMemo);
}
info.vjsout.push_back(jso);
// create output for any change
if (jsChange>0) {
info.vjsout.push_back(JSOutput(frompaymentaddress_, jsChange));
}
LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
getId(),
FormatMoney(jsChange)
);
}
// create output for any change
if (jsChange>0) {
info.vjsout.push_back(JSOutput(frompaymentaddress_, jsChange));
obj = perform_joinsplit(info, witnesses, jsAnchor);
LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n",
getId(),
FormatMoney(jsChange)
);
}
if (jsChange > 0) {
changeOutputIndex = find_output(obj, 1);
}
obj = perform_joinsplit(info, witnesses, jsAnchor);
if (jsChange > 0) {
changeOutputIndex = find_output(obj, 1);
}
}
// Sanity check in case changes to code block above exits loop by invoking 'break'
assert(zInputsDeque.size() == 0);
assert(zOutputsDeque.size() == 0);
assert(vpubNewProcessed);
sign_send_raw_transaction(obj);
return true;
}

Loading…
Cancel
Save