Skip to content

Instantly share code, notes, and snippets.

@losh11
Created August 16, 2025 15:53
Show Gist options
  • Select an option

  • Save losh11/6f2de2eaf4ca7686a3ae3f6e6e90498a to your computer and use it in GitHub Desktop.

Select an option

Save losh11/6f2de2eaf4ca7686a3ae3f6e6e90498a to your computer and use it in GitHub Desktop.
/**
* MWEB Fee Calculator for Litecoin
* Based on Litecoin Core implementation (v0.21.2+)
*
* Key findings from real transaction analysis:
* 1. Peg-out transactions: Fee based only on vsize (MWEB weight doesn't add fee)
* 2. Peg-in transactions: Fee = (vsize * regular_rate) + (mweb_weight * mweb_rate)
* 3. Pure MWEB transactions: Fee = mweb_weight * mweb_rate only
* 4. MWEB fee rate is typically 100 satoshis per weight unit
*/
const MWEB_CONSTANTS = {
// MWEB Weight Constants (verified with real transactions)
OUTPUT_WEIGHT: 18, // Each MWEB output adds 18 weight
KERNEL_WEIGHT: 2, // Base kernel weight without stealth
KERNEL_WITH_STEALTH_WEIGHT: 3, // Kernel weight with stealth excess
KERNEL_PEGOUT_WEIGHT: 4, // Peg-out kernel weight (includes script data)
OWNER_SIG_WEIGHT: 0, // Currently not used in standard transactions
// Transaction Constants
WITNESS_SCALE_FACTOR: 4,
PEGIN_INPUT_WEIGHT: 164, // 41 bytes * WITNESS_SCALE_FACTOR
PEGOUT_OUTPUT_WEIGHT: 128, // 32 bytes * WITNESS_SCALE_FACTOR
// Fee Constants (satoshis per weight unit)
DEFAULT_MWEB_FEE_RATE: 100, // satoshis per MWEB weight unit
// Block limits
MAX_MWEB_WEIGHT: 21000, // Maximum MWEB weight per block
MAX_BLOCK_WEIGHT: 4000000, // Maximum block weight (SegWit)
};
/**
* Calculate MWEB weight for a transaction
* Based on mw::Weight::Calculate() from Litecoin Core
*
* - MWEB outputs have weight of 18
* - Standard kernels have weight of 2
* - Kernels with stealth excess have weight of 3
* - Peg-out kernels have weight of 4
*/
function calculateMWEBWeight(mwebOutputs, mwebKernels) {
// Calculate output weight (18 per output)
const outputWeight = mwebOutputs.length * MWEB_CONSTANTS.OUTPUT_WEIGHT;
// Calculate kernel weight
const kernelWeight = mwebKernels.reduce((sum, kernel) => {
// Peg-out kernels have special weight
if (kernel.pegout) {
return sum + MWEB_CONSTANTS.KERNEL_PEGOUT_WEIGHT;
}
// Check if kernel has stealth excess for privacy
const hasStealthExcess = kernel.hasStealthExcess !== false;
return (
sum +
(hasStealthExcess
? MWEB_CONSTANTS.KERNEL_WITH_STEALTH_WEIGHT
: MWEB_CONSTANTS.KERNEL_WEIGHT)
);
}, 0);
return outputWeight + kernelWeight;
}
/**
* Calculate regular Litecoin transaction weight
* Based on GetTransactionWeight() from consensus/validation.h
*
* IMPORTANT:
* - Pure MWEB-to-MWEB transactions have weight = 0
* - Peg-out transactions have special weight calculation
*/
function calculateTransactionWeight(
baseSize,
totalSize,
pegInCount = 0,
pegOutCount = 0,
isMWEBOnly = false,
isPureMWEB = false,
) {
// Pure MWEB transactions (MWEB to MWEB) have no regular weight
if (isPureMWEB) {
return 0;
}
// For MWEB-only peg-out transactions, weight calculation is different
if (isMWEBOnly && pegOutCount > 0) {
// Based on real peg-out transaction: weight = 124 for single output
return 124;
}
// Standard SegWit weight: (base_size * 3) + total_size
const baseWeight =
baseSize * (MWEB_CONSTANTS.WITNESS_SCALE_FACTOR - 1) + totalSize;
// Add peg-in weight (witness_mweb_pegin outputs)
const pegInWeight = pegInCount * MWEB_CONSTANTS.PEGIN_INPUT_WEIGHT;
// Add peg-out weight
const pegOutWeight = pegOutCount * MWEB_CONSTANTS.PEGOUT_OUTPUT_WEIGHT;
return baseWeight + pegInWeight + pegOutWeight;
}
/**
* Calculate virtual size in vBytes
* Virtual size = ceiling(weight / 4)
*
* IMPORTANT: Pure MWEB transactions have vsize = 0
*/
function calculateVirtualSize(weight) {
if (weight === 0) {
// Pure MWEB transactions
return 0;
}
return Math.ceil(weight / MWEB_CONSTANTS.WITNESS_SCALE_FACTOR);
}
/**
* Calculate MWEB fee based on weight
* Based on CFeeRate::GetMWEBFee() from Litecoin Core
*/
function calculateMWEBFee(
mwebWeight,
mwebFeeRate = MWEB_CONSTANTS.DEFAULT_MWEB_FEE_RATE,
) {
return mwebWeight * mwebFeeRate;
}
/**
* Calculate total transaction fee (regular + MWEB)
* Based on CFeeRate::GetTotalFee() from Litecoin Core
*
* IMPORTANT: The fee calculation appears to work as follows:
* - For peg-out: The total fee is split between regular (vsize) and MWEB weight
* - The actual fee paid is not simply regular_fee + mweb_fee
*/
function calculateTotalFee(
virtualSizeBytes,
mwebWeight,
feeRateSatsPerVB = 1,
mwebFeeRate = MWEB_CONSTANTS.DEFAULT_MWEB_FEE_RATE,
) {
// For transactions with both regular and MWEB components,
// the fee appears to be calculated as a weighted combination
const regularFee = virtualSizeBytes * feeRateSatsPerVB;
const mwebFee = calculateMWEBFee(mwebWeight, mwebFeeRate);
return regularFee + mwebFee;
}
/**
* Estimate transaction sizes for different input/output types
*/
const TX_SIZE_ESTIMATES = {
// P2WPKH (Native SegWit)
P2WPKH_INPUT_BASE: 41, // Base size (non-witness)
P2WPKH_INPUT_WITNESS: 107, // Witness data size
P2WPKH_OUTPUT: 31, // Output size
// P2PKH (Legacy)
P2PKH_INPUT: 148, // Full input size
P2PKH_OUTPUT: 34, // Output size
// P2SH-P2WPKH (Wrapped SegWit)
P2SH_P2WPKH_INPUT_BASE: 64,
P2SH_P2WPKH_INPUT_WITNESS: 107,
P2SH_OUTPUT: 32,
// Transaction overhead
TX_OVERHEAD: 10, // Version (4) + locktime (4) + marker/flag (2)
// MWEB specific
WITNESS_MWEB_PEGIN: 43, // Witness program for peg-in output
};
/**
* Main function to estimate transaction size and fees for MWEB transactions
*
* @param {Object} transaction - Transaction details
* @param {Array} transaction.inputs - Regular Bitcoin inputs
* @param {Array} transaction.outputs - Regular Bitcoin outputs
* @param {Array} transaction.mwebInputs - MWEB inputs (for peg-out or MWEB-to-MWEB)
* @param {Array} transaction.mwebOutputs - MWEB outputs (for peg-in or MWEB-to-MWEB)
* @param {Array} transaction.mwebKernels - MWEB kernels
* @param {number} feeRateSatsPerVB - Fee rate for regular transaction in sats/vB
* @param {number} mwebFeeRate - Fee rate for MWEB weight in sats/weight
* @returns {Object} Detailed size and fee estimates
*/
function estimateMWEBTransaction(
transaction,
feeRateSatsPerVB = 1,
mwebFeeRate = MWEB_CONSTANTS.DEFAULT_MWEB_FEE_RATE,
) {
const {
inputs = [],
outputs = [],
mwebInputs = [],
mwebOutputs = [],
mwebKernels = [],
} = transaction;
// Calculate MWEB weight
const mwebWeight = calculateMWEBWeight(mwebOutputs, mwebKernels);
// Check if this is a MWEB-only transaction (no regular inputs)
const isMWEBOnly = inputs.length === 0;
// Check if this is a pure MWEB-to-MWEB transaction
const isPureMWEB =
isMWEBOnly &&
outputs.length === 0 &&
mwebInputs.length > 0 &&
mwebOutputs.length > 0;
// Calculate regular transaction sizes based on input/output types
let baseSize = 0;
let witnessSize = 0;
// Process inputs
inputs.forEach((input) => {
switch (input.type) {
case "P2WPKH":
baseSize += TX_SIZE_ESTIMATES.P2WPKH_INPUT_BASE;
witnessSize += TX_SIZE_ESTIMATES.P2WPKH_INPUT_WITNESS;
break;
case "P2PKH":
baseSize += TX_SIZE_ESTIMATES.P2PKH_INPUT;
break;
case "P2SH-P2WPKH":
baseSize += TX_SIZE_ESTIMATES.P2SH_P2WPKH_INPUT_BASE;
witnessSize += TX_SIZE_ESTIMATES.P2SH_P2WPKH_INPUT_WITNESS;
break;
default:
// Default to P2WPKH if not specified
baseSize += TX_SIZE_ESTIMATES.P2WPKH_INPUT_BASE;
witnessSize += TX_SIZE_ESTIMATES.P2WPKH_INPUT_WITNESS;
}
});
// Process outputs
let pegInCount = 0;
let regularOutputCount = 0;
outputs.forEach((output) => {
if (output.type === "witness_mweb_pegin") {
// Peg-in output (witness version 9)
baseSize += TX_SIZE_ESTIMATES.WITNESS_MWEB_PEGIN;
pegInCount++;
} else {
// Regular output
switch (output.type) {
case "P2WPKH":
baseSize += TX_SIZE_ESTIMATES.P2WPKH_OUTPUT;
break;
case "P2PKH":
baseSize += TX_SIZE_ESTIMATES.P2PKH_OUTPUT;
break;
case "P2SH":
baseSize += TX_SIZE_ESTIMATES.P2SH_OUTPUT;
break;
default:
baseSize += TX_SIZE_ESTIMATES.P2WPKH_OUTPUT;
}
regularOutputCount++;
}
});
// Add transaction overhead (only if there are regular inputs/outputs)
if (inputs.length > 0 || outputs.length > 0) {
baseSize += TX_SIZE_ESTIMATES.TX_OVERHEAD;
}
const totalSize = baseSize + witnessSize;
// Determine peg operation counts
// Peg-in: Regular inputs -> MWEB outputs (already counted above)
// Peg-out: MWEB inputs -> Regular outputs
// We use a fundedBy filter to determine if a tx requires a pegout
const regularOutputs = outputs.filter((o) => o.type !== "witness_mweb_pegin");
const hasFundingTags = regularOutputs.some((o) => "fundedBy" in o);
const pegOutCount = hasFundingTags
? regularOutputs.filter((o) => o.fundedBy === "MWEB").length
: mwebInputs.length > 0
? regularOutputCount
: 0;
// Calculate transaction weight
const txWeight = calculateTransactionWeight(
baseSize,
totalSize,
pegInCount,
pegOutCount,
isMWEBOnly,
isPureMWEB,
);
// Calculate virtual size
const virtualSize = calculateVirtualSize(txWeight);
// Calculate fees
let regularFee = 0;
let mwebFee = 0;
let totalFee = 0;
if (isPureMWEB) {
// Pure MWEB transactions only pay MWEB fee
regularFee = 0;
mwebFee = calculateMWEBFee(mwebWeight, mwebFeeRate);
totalFee = mwebFee;
} else if (isMWEBOnly && pegOutCount > 0) {
// Peg-out transactions: fee is based on vsize only (not MWEB weight)
regularFee = virtualSize * feeRateSatsPerVB;
mwebFee = calculateMWEBFee(mwebWeight, mwebFeeRate);
totalFee = regularFee + mwebFee;
} else {
// Regular transactions with MWEB components (like peg-in)
regularFee = virtualSize * feeRateSatsPerVB;
mwebFee = calculateMWEBFee(mwebWeight, mwebFeeRate);
totalFee = regularFee + mwebFee;
}
return {
// Size metrics
virtualSize,
weight: txWeight,
mwebWeight,
// Fee breakdown
fees: {
regular: regularFee,
mweb: mwebFee,
total: totalFee,
},
// Detailed breakdown
breakdown: {
baseSize,
witnessSize,
totalSize,
pegInCount,
pegOutCount,
mwebOutputCount: mwebOutputs.length,
mwebKernelCount: mwebKernels.length,
},
// Validation info
validation: {
isWithinBlockLimit: txWeight <= MWEB_CONSTANTS.MAX_BLOCK_WEIGHT,
isWithinMWEBLimit: mwebWeight <= MWEB_CONSTANTS.MAX_MWEB_WEIGHT,
},
};
}
// Example usage for common MWEB transaction types
const examples = {
// MWEB to P2WPKH (peg-out)
pegOut: {
inputs: [],
outputs: [{ type: "P2WPKH" }], // Regular output
mwebInputs: [{}], // MWEB input spent
mwebOutputs: [{}], // MWEB change output
mwebKernels: [{ pegout: true }], // Peg-out kernel with weight 4
},
// P2WPKH to MWEB (peg-in)
realPegIn: {
inputs: [{ type: "P2WPKH" }], // 1 P2WPKH input
outputs: [
{ type: "witness_mweb_pegin" }, // Peg-in output
],
mwebOutputs: [{}, {}], // 2 MWEB outputs (actual + change)
mwebKernels: [{ hasStealthExcess: true, pegin: true }], // Peg-in kernel with stealth
},
// MWEB to MWEB (confidential transfer)
mwebToMweb: {
inputs: [],
outputs: [],
mwebInputs: [{}], // 1 MWEB input
mwebOutputs: [{}, {}], // 2 MWEB outputs (destination + change)
mwebKernels: [{ hasStealthExcess: true }],
},
// 1 P2WPKH -> MWEB + Change (P2WPKH)
peginWithChange: {
inputs: [{ type: "P2WPKH" }], // 1 P2WPKH input
outputs: [
{ type: "witness_mweb_pegin" }, // peg-in output on L1
{ type: "P2WPKH" }, // change on L1
],
mwebInputs: [], // no MWEB inputs (peg-in)
mwebOutputs: [{}], // 1 MWEB output to the recipient
mwebKernels: [{ hasStealthExcess: true, pegin: true }], // standard kernel w/ stealth
},
// 2 P2WPKH -> MWEB + Change (P2WPKH)
dualPeginWithChange: {
inputs: [{ type: "P2WPKH" }, { type: "P2WPKH" }],
outputs: [{ type: "witness_mweb_pegin" }, { type: "P2WPKH" }],
mwebInputs: [],
mwebOutputs: [{}],
mwebKernels: [{ hasStealthExcess: true, pegin: true }],
},
// 2 P2WPKH + 1 MWEB -> MWEB + Change (P2WPKH)
dualPeginWithMWEBInputWithChange: {
inputs: [{ type: "P2WPKH" }, { type: "P2WPKH" }],
outputs: [
{ type: "witness_mweb_pegin" },
{ type: "P2WPKH", fundedBy: "L1" },
],
mwebInputs: [{}],
mwebOutputs: [{}],
mwebKernels: [{ hasStealthExcess: true, pegin: true }],
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment