From fb32c3f73d287bb39830c7850e96523d2582770f Mon Sep 17 00:00:00 2001 From: Jim Posen Date: Mon, 25 Sep 2017 16:35:19 -0700 Subject: [PATCH] lnwallet: Create type to estimate transaction weights. The new TxWeightEstimator type can be used to replace the fee estimation code for funding transactions and transactions in breacharbiter. --- lnwallet/size.go | 96 +++++++++++++++++++++++++++++++-- lnwallet/size_test.go | 123 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 lnwallet/size_test.go diff --git a/lnwallet/size.go b/lnwallet/size.go index 55c956c3..45c110a4 100644 --- a/lnwallet/size.go +++ b/lnwallet/size.go @@ -2,9 +2,16 @@ package lnwallet import ( "github.com/roasbeef/btcd/blockchain" + "github.com/roasbeef/btcd/wire" ) const ( + // witnessScaleFactor determines the level of "discount" witness data + // receives compared to "base" data. A scale factor of 4, denotes that + // witness data is 1/4 as cheap as regular non-witness data. Value copied + // here for convenience. + witnessScaleFactor = blockchain.WitnessScaleFactor + // The weight(weight), which is different from the !size! (see BIP-141), // is calculated as: // Weight = 4 * BaseSize + WitnessSize (weight). @@ -18,6 +25,12 @@ const ( // - WitnessScriptSHA256: 32 bytes P2WSHSize = 1 + 1 + 32 + // P2PKHOutputSize 34 bytes + // - value: 8 bytes + // - var_int: 1 byte (pkscript_length) + // - pkscript (p2pkh): 25 bytes + P2PKHOutputSize = 8 + 1 + 25 + // P2WKHOutputSize 31 bytes // - value: 8 bytes // - var_int: 1 byte (pkscript_length) @@ -36,12 +49,20 @@ const ( // - PublicKeyHASH160: 20 bytes P2WPKHSize = 1 + 1 + 20 - // P2WKHWitnessSize 108 bytes + // P2PKHScriptSigSize 108 bytes // - OP_DATA: 1 byte (signature length) // - signature // - OP_DATA: 1 byte (pubkey length) // - pubkey - P2WKHWitnessSize = 1 + 73 + 1 + 33 + P2PKHScriptSigSize = 1 + 73 + 1 + 33 + + // P2WKHWitnessSize 109 bytes + // - number_of_witness_elements: 1 byte + // - signature_length: 1 byte + // - signature + // - pubkey_length: 1 byte + // - pubkey + P2WKHWitnessSize = 1 + 1 + 73 + 1 + 33 // MultiSigSize 71 bytes // - OP_2: 1 byte @@ -106,6 +127,11 @@ const ( // - Marker: 1 byte WitnessHeaderSize = 1 + 1 + // BaseTxSize 8 bytes + // - Version: 4 bytes + // - LockTime: 4 bytes + BaseTxSize = 4 + 4 + // BaseSweepTxSize 42 + 41 * num-swept-inputs bytes // - Version: 4 bytes // - WitnessHeader <---- part of the witness data @@ -134,13 +160,13 @@ const ( CommitmentDelayOutput + CommitmentKeyHashOutput + 4 // BaseCommitmentTxWeight 500 weight - BaseCommitmentTxWeight = blockchain.WitnessScaleFactor * BaseCommitmentTxSize + BaseCommitmentTxWeight = witnessScaleFactor * BaseCommitmentTxSize // WitnessCommitmentTxWeight 224 weight WitnessCommitmentTxWeight = WitnessHeaderSize + WitnessSize // HTLCWeight 172 weight - HTLCWeight = blockchain.WitnessScaleFactor * HTLCSize + HTLCWeight = witnessScaleFactor * HTLCSize // HtlcTimeoutWeight is the weight of the HTLC timeout transaction // which will transition an outgoing HTLC to the delay-and-claim state. @@ -301,3 +327,65 @@ func estimateCommitTxWeight(count int, prediction bool) int64 { return htlcWeight + baseWeight + witnessWeight } + +// TxWeightEstimator is able to calculate weight estimates for transactions +// based on the input and output types. For purposes of estimation, all +// signatures are assumed to be of the maximum possible size, 73 bytes. +type TxWeightEstimator struct { + hasWitness bool + inputCount uint32 + outputCount uint32 + inputSize int + inputWitnessSize int + outputSize int +} + +// AddP2PKHInput updates the weight estimate to account for an additional input +// spending a P2PKH output. +func (twe *TxWeightEstimator) AddP2PKHInput() { + twe.inputSize += InputSize + P2PKHScriptSigSize + twe.inputWitnessSize++ + twe.inputCount++ +} + +// AddP2WKHInput updates the weight estimate to account for an additional input +// spending a P2PWKH output. +func (twe *TxWeightEstimator) AddP2WKHInput() { + twe.inputSize += InputSize + twe.inputWitnessSize += P2WKHWitnessSize + twe.inputCount++ + twe.hasWitness = true +} + +// AddP2PKHOutput updates the weight estimate to account for an additional P2PKH +// output. +func (twe *TxWeightEstimator) AddP2PKHOutput() { + twe.outputSize += P2PKHOutputSize + twe.outputCount++ +} + +// AddP2WKHOutput updates the weight estimate to account for an additional P2WKH +// output. +func (twe *TxWeightEstimator) AddP2WKHOutput() { + twe.outputSize += P2WKHOutputSize + twe.outputCount++ +} + +// AddP2WSHOutput updates the weight estimate to account for an additional P2WSH +// output. +func (twe *TxWeightEstimator) AddP2WSHOutput() { + twe.outputSize += P2WSHOutputSize + twe.outputCount++ +} + +// Weight gets the estimated weight of the transaction. +func (twe *TxWeightEstimator) Weight() int { + txSizeStripped := BaseTxSize + + wire.VarIntSerializeSize(uint64(twe.inputCount)) + twe.inputSize + + wire.VarIntSerializeSize(uint64(twe.outputCount)) + twe.outputSize + weight := txSizeStripped * witnessScaleFactor + if twe.hasWitness { + weight += WitnessHeaderSize + twe.inputWitnessSize + } + return weight +} diff --git a/lnwallet/size_test.go b/lnwallet/size_test.go new file mode 100644 index 00000000..95296fdd --- /dev/null +++ b/lnwallet/size_test.go @@ -0,0 +1,123 @@ +package lnwallet_test + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/roasbeef/btcd/blockchain" + "github.com/roasbeef/btcd/chaincfg" + "github.com/roasbeef/btcd/txscript" + "github.com/roasbeef/btcd/wire" + "github.com/roasbeef/btcutil" +) + +// TestTxWeightEstimator tests that transaction weight estimates are calculated +// correctly by comparing against an actual (though invalid) transaction +// matching the template. +func TestTxWeightEstimator(t *testing.T) { + netParams := &chaincfg.MainNetParams + + p2pkhAddr, err := btcutil.NewAddressPubKeyHash( + make([]byte, 20), netParams) + if err != nil { + t.Fatalf("Failed to generate address: %v", err) + } + p2pkhScript, err := txscript.PayToAddrScript(p2pkhAddr) + if err != nil { + t.Fatalf("Failed to generate scriptPubKey: %v", err) + } + + p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash( + make([]byte, 20), netParams) + if err != nil { + t.Fatalf("Failed to generate address: %v", err) + } + p2wkhScript, err := txscript.PayToAddrScript(p2wkhAddr) + if err != nil { + t.Fatalf("Failed to generate scriptPubKey: %v", err) + } + + p2wshAddr, err := btcutil.NewAddressWitnessScriptHash( + make([]byte, 32), netParams) + if err != nil { + t.Fatalf("Failed to generate address: %v", err) + } + p2wshScript, err := txscript.PayToAddrScript(p2wshAddr) + if err != nil { + t.Fatalf("Failed to generate scriptPubKey: %v", err) + } + + testCases := []struct { + numP2PKHInputs int + numP2WKHInputs int + numP2PKHOutputs int + numP2WKHOutputs int + numP2WSHOutputs int + }{ + { + numP2PKHInputs: 1, + numP2PKHOutputs: 2, + }, + { + numP2PKHInputs: 1, + numP2WKHInputs: 1, + numP2WKHOutputs: 1, + numP2WSHOutputs: 1, + }, + { + numP2WKHInputs: 1, + numP2WKHOutputs: 1, + numP2WSHOutputs: 1, + }, + { + numP2WKHInputs: 2, + numP2WKHOutputs: 1, + numP2WSHOutputs: 1, + }, + } + + for i, test := range testCases { + var weightEstimate lnwallet.TxWeightEstimator + tx := wire.NewMsgTx(1) + + for j := 0; j < test.numP2PKHInputs; j++ { + weightEstimate.AddP2PKHInput() + + signature := make([]byte, 73) + compressedPubKey := make([]byte, 33) + scriptSig, err := txscript.NewScriptBuilder().AddData(signature). + AddData(compressedPubKey).Script() + if err != nil { + t.Fatalf("Failed to generate scriptSig: %v", err) + } + + tx.AddTxIn(&wire.TxIn{SignatureScript: scriptSig}) + } + for j := 0; j < test.numP2WKHInputs; j++ { + weightEstimate.AddP2WKHInput() + + signature := make([]byte, 73) + compressedPubKey := make([]byte, 33) + witness := wire.TxWitness{signature, compressedPubKey} + tx.AddTxIn(&wire.TxIn{Witness: witness}) + } + for j := 0; j < test.numP2PKHOutputs; j++ { + weightEstimate.AddP2PKHOutput() + tx.AddTxOut(&wire.TxOut{PkScript: p2pkhScript}) + } + for j := 0; j < test.numP2WKHOutputs; j++ { + weightEstimate.AddP2WKHOutput() + tx.AddTxOut(&wire.TxOut{PkScript: p2wkhScript}) + } + for j := 0; j < test.numP2WSHOutputs; j++ { + weightEstimate.AddP2WSHOutput() + tx.AddTxOut(&wire.TxOut{PkScript: p2wshScript}) + } + + expectedWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) + if weightEstimate.Weight() != int(expectedWeight) { + t.Errorf("Case %d: Got wrong weight: expected %d, got %d", + i, expectedWeight, weightEstimate.Weight()) + } + } +}