Created
August 4, 2023 08:41
-
-
Save delta1/659416d0c273b9112cd03b5350cbe00e to your computer and use it in GitHub Desktop.
elements code sample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
static bool CreateTransactionInternal( | |
CWallet& wallet, | |
const std::vector<CRecipient>& vecSend, | |
CTransactionRef& tx, | |
CAmount& nFeeRet, | |
int& nChangePosInOut, | |
bilingual_str& error, | |
const CCoinControl& coin_control, | |
FeeCalculation& fee_calc_out, | |
bool sign, | |
BlindDetails* blind_details, | |
const IssuanceDetails* issuance_details) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) | |
{ | |
if (blind_details || issuance_details) { | |
assert(g_con_elementsmode); | |
} | |
if (blind_details) { | |
// Clear out previous blinding/data info as needed | |
resetBlindDetails(blind_details); | |
} | |
AssertLockHeld(wallet.cs_wallet); | |
CMutableTransaction txNew; // The resulting transaction that we make | |
txNew.nLockTime = GetLocktimeForNewTransaction(wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); | |
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy | |
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; | |
CScript dummy_script = CScript() << 0x00; | |
CAmountMap map_recipients_sum; | |
// Always assume that we are at least sending policyAsset. | |
map_recipients_sum[::policyAsset] = 0; | |
std::vector<std::unique_ptr<ReserveDestination>> reservedest; | |
// Set the long term feerate estimate to the wallet's consolidate feerate | |
coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate; | |
const OutputType change_type = wallet.TransactionChangeType(coin_control.m_change_type ? *coin_control.m_change_type : wallet.m_default_change_type, vecSend); | |
reservedest.emplace_back(new ReserveDestination(&wallet, change_type)); // policy asset | |
std::set<CAsset> assets_seen; | |
unsigned int outputs_to_subtract_fee_from = 0; // The number of outputs which we are subtracting the fee from | |
for (const auto& recipient : vecSend) | |
{ | |
// Pad change keys to cover total possible number of assets | |
// One already exists(for policyAsset), so one for each destination | |
if (assets_seen.insert(recipient.asset).second) { | |
reservedest.emplace_back(new ReserveDestination(&wallet, change_type)); | |
} | |
// Skip over issuance outputs, no need to select those coins | |
if (recipient.asset == CAsset(uint256S("1")) || recipient.asset == CAsset(uint256S("2"))) { | |
continue; | |
} | |
map_recipients_sum[recipient.asset] += recipient.nAmount; | |
if (recipient.fSubtractFeeFromAmount) { | |
outputs_to_subtract_fee_from++; | |
coin_selection_params.m_subtract_fee_outputs = true; | |
} | |
} | |
// Create change script that will be used if we need change | |
// ELEMENTS: A map that keeps track of the change script for each asset and also | |
// the index of the reservedest used for that script (-1 if none). | |
std::map<CAsset, std::pair<int, CScript>> mapScriptChange; | |
// For manually set change, we need to use the blinding pubkey associated | |
// with the manually-set address rather than generating one from the wallet | |
std::map<CAsset, std::optional<CPubKey>> mapBlindingKeyChange; | |
// coin control: send change to custom address | |
if (coin_control.destChange.size() > 0) { | |
for (const auto& dest : coin_control.destChange) { | |
// No need to test we cover all assets. We produce error for that later. | |
mapScriptChange[dest.first] = std::pair<int, CScript>(-1, GetScriptForDestination(dest.second)); | |
if (IsBlindDestination(dest.second)) { | |
mapBlindingKeyChange[dest.first] = GetDestinationBlindingKey(dest.second); | |
} else { | |
mapBlindingKeyChange[dest.first] = std::nullopt; | |
} | |
} | |
} else { // no coin control: send change to newly generated address | |
// Note: We use a new key here to keep it from being obvious which side is the change. | |
// The drawback is that by not reusing a previous key, the change may be lost if a | |
// backup is restored, if the backup doesn't have the new private key for the change. | |
// If we reused the old key, it would be possible to add code to look for and | |
// rediscover unknown transactions that were written with keys of ours to recover | |
// post-backup change. | |
// One change script per output asset. | |
size_t index = 0; | |
for (const auto& value : map_recipients_sum) { | |
// Reserve a new key pair from key pool. If it fails, provide a dummy | |
// destination in case we don't need change. | |
CTxDestination dest; | |
bilingual_str dest_err; | |
if (index >= reservedest.size() || !reservedest[index]->GetReservedDestination(dest, true, dest_err)) { | |
if (dest_err.empty()) { | |
dest_err = _("Please call keypoolrefill first"); | |
} | |
error = _("Transaction needs a change address, but we can't generate it.") + Untranslated(" ") + dest_err; | |
// ELEMENTS: We need to put a dummy destination here. Core uses an empty script | |
// but we can't because empty scripts indicate fees (which trigger assertion | |
// failures in `BlindTransaction`). We also set the index to -1, indicating | |
// that this destination is not actually used, and therefore should not be | |
// returned by the `ReturnDestination` loop below. | |
mapScriptChange[value.first] = std::pair<int, CScript>(-1, dummy_script); | |
} else { | |
mapScriptChange[value.first] = std::pair<int, CScript>(index, GetScriptForDestination(dest)); | |
++index; | |
} | |
} | |
// Also make sure we have change scripts for the pre-selected inputs. | |
std::vector<COutPoint> vPresetInputs; | |
coin_control.ListSelected(vPresetInputs); | |
for (const COutPoint& presetInput : vPresetInputs) { | |
CAsset asset; | |
std::map<uint256, CWalletTx>::const_iterator it = wallet.mapWallet.find(presetInput.hash); | |
CTxOut txout; | |
if (it != wallet.mapWallet.end()) { | |
asset = it->second.GetOutputAsset(wallet, presetInput.n); | |
} else if (coin_control.GetExternalOutput(presetInput, txout)) { | |
asset = txout.nAsset.GetAsset(); | |
} else { | |
// Ignore this here, will fail more gracefully later. | |
continue; | |
} | |
if (mapScriptChange.find(asset) != mapScriptChange.end()) { | |
// This asset already has a change script. | |
continue; | |
} | |
CTxDestination dest; | |
bilingual_str dest_err; | |
if (index >= reservedest.size() || !reservedest[index]->GetReservedDestination(dest, true, dest_err)) { | |
if (dest_err.empty()) { | |
dest_err = _("Keypool ran out, please call keypoolrefill first"); | |
} | |
error = _("Transaction needs a change address, but we can't generate it.") + Untranslated(" ") + dest_err; | |
return false; | |
} | |
CScript scriptChange = GetScriptForDestination(dest); | |
// A valid destination implies a change script (and | |
// vice-versa). An empty change script will abort later, if the | |
// change keypool ran out, but change is required. | |
CHECK_NONFATAL(IsValidDestination(dest) != (scriptChange == dummy_script)); | |
mapScriptChange[asset] = std::pair<int, CScript>(index, scriptChange); | |
++index; | |
} | |
} | |
assert(mapScriptChange.size() > 0); | |
CTxOut change_prototype_txout(mapScriptChange.begin()->first, 0, mapScriptChange.begin()->second.second); | |
// TODO CA: Set this for each change output | |
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); | |
if (g_con_elementsmode) { | |
if (blind_details) { | |
change_prototype_txout.nAsset.vchCommitment.resize(33); | |
change_prototype_txout.nValue.vchCommitment.resize(33); | |
change_prototype_txout.nNonce.vchCommitment.resize(33); | |
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); | |
coin_selection_params.change_output_size += (MAX_RANGEPROOF_SIZE + DEFAULT_SURJECTIONPROOF_SIZE + WITNESS_SCALE_FACTOR - 1)/WITNESS_SCALE_FACTOR; | |
} else { | |
change_prototype_txout.nAsset.vchCommitment.resize(33); | |
change_prototype_txout.nValue.vchCommitment.resize(9); | |
change_prototype_txout.nNonce.vchCommitment.resize(1); | |
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); | |
} | |
} | |
// Get size of spending the change output | |
int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet); | |
// If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh | |
// as lower-bound to allow BnB to do it's thing | |
if (change_spend_size == -1) { | |
coin_selection_params.change_spend_size = DUMMY_NESTED_P2WPKH_INPUT_SIZE; | |
} else { | |
coin_selection_params.change_spend_size = (size_t)change_spend_size; | |
} | |
// Set discard feerate | |
coin_selection_params.m_discard_feerate = GetDiscardRate(wallet); | |
// Get the fee rate to use effective values in coin selection | |
FeeCalculation feeCalc; | |
coin_selection_params.m_effective_feerate = GetMinimumFeeRate(wallet, coin_control, &feeCalc); | |
// Do not, ever, assume that it's fine to change the fee rate if the user has explicitly | |
// provided one | |
if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) { | |
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::SAT_VB)); | |
return false; | |
} | |
if (feeCalc.reason == FeeReason::FALLBACK && !wallet.m_allow_fallback_fee) { | |
// eventually allow a fallback fee | |
error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); | |
return false; | |
} | |
// Calculate the cost of change | |
// Cost of change is the cost of creating the change output + cost of spending the change output in the future. | |
// For creating the change output now, we use the effective feerate. | |
// For spending the change output in the future, we use the discard feerate for now. | |
// So cost of change = (change output size * effective feerate) + (size of spending change output * discard feerate) | |
coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size); | |
coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee; | |
// vouts to the payees | |
if (!coin_selection_params.m_subtract_fee_outputs) { | |
coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size) | |
if (g_con_elementsmode) { | |
coin_selection_params.tx_noinputs_size += 46; // fee output: 9 bytes value, 1 byte scriptPubKey, 33 bytes asset, 1 byte nonce, 1 byte each for null rangeproof/surjectionproof | |
} | |
} | |
// ELEMENTS: If we have blinded inputs but no blinded outputs (which, since the wallet | |
// makes an effort to not produce change, is a common case) then we need to add a | |
// dummy output. | |
bool may_need_blinded_dummy = !!blind_details; | |
for (const auto& recipient : vecSend) | |
{ | |
CTxOut txout(recipient.asset, recipient.nAmount, recipient.scriptPubKey); | |
txout.nNonce.vchCommitment = std::vector<unsigned char>(recipient.confidentiality_key.begin(), recipient.confidentiality_key.end()); | |
// Include the fee cost for outputs. | |
if (!coin_selection_params.m_subtract_fee_outputs) { | |
coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION); | |
} | |
if (recipient.asset == policyAsset && IsDust(txout, wallet.chain().relayDustFee())) | |
{ | |
error = _("Transaction amount too small"); | |
return false; | |
} | |
txNew.vout.push_back(txout); | |
// ELEMENTS | |
if (blind_details) { | |
blind_details->o_pubkeys.push_back(recipient.confidentiality_key); | |
if (blind_details->o_pubkeys.back().IsFullyValid()) { | |
may_need_blinded_dummy = false; | |
blind_details->num_to_blind++; | |
blind_details->only_recipient_blind_index = txNew.vout.size()-1; | |
if (!coin_selection_params.m_subtract_fee_outputs) { | |
coin_selection_params.tx_noinputs_size += (MAX_RANGEPROOF_SIZE + DEFAULT_SURJECTIONPROOF_SIZE + WITNESS_SCALE_FACTOR - 1)/WITNESS_SCALE_FACTOR; | |
} | |
} | |
} | |
} | |
if (may_need_blinded_dummy && !coin_selection_params.m_subtract_fee_outputs) { | |
// dummy output: 33 bytes value, 2 byte scriptPubKey, 33 bytes asset, 1 byte nonce, 66 bytes dummy rangeproof, 1 byte null surjectionproof | |
// FIXME actually, we currently just hand off to BlindTransaction which will put | |
// a full rangeproof and surjectionproof. We should fix this when we overhaul | |
// the blinding logic. | |
coin_selection_params.tx_noinputs_size += 70 + 66 +(MAX_RANGEPROOF_SIZE + DEFAULT_SURJECTIONPROOF_SIZE + WITNESS_SCALE_FACTOR - 1)/WITNESS_SCALE_FACTOR; | |
} | |
// If we are going to issue an asset, add the issuance data to the noinputs_size so that | |
// we allocate enough coins for them. | |
if (issuance_details) { | |
size_t issue_count = 0; | |
for (unsigned int i = 0; i < txNew.vout.size(); i++) { | |
if (txNew.vout[i].nAsset.IsExplicit() && txNew.vout[i].nAsset.GetAsset() == CAsset(uint256S("1"))) { | |
issue_count++; | |
} else if (txNew.vout[i].nAsset.IsExplicit() && txNew.vout[i].nAsset.GetAsset() == CAsset(uint256S("2"))) { | |
issue_count++; | |
} | |
} | |
if (issue_count > 0) { | |
// Allocate space for blinding nonce, entropy, and whichever of nAmount/nInflationKeys is null | |
coin_selection_params.tx_noinputs_size += 2 * 32 + 2 * (2 - issue_count); | |
} | |
// Allocate non-null nAmount/nInflationKeys and rangeproofs | |
if (issuance_details->blind_issuance) { | |
coin_selection_params.tx_noinputs_size += issue_count * (33 * WITNESS_SCALE_FACTOR + MAX_RANGEPROOF_SIZE + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR; | |
} else { | |
coin_selection_params.tx_noinputs_size += issue_count * 9; | |
} | |
} | |
// Include the fees for things that aren't inputs, excluding the change output | |
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); | |
CAmountMap map_selection_target = map_recipients_sum; | |
map_selection_target[policyAsset] += not_input_fees; | |
// Get available coins | |
std::vector<COutput> vAvailableCoins; | |
AvailableCoins(wallet, vAvailableCoins, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0); | |
// Choose coins to use | |
std::optional<SelectionResult> result = SelectCoins(wallet, vAvailableCoins, /* nTargetValue */ map_selection_target, coin_control, coin_selection_params); | |
if (!result) { | |
error = _("Insufficient funds"); | |
return false; | |
} | |
// If all of our inputs are explicit, we don't need a blinded dummy | |
if (may_need_blinded_dummy) { | |
may_need_blinded_dummy = false; | |
for (const auto& coin : result->GetInputSet()) { | |
if (!coin.txout.nValue.IsExplicit()) { | |
may_need_blinded_dummy = true; | |
break; | |
} | |
} | |
} | |
// Always make a change output | |
// We will reduce the fee from this change output later, and remove the output if it is too small. | |
// ELEMENTS: wrap this all in a loop, set nChangePosInOut specifically for policy asset | |
CAmountMap map_change_and_fee = result->GetSelectedValue() - map_recipients_sum; | |
// Zero out any non-policy assets which have zero change value | |
for (auto it = map_change_and_fee.begin(); it != map_change_and_fee.end(); ) { | |
if (it->first != policyAsset && it->second == 0) { | |
it = map_change_and_fee.erase(it); | |
} else { | |
++it; | |
} | |
} | |
// Uniformly randomly place change outputs for all assets, except that the policy-asset | |
// change may have a fixed position. | |
std::vector<std::optional<CAsset>> change_pos{txNew.vout.size() + map_change_and_fee.size()}; | |
if (nChangePosInOut == -1) { | |
// randomly set policyasset change position | |
} else if ((unsigned int)nChangePosInOut >= change_pos.size()) { | |
error = _("Transaction change output index out of range"); | |
return false; | |
} else { | |
change_pos[nChangePosInOut] = policyAsset; | |
} | |
for (const auto& asset_change_and_fee : map_change_and_fee) { | |
// No need to randomly set the policyAsset change if has been set manually | |
if (nChangePosInOut >= 0 && asset_change_and_fee.first == policyAsset) { | |
continue; | |
} | |
int index; | |
do { | |
index = GetRandInt(change_pos.size()); | |
} while (change_pos[index]); | |
change_pos[index] = asset_change_and_fee.first; | |
if (asset_change_and_fee.first == policyAsset) { | |
nChangePosInOut = index; | |
} | |
} | |
// Create all the change outputs in their respective places, inserting them | |
// in increasing order so that none of them affect each others' indices | |
for (unsigned int i = 0; i < change_pos.size(); i++) { | |
if (!change_pos[i]) { | |
continue; | |
} | |
const CAsset& asset = *change_pos[i]; | |
const CAmount& change_and_fee = map_change_and_fee.at(asset); | |
assert(change_and_fee >= 0); | |
const std::map<CAsset, std::pair<int, CScript>>::const_iterator itScript = mapScriptChange.find(asset); | |
if (itScript == mapScriptChange.end()) { | |
error = Untranslated(strprintf("No change destination provided for asset %s", asset.GetHex())); | |
return false; | |
} | |
CTxOut newTxOut(asset, change_and_fee, itScript->second.second); | |
if (blind_details) { | |
std::optional<CPubKey> blind_pub = std::nullopt; | |
// We cannot blind zero-valued outputs, and anyway they will be dropped | |
// later in this function during the dust check | |
if (change_and_fee > 0) { | |
const auto itBlindingKey = mapBlindingKeyChange.find(asset); | |
if (itBlindingKey != mapBlindingKeyChange.end()) { | |
// If the change output was specified, use the blinding key that | |
// came with the specified address (if any) | |
blind_pub = itBlindingKey->second; | |
} else { | |
// Otherwise, we generated it from our own wallet, so get the | |
// blinding key from our own wallet. | |
blind_pub = wallet.GetBlindingPubKey(itScript->second.second); | |
} | |
} else { | |
assert(asset == policyAsset); | |
} | |
if (blind_pub) { | |
blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + i, *blind_pub); | |
assert(blind_pub->IsFullyValid()); | |
blind_details->num_to_blind++; | |
blind_details->change_to_blind++; | |
blind_details->only_change_pos = i; | |
// Place the blinding pubkey here in case of fundraw calls | |
newTxOut.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub->begin(), blind_pub->end()); | |
} else { | |
blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + i, CPubKey()); | |
} | |
} | |
// Insert change output | |
txNew.vout.insert(txNew.vout.begin() + i, newTxOut); | |
} | |
// Add fee output. | |
if (g_con_elementsmode) { | |
CTxOut fee(::policyAsset, 0, CScript()); | |
assert(fee.IsFee()); | |
txNew.vout.push_back(fee); | |
if (blind_details) { | |
blind_details->o_pubkeys.push_back(CPubKey()); | |
} | |
} | |
assert(nChangePosInOut != -1); | |
auto change_position = txNew.vout.begin() + nChangePosInOut; | |
// end ELEMENTS | |
// Set token input if reissuing | |
int reissuance_index = -1; | |
uint256 token_blinding; | |
// Elements: Shuffle here to preserve random ordering for surjection proofs | |
// selected_coins = std::vector<CInputCoin>(setCoins.begin(), setCoins.end()); | |
// Shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext()); | |
// Shuffle selected coins and fill in final vin | |
std::vector<CInputCoin> selected_coins = result->GetShuffledInputVector(); | |
// Note how the sequence number is set to non-maxint so that | |
// the nLockTime set above actually works. | |
// | |
// BIP125 defines opt-in RBF as any nSequence < maxint-1, so | |
// we use the highest possible value in that range (maxint-2) | |
// to avoid conflicting with other possible uses of nSequence, | |
// and in the spirit of "smallest possible change from prior | |
// behavior." | |
const uint32_t nSequence{coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : CTxIn::MAX_SEQUENCE_NONFINAL}; | |
for (const auto& coin : selected_coins) { | |
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); | |
if (issuance_details && coin.asset == issuance_details->reissuance_token) { | |
reissuance_index = txNew.vin.size() - 1; | |
token_blinding = coin.bf_asset; | |
} | |
} | |
// ELEMENTS add issuance details and blinding details | |
std::vector<CKey> issuance_asset_keys; | |
std::vector<CKey> issuance_token_keys; | |
if (issuance_details) { | |
// Fill in issuances now that inputs are set | |
assert(txNew.vin.size() > 0); | |
int asset_index = -1; | |
int token_index = -1; | |
for (unsigned int i = 0; i < txNew.vout.size(); i++) { | |
if (txNew.vout[i].nAsset.IsExplicit() && txNew.vout[i].nAsset.GetAsset() == CAsset(uint256S("1"))) { | |
asset_index = i; | |
} else if (txNew.vout[i].nAsset.IsExplicit() && txNew.vout[i].nAsset.GetAsset() == CAsset(uint256S("2"))) { | |
token_index = i; | |
} | |
} | |
// Initial issuance request | |
if (issuance_details->reissuance_asset.IsNull() && issuance_details->reissuance_token.IsNull() && (asset_index != -1 || token_index != -1)) { | |
uint256 entropy; | |
CAsset asset; | |
CAsset token; | |
// Initial issuance always uses vin[0] | |
GenerateAssetEntropy(entropy, txNew.vin[0].prevout, issuance_details->contract_hash); | |
CalculateAsset(asset, entropy); | |
CalculateReissuanceToken(token, entropy, issuance_details->blind_issuance); | |
CScript blindingScript(CScript() << OP_RETURN << std::vector<unsigned char>(txNew.vin[0].prevout.hash.begin(), txNew.vin[0].prevout.hash.end()) << txNew.vin[0].prevout.n); | |
txNew.vin[0].assetIssuance.assetEntropy = issuance_details->contract_hash; | |
// We're making asset outputs, fill out asset type and issuance input | |
if (asset_index != -1) { | |
txNew.vin[0].assetIssuance.nAmount = txNew.vout[asset_index].nValue; | |
txNew.vout[asset_index].nAsset = asset; | |
if (issuance_details->blind_issuance && blind_details) { | |
issuance_asset_keys.push_back(wallet.GetBlindingKey(&blindingScript)); | |
blind_details->num_to_blind++; | |
} | |
} | |
// We're making reissuance token outputs | |
if (token_index != -1) { | |
txNew.vin[0].assetIssuance.nInflationKeys = txNew.vout[token_index].nValue; | |
txNew.vout[token_index].nAsset = token; | |
if (issuance_details->blind_issuance && blind_details) { | |
issuance_token_keys.push_back(wallet.GetBlindingKey(&blindingScript)); | |
blind_details->num_to_blind++; | |
// If we're blinding a token issuance and no assets, we must make | |
// the asset issuance a blinded commitment to 0 | |
if (asset_index == -1) { | |
txNew.vin[0].assetIssuance.nAmount = 0; | |
issuance_asset_keys.push_back(wallet.GetBlindingKey(&blindingScript)); | |
blind_details->num_to_blind++; | |
} | |
} | |
} | |
// Asset being reissued with explicitly named asset/token | |
} else if (asset_index != -1) { | |
assert(reissuance_index != -1); | |
// Fill in output with issuance | |
txNew.vout[asset_index].nAsset = issuance_details->reissuance_asset; | |
// Fill in issuance | |
// Blinding revealing underlying asset | |
txNew.vin[reissuance_index].assetIssuance.assetBlindingNonce = token_blinding; | |
txNew.vin[reissuance_index].assetIssuance.assetEntropy = issuance_details->entropy; | |
txNew.vin[reissuance_index].assetIssuance.nAmount = txNew.vout[asset_index].nValue; | |
// If blinded token derivation, blind the issuance | |
CAsset temp_token; | |
CalculateReissuanceToken(temp_token, issuance_details->entropy, true); | |
if (temp_token == issuance_details->reissuance_token && blind_details) { | |
CScript blindingScript(CScript() << OP_RETURN << std::vector<unsigned char>(txNew.vin[reissuance_index].prevout.hash.begin(), txNew.vin[reissuance_index].prevout.hash.end()) << txNew.vin[reissuance_index].prevout.n); | |
issuance_asset_keys.resize(reissuance_index); | |
issuance_asset_keys.push_back(wallet.GetBlindingKey(&blindingScript)); | |
blind_details->num_to_blind++; | |
} | |
} | |
} | |
// Do "initial blinding" for fee estimation purposes | |
TxSize tx_sizes; | |
CMutableTransaction tx_blinded = txNew; | |
if (blind_details) { | |
if (!fillBlindDetails(blind_details, &wallet, tx_blinded, selected_coins, error)) { | |
return false; | |
} | |
txNew = tx_blinded; // sigh, `fillBlindDetails` may have modified txNew | |
int ret = BlindTransaction(blind_details->i_amount_blinds, blind_details->i_asset_blinds, blind_details->i_assets, blind_details->i_amounts, blind_details->o_amount_blinds, blind_details->o_asset_blinds, blind_details->o_pubkeys, issuance_asset_keys, issuance_token_keys, tx_blinded); | |
assert(ret != -1); | |
if (ret != blind_details->num_to_blind) { | |
error = _("Unable to blind the transaction properly. This should not happen."); | |
return false; | |
} | |
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(tx_blinded), &wallet, &coin_control); | |
} else { | |
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control); | |
} | |
// end ELEMENTS | |
// Calculate the transaction fee | |
int nBytes = tx_sizes.vsize; | |
if (nBytes == -1) { | |
error = _("Missing solving data for estimating transaction size"); | |
return false; | |
} | |
nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes); | |
// Subtract fee from the change output if not subtracting it from recipient outputs | |
CAmount fee_needed = nFeeRet; | |
if (!coin_selection_params.m_subtract_fee_outputs) { | |
change_position->nValue = change_position->nValue.GetAmount() - fee_needed; | |
} | |
// We want to drop the change to fees if: | |
// 1. The change output would be dust | |
// 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change) | |
CAmount change_amount = change_position->nValue.GetAmount(); | |
if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change) | |
{ | |
bool was_blinded = blind_details && blind_details->o_pubkeys[nChangePosInOut].IsValid(); | |
// If the change was blinded, and was the only blinded output, we cannot drop it | |
// without causing the transaction to fail to balance. So keep it, and merely | |
// zero it out. | |
if (was_blinded && blind_details->num_to_blind == 1) { | |
assert (may_need_blinded_dummy); | |
change_position->scriptPubKey = CScript() << OP_RETURN; | |
change_position->nValue = 0; | |
} else { | |
txNew.vout.erase(change_position); | |
change_pos[nChangePosInOut] = std::nullopt; | |
tx_blinded.vout.erase(tx_blinded.vout.begin() + nChangePosInOut); | |
if (tx_blinded.witness.vtxoutwit.size() > (unsigned) nChangePosInOut) { | |
tx_blinded.witness.vtxoutwit.erase(tx_blinded.witness.vtxoutwit.begin() + nChangePosInOut); | |
} | |
if (blind_details) { | |
blind_details->o_amounts.erase(blind_details->o_amounts.begin() + nChangePosInOut); | |
blind_details->o_assets.erase(blind_details->o_assets.begin() + nChangePosInOut); | |
blind_details->o_pubkeys.erase(blind_details->o_pubkeys.begin() + nChangePosInOut); | |
// If change_amount == 0, we did not increment num_to_blind initially | |
// and therefore do not need to decrement it here. | |
if (was_blinded) { | |
blind_details->num_to_blind--; | |
blind_details->change_to_blind--; | |
// FIXME: If we drop the change *and* this means we have only one | |
// blinded output *and* we have no blinded inputs, then this puts | |
// us in a situation where BlindTransaction will fail. This is | |
// prevented in fillBlindDetails, which adds an OP_RETURN output | |
// to handle this case. So do this ludicrous hack to accomplish | |
// this. This whole lump of un-followable-logic needs to be replaced | |
// by a complete rewriting of the wallet blinding logic. | |
if (blind_details->num_to_blind < 2) { | |
resetBlindDetails(blind_details, true /* don't wipe output data */); | |
if (!fillBlindDetails(blind_details, &wallet, txNew, selected_coins, error)) { | |
return false; | |
} | |
} | |
} | |
} | |
} | |
change_amount = 0; | |
nChangePosInOut = -1; | |
// Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those | |
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(tx_blinded), &wallet, &coin_control); | |
nBytes = tx_sizes.vsize; | |
fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes); | |
} | |
// The only time that fee_needed should be less than the amount available for fees (in change_and_fee - change_amount) is when | |
// we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug. | |
assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= map_change_and_fee.at(policyAsset) - change_amount); | |
// Update nFeeRet in case fee_needed changed due to dropping the change output | |
if (fee_needed <= map_change_and_fee.at(policyAsset) - change_amount) { | |
nFeeRet = map_change_and_fee.at(policyAsset) - change_amount; | |
} | |
// Reduce output values for subtractFeeFromAmount | |
if (coin_selection_params.m_subtract_fee_outputs) { | |
CAmount to_reduce = fee_needed + change_amount - map_change_and_fee.at(policyAsset); | |
int i = 0; | |
bool fFirst = true; | |
for (const auto& recipient : vecSend) | |
{ | |
if (i == nChangePosInOut) { | |
++i; | |
} | |
CTxOut& txout = txNew.vout[i]; | |
if (recipient.fSubtractFeeFromAmount) | |
{ | |
CAmount value = txout.nValue.GetAmount(); | |
if (recipient.asset != policyAsset) { | |
error = Untranslated(strprintf("Wallet does not support more than one type of fee at a time, therefore can not subtract fee from address amount, which is of a different asset id. fee asset: %s recipient asset: %s", policyAsset.GetHex(), recipient.asset.GetHex())); | |
return false; | |
} | |
value -= to_reduce / outputs_to_subtract_fee_from; // Subtract fee equally from each selected recipient | |
if (fFirst) // first receiver pays the remainder not divisible by output count | |
{ | |
fFirst = false; | |
value -= to_reduce % outputs_to_subtract_fee_from; | |
} | |
// Error if this output is reduced to be below dust | |
if (IsDust(txout, wallet.chain().relayDustFee())) { | |
if (value < 0) { | |
error = _("The transaction amount is too small to pay the fee"); | |
} else { | |
error = _("The transaction amount is too small to send after the fee has been deducted"); | |
} | |
return false; | |
} | |
txout.nValue = value; | |
} | |
++i; | |
} | |
nFeeRet = fee_needed; | |
} | |
// ELEMENTS: Give up if change keypool ran out and change is required | |
for (const auto& maybe_change_asset : change_pos) { | |
if (maybe_change_asset) { | |
auto used = mapScriptChange.extract(*maybe_change_asset); | |
if (used.mapped().second == dummy_script) { | |
return false; | |
} | |
} | |
} | |
// ELEMENTS update fee output | |
if (g_con_elementsmode) { | |
for (auto& txout : txNew.vout) { | |
if (txout.IsFee()) { | |
txout.nValue = nFeeRet; | |
break; | |
} | |
} | |
} | |
// ELEMENTS do actual blinding | |
if (blind_details) { | |
// Print blinded transaction info before we possibly blow it away when !sign. | |
std::string summary = "CreateTransaction created blinded transaction:\nIN: "; | |
for (unsigned int i = 0; i < selected_coins.size(); ++i) { | |
if (i > 0) { | |
summary += " "; | |
} | |
summary += strprintf("#%d: %s [%s] (%s [%s])\n", i, | |
selected_coins[i].value, | |
selected_coins[i].txout.nValue.IsExplicit() ? "explicit" : "blinded", | |
selected_coins[i].asset.GetHex(), | |
selected_coins[i].txout.nAsset.IsExplicit() ? "explicit" : "blinded" | |
); | |
} | |
summary += "OUT: "; | |
for (unsigned int i = 0; i < txNew.vout.size(); ++i) { | |
if (i > 0) { | |
summary += " "; | |
} | |
const CTxOut& unblinded = txNew.vout[i]; | |
summary += strprintf("#%d: %s%s [%s] (%s [%s])\n", i, | |
txNew.vout[i].IsFee() ? "[fee] " : "", | |
unblinded.nValue.GetAmount(), | |
blind_details->o_pubkeys[i].IsValid() ? "blinded" : "explicit", | |
unblinded.nAsset.GetAsset().GetHex(), | |
blind_details->o_pubkeys[i].IsValid() ? "blinded" : "explicit" | |
); | |
} | |
wallet.WalletLogPrintf(summary+"\n"); | |
// Wipe output blinding factors and start over | |
blind_details->o_amount_blinds.clear(); | |
blind_details->o_asset_blinds.clear(); | |
for (unsigned int i = 0; i < txNew.vout.size(); i++) { | |
blind_details->o_amounts[i] = txNew.vout[i].nValue.GetAmount(); | |
assert(blind_details->o_assets[i] == txNew.vout[i].nAsset.GetAsset()); | |
} | |
if (sign) { | |
int ret = BlindTransaction(blind_details->i_amount_blinds, blind_details->i_asset_blinds, blind_details->i_assets, blind_details->i_amounts, blind_details->o_amount_blinds, blind_details->o_asset_blinds, blind_details->o_pubkeys, issuance_asset_keys, issuance_token_keys, txNew); | |
assert(ret != -1); | |
if (ret != blind_details->num_to_blind) { | |
wallet.WalletLogPrintf("ERROR: tried to blind %d outputs but only blinded %d\n", (int) blind_details->num_to_blind, (int) ret); | |
error = _("Unable to blind the transaction properly. This should not happen."); | |
return false; | |
} | |
} | |
} | |
// Release any change keys that we didn't use. | |
for (const auto& it : mapScriptChange) { | |
int index = it.second.first; | |
if (index < 0) { | |
continue; | |
} | |
reservedest[index]->ReturnDestination(); | |
} | |
if (sign) { | |
if (!wallet.SignTransaction(txNew)) { | |
error = _("Signing transaction failed"); | |
return false; | |
} | |
} | |
// Normalize the witness in case it is not serialized before mempool | |
if (!txNew.HasWitness()) { | |
txNew.witness.SetNull(); | |
} | |
// Return the constructed transaction data. | |
tx = MakeTransactionRef(std::move(txNew)); | |
// Limit size | |
if ((sign && GetTransactionWeight(*tx) > MAX_STANDARD_TX_WEIGHT) || | |
(!sign && tx_sizes.weight > MAX_STANDARD_TX_WEIGHT)) | |
{ | |
error = _("Transaction too large"); | |
return false; | |
} | |
if (nFeeRet > wallet.m_default_max_tx_fee) { | |
error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED); | |
return false; | |
} | |
if (gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS)) { | |
// Lastly, ensure this tx will pass the mempool's chain limits | |
if (!wallet.chain().checkChainLimits(tx)) { | |
error = _("Transaction has too long of a mempool chain"); | |
return false; | |
} | |
} | |
// Before we return success, we assume any change key will be used to prevent | |
// accidental re-use. | |
for (auto& reservedest_ : reservedest) { | |
reservedest_->KeepDestination(); | |
} | |
fee_calc_out = feeCalc; | |
wallet.WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n", | |
nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay, | |
feeCalc.est.pass.start, feeCalc.est.pass.end, | |
(feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0, | |
feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool, | |
feeCalc.est.fail.start, feeCalc.est.fail.end, | |
(feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0, | |
feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool); | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment