Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion REPO_MAP_DIGIDOLLAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ This is the granular file index for all DigiDollar and Oracle source code. Read
- Global `g_scriptMetadataMap` protected by `RecursiveMutex`, capped at 10,000 entries

### src/digidollar/txbuilder.h
- `DigiDollar::MIN_DD_FEE_RATE` = 35,000,000 sat/kvB (0.35 DGB/kvB) → the DigiDollar fee rate; shared by mint/transfer/redeem RPCs and wallet coin selection
- `DigiDollar::MIN_DD_TX_FEE` = 10,000,000 sat (0.1 DGB) → absolute fee floor for any DigiDollar transaction
- `DigiDollar::TxBuilderResult` (struct) → result of tx building: success, CMutableTransaction, error string, totalFees, collateralRequired, ddChange
- `DigiDollar::TxBuilderMintParams` (struct) → ddAmount, lockDays, lockTier (0-9), ownerKey, feeRate, utxos, optional dgbChangeDest
- `DigiDollar::TxBuilderTransferParams` (struct) → recipients vector (address, amount), feeRate, ddUtxos, ddAmounts, feeUtxos, feeAmounts, spenderKey, optional dgbChangeDest
Expand Down Expand Up @@ -127,6 +129,7 @@ This is the granular file index for all DigiDollar and Oracle source code. Read
- `CreateRedemptionScript(path, owner)` → creates Schnorr-signed redemption script
- `DigiDollar::EncodeDigiDollarAddress(dest, chainParams)` → converts CTxDestination to DD address string via CDigiDollarAddress
- `DigiDollar::EstimateTransactionVSize(tx)` → estimates vsize with 110-byte witness per input + 35% safety margin
- `DigiDollar::EstimateInputSpendCost(feeRate)` → approximate fee cost of one extra input (92 vB marginal measured against EstimateTransactionVSize at zero inputs → 3,220,000 sat at MIN_DD_FEE_RATE); a fee UTXO worth less has negative effective value. Approximation only: the estimator's double truncation makes the true marginal alternate 92/93 vB, absorbed by the redemption re-projection loop. Returns 0 for a non-positive rate, MAX_MONEY on overflow

### src/digidollar/txbuilder.cpp
- Full implementation of all TxBuilder classes (~1,425 lines)
Expand Down Expand Up @@ -708,7 +711,8 @@ This is the granular file index for all DigiDollar and Oracle source code. Read
- `ProcessIncomingTransaction(tx, txid)` → processes any incoming DD tx and adds to history
- **Coin Selection:**
- `SelectDDCoins(target, selected_utxos, selected_total, amounts)` → selects DD UTXOs for target amount
- `SelectFeeCoins(fee_amount, selected_utxos, selected_total, amounts, exclude)` → selects DGB UTXOs for fees
- `SelectFeeCoins(fee_amount, selected_utxos, selected_total, amounts, exclude, minimize_inputs=false, fee_rate=MIN_DD_FEE_RATE)` → selects DGB UTXOs for fees. Prices candidates by EFFECTIVE value (value − `EstimateInputSpendCost(fee_rate)`): UTXOs that cost at least as much to spend as they are worth are skipped, and `fee_amount` must be met by the sum of effective values, not the raw sum (`selected_total` is still the raw total). Smallest-first by default (spends small UTXOs down, at the cost of a larger fee); `minimize_inputs=true` sorts largest-first for the fewest inputs
- `SelectRedemptionFeeCoins(params, error, projected_fee)` → `DDFeeSelectionResult` {OK, INSUFFICIENT_FUNDS, INVALID_TRANSACTION}. Fee-input selection for redemptions: excludes the collateral outpoint and the DD UTXOs being burned, then converges (≤6 rounds) by projecting the redemption tx, deriving the real fee from `EstimateTransactionVSize`, and re-selecting against it; enforces MAX_STANDARD_TX_WEIGHT and the MIN_DD_TX_FEE floor. Fills `params.feeUtxos`/`feeAmounts`; `projected_fee` is an upper bound
- `CalculateTransactionFee(tx)` → estimates fee for transaction
- **Utility:**
- `IsLockedByDD(outpoint)` → checks if outpoint is locked by DD (protects from UnlockAllCoins)
Expand Down
38 changes: 35 additions & 3 deletions src/digidollar/txbuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ static const size_t ESTIMATED_TX_VSIZE = 500; // Estimated transaction size
static const int DEFAULT_SYSTEM_COLLATERAL = 150; // Default system health (150%)
static const double MAX_FEE_RATIO = 0.5; // Maximum fee as ratio of total input
static const size_t MAX_TX_INPUTS = 400; // Maximum inputs per transaction to stay under MAX_STANDARD_TX_WEIGHT
static const CAmount MIN_DD_TX_FEE = 10000000; // 0.1 DGB minimum DD transaction fee

CAmount ApplyCollateralSafetyMargin(CAmount requiredCollateral)
{
Expand Down Expand Up @@ -1295,14 +1294,24 @@ TxBuilderResult RedeemTxBuilder::BuildRedemptionTransaction(const TxBuilderRedee
LogPrintf("DigiDollar: Calculated fees: %d sats (fee inputs: %d sats)\n", result.totalFees, totalFeeIn);

if (totalFeeIn <= 0) {
result.error = "Insufficient fee inputs for DD redemption fee";
result.error = strprintf("Insufficient fee inputs for DD redemption fee: no DGB fee input was supplied "
"for a transaction that owes %lld sats. Fund the wallet with spendable DGB and retry.",
static_cast<long long>(result.totalFees));
LogPrintf("DigiDollar: BuildRedemptionTransaction FAILED - %s\n", result.error);
return result;
}

CAmount feeChange = totalFeeIn - result.totalFees;
if (feeChange < 0) {
result.error = "Insufficient fee inputs for DD redemption fee";
// Every fee input costs a fee of its own to spend, so a pile of small
// UTXOs can total more than the fee and still not pay it.
result.error = strprintf("Insufficient fee inputs for DD redemption fee: %u fee input(s) totalling %lld sats "
"against a %lld sat fee (each extra fee input costs about %lld sats to spend). "
"Consolidate small DGB UTXOs into fewer, larger ones and retry.",
static_cast<unsigned>(params.feeUtxos.size()),
static_cast<long long>(totalFeeIn),
static_cast<long long>(result.totalFees),
static_cast<long long>(EstimateInputSpendCost(params.feeRate)));
LogPrintf("DigiDollar: BuildRedemptionTransaction FAILED - %s\n", result.error);
return result;
}
Expand Down Expand Up @@ -1490,4 +1499,27 @@ size_t EstimateTransactionVSize(const CMutableTransaction& tx) {
return vsize + (vsize * 35 / 100);
}

namespace {
//! Marginal vsize of one extra input, measured against EstimateTransactionVSize()
//! at zero inputs. The estimator truncates twice, so the true marginal alternates
//! between 92 and 93 vB with the size of the rest of the transaction; this is the
//! lower of the two and therefore an approximation, not a bound. See
//! EstimateInputSpendCost() in the header.
size_t EstimateInputVSize() {
CMutableTransaction probe;
const size_t without_input = EstimateTransactionVSize(probe);
probe.vin.emplace_back();
const size_t with_input = EstimateTransactionVSize(probe);
return with_input - without_input;
}
} // namespace

CAmount EstimateInputSpendCost(CAmount feeRate) {
if (feeRate <= 0) return 0;
const CAmount vsize = static_cast<CAmount>(EstimateInputVSize());
// Fee rates reach this from RPC parameters, so guard the multiply.
if (feeRate > std::numeric_limits<CAmount>::max() / vsize) return MAX_MONEY;
return (vsize * feeRate) / 1000;
}

} // namespace DigiDollar
32 changes: 32 additions & 0 deletions src/digidollar/txbuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@

namespace DigiDollar {

/**
* Minimum fee rate for DigiDollar transactions, in satoshis per kvB
* (0.35 DGB/kvB, i.e. 35,000 sat/vB). DigiByte expresses fee rates per
* kilo-vbyte, not per vbyte.
*/
static constexpr CAmount MIN_DD_FEE_RATE{35000000};

/** Absolute fee floor for any DigiDollar transaction (0.1 DGB). */
static constexpr CAmount MIN_DD_TX_FEE{10000000};

// Apply the wallet mint collateral safety margin used by MintTxBuilder.
// The input and output are DGB satoshis.
CAmount ApplyCollateralSafetyMargin(CAmount requiredCollateral);
Expand Down Expand Up @@ -313,6 +323,28 @@ std::string EncodeDigiDollarAddress(const CTxDestination& dest, const CChainPara
*/
size_t EstimateTransactionVSize(const CMutableTransaction& tx);

/**
* Approximate fee that spending one additional input costs at the given fee rate.
*
* A fee UTXO worth less than this has negative effective value: adding it to a
* transaction reduces, rather than increases, the amount available to pay the
* fee. Coin selection for DigiDollar fee inputs prices inputs with this.
*
* The marginal input size is measured against EstimateTransactionVSize() at zero
* inputs and comes out at 92 vB (41 base bytes plus the flat 110-byte witness
* allowance, i.e. 68.5 vB, carrying the estimator's 35% margin). It is an
* approximation, not an exact per-input cost: EstimateTransactionVSize()
* truncates twice, so the true marginal alternates between 92 and 93 vB
* depending on the size of the rest of the transaction. Callers that must not
* come up short re-project the transaction and re-select (see
* DigiDollarWallet::SelectRedemptionFeeCoins), which absorbs the difference.
*
* @param feeRate Fee rate in satoshis per kvB
* @return Cost in satoshis of spending one extra input, 0 for a non-positive
* fee rate, MAX_MONEY if the fee rate is large enough to overflow
*/
CAmount EstimateInputSpendCost(CAmount feeRate);

} // namespace DigiDollar

#endif // DIGIBYTE_DIGIDOLLAR_TXBUILDER_H
53 changes: 22 additions & 31 deletions src/rpc/digidollar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1308,9 +1308,8 @@ RPCHelpMan mintdigidollar()
// MIN_DD_TX_FEE = 10,000,000 satoshis = 0.1 DGB
// For a typical 300-byte tx, we need feeRate = 10,000,000 / 300 * 1000 = 33,333,333 sat/kB
// We use 35,000,000 sat/kB to ensure minimum is always met
static const CAmount MIN_DD_FEE_RATE = 35000000; // 0.35 DGB/kB ensures min 0.1 DGB for typical tx
CAmount feeRate = OptionalParamIsSet(request, 2) ?
std::max(request.params[2].getInt<int64_t>(), MIN_DD_FEE_RATE) : MIN_DD_FEE_RATE;
std::max<CAmount>(request.params[2].getInt<int64_t>(), DigiDollar::MIN_DD_FEE_RATE) : DigiDollar::MIN_DD_FEE_RATE;

// Validate parameters
if (ddAmount <= 0) {
Expand Down Expand Up @@ -2307,8 +2306,7 @@ RPCHelpMan redeemdigidollar()
redeemParams.path = errRedemptionActive ? DigiDollar::RedemptionPath::ERR : DigiDollar::RedemptionPath::NORMAL;
redeemParams.ownerKey = ownerKey; // BUG #10 FIX: Use position owner key directly
// DigiDollar transactions MUST pay at least 0.1 DGB fee to miners
static const CAmount MIN_DD_FEE_RATE = 35000000; // 0.35 DGB/kB ensures min 0.1 DGB for typical tx
redeemParams.feeRate = MIN_DD_FEE_RATE;
redeemParams.feeRate = DigiDollar::MIN_DD_FEE_RATE; // 0.35 DGB/kB ensures min 0.1 DGB for typical tx

// Use the caller's requested DGB return address if supplied. If no
// address is supplied, create a wallet destination so the returned
Expand Down Expand Up @@ -2374,33 +2372,26 @@ RPCHelpMan redeemdigidollar()
LogPrintf(" - DD Minted: %d cents\n", foundPosition.dd_minted);
LogPrintf(" - Unlock Height: %d\n", foundPosition.unlock_height);

// Select fee UTXOs from wallet
// CRITICAL: Build exclude list to prevent selecting collateral or DD UTXOs as fee inputs
std::vector<COutPoint> exclude_utxos;
exclude_utxos.push_back(redeemParams.collateralOutpoint); // Don't select collateral
exclude_utxos.insert(exclude_utxos.end(), redeemParams.ddUtxos.begin(), redeemParams.ddUtxos.end()); // Don't select DD UTXOs

LogPrintf("DigiDollar: Building exclude list with %d UTXOs (1 collateral + %d DD)\n",
exclude_utxos.size(), redeemParams.ddUtxos.size());

// Bug #9 fix: Calculate fee from feeRate and estimated tx size instead of hardcoding.
// Redemption tx: ~3 inputs (collateral + DD + fee), ~2-3 outputs → ~400 vbytes.
// Apply 50% safety margin for script-path spending variance.
CAmount estimatedFee = (400 * redeemParams.feeRate) / 1000; // vsize * feeRate / 1000
estimatedFee = estimatedFee + (estimatedFee / 2); // 50% safety margin
if (estimatedFee < 10000000) estimatedFee = 10000000; // Floor at 0.1 DGB
LogPrintf("DigiDollar: Estimated redemption fee: %lld sats (%.8f DGB)\n",
static_cast<long long>(estimatedFee), estimatedFee / 100000000.0);
CAmount selectedFeeTotal = 0;
std::vector<CAmount> feeAmounts;

if (!dd_wallet->SelectFeeCoins(estimatedFee, redeemParams.feeUtxos, selectedFeeTotal, &feeAmounts, &exclude_utxos)) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient DGB balance for transaction fees");
}

redeemParams.feeAmounts = feeAmounts;
LogPrintf("DigiDollar: Selected %d sats in fees from %d UTXOs for redemption\n",
selectedFeeTotal, redeemParams.feeUtxos.size());
// Select fee UTXOs from the wallet. A fixed size guess cannot work
// here: the fee a redemption owes depends on how many fee inputs it
// ends up carrying, and each input costs a fee of its own to spend.
// SelectRedemptionFeeCoins() re-projects the transaction after every
// selection round, and excludes the collateral outpoint and the DD
// UTXOs being burned from the candidate set.
std::string feeSelectionError;
CAmount projectedFee = 0;
switch (dd_wallet->SelectRedemptionFeeCoins(redeemParams, feeSelectionError, &projectedFee)) {
case DDFeeSelectionResult::OK:
break;
case DDFeeSelectionResult::INSUFFICIENT_FUNDS:
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, feeSelectionError);
case DDFeeSelectionResult::INVALID_TRANSACTION:
// Not a funding problem: the redemption cannot be built as asked.
throw JSONRPCError(RPC_WALLET_ERROR, feeSelectionError);
}

LogPrintf("DigiDollar: Selected %zu fee UTXOs for redemption (projected fee at most %lld sats)\n",
redeemParams.feeUtxos.size(), static_cast<long long>(projectedFee));

DigiDollar::TxBuilderResult redeemResult = redeemBuilder.BuildRedemptionTransaction(redeemParams);

Expand Down
Loading
Loading