Skip to content

Instantly share code, notes, and snippets.

@delta1
Created August 4, 2023 08:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save delta1/659416d0c273b9112cd03b5350cbe00e to your computer and use it in GitHub Desktop.
Save delta1/659416d0c273b9112cd03b5350cbe00e to your computer and use it in GitHub Desktop.
elements code sample
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