Created
August 16, 2025 15:53
-
-
Save losh11/6f2de2eaf4ca7686a3ae3f6e6e90498a to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| /** | |
| * 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