From 391d5cd401a540156eaafc7f37f018c7a92b6f84 Mon Sep 17 00:00:00 2001 From: Andrey Samokhvalov Date: Wed, 23 Nov 2016 11:29:05 +0300 Subject: [PATCH] lnwallet: add commitment transaction estimation --- lnwallet/channel.go | 58 ++++++++++------- lnwallet/channel_test.go | 92 ++++++++++++++++++++++++++ lnwallet/size.go | 136 +++++++++++++++++++++++++++++++++++++++ lnwire/error_generic.go | 4 -- 4 files changed, 264 insertions(+), 26 deletions(-) create mode 100644 lnwallet/size.go diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 1928a8ba..3f3e98e7 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -25,6 +25,10 @@ var ( ErrChanClosing = fmt.Errorf("channel is being closed, operation disallowed") ErrNoWindow = fmt.Errorf("unable to sign new commitment, the current" + " revocation window is exhausted") + ErrMaxWeightCost = fmt.Errorf("commitment transaction exceed max " + + "available weight") + ErrMaxHTLCNumber = fmt.Errorf("commitment transaction exceed max " + + "htlc number") ) const ( @@ -1321,7 +1325,6 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.CommitRevocation, // commitment, and a log compaction is attempted. In addition, a slice of // HTLC's which can be forwarded upstream are returned. func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ([]*PaymentDescriptor, error) { - lc.Lock() defer lc.Unlock() @@ -1736,24 +1739,9 @@ type ForceCloseSummary struct { SelfOutputSignDesc *SignDescriptor } -// ForceClose executes a unilateral closure of the transaction at the current -// lowest commitment height of the channel. Following a force closure, all -// state transitions, or modifications to the state update logs will be -// rejected. Additionally, this function also returns a ForceCloseSummary which -// includes the necessary details required to sweep all the time-locked within -// the commitment transaction. -// -// TODO(roasbeef): all methods need to abort if in dispute state -// TODO(roasbeef): method to generate CloseSummaries for when the remote peer -// does a unilateral close -func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { - lc.Lock() - defer lc.Unlock() - - // Set the channel state to indicate that the channel is now in a - // contested state. - lc.status = channelDispute - +// getSignedCommitTx function take the latest commitment transaction and populate +// it with witness data. +func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { // Fetch the current commitment transaction, along with their signature // for the transaction. commitTx := lc.channelState.OurCommitTx @@ -1773,9 +1761,35 @@ func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { // required to spend from the multi-sig output. ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed() theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed() - witness := SpendMultiSig(lc.FundingWitnessScript, ourKey, ourSig, - theirKey, theirSig) - commitTx.TxIn[0].Witness = witness + + commitTx.TxIn[0].Witness = SpendMultiSig(lc.FundingWitnessScript, ourKey, + ourSig, theirKey, theirSig) + + return commitTx, nil +} + +// ForceClose executes a unilateral closure of the transaction at the current +// lowest commitment height of the channel. Following a force closure, all +// state transitions, or modifications to the state update logs will be +// rejected. Additionally, this function also returns a ForceCloseSummary which +// includes the necessary details required to sweep all the time-locked within +// the commitment transaction. +// +// TODO(roasbeef): all methods need to abort if in dispute state +// TODO(roasbeef): method to generate CloseSummaries for when the remote peer +// does a unilateral close +func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) { + lc.Lock() + defer lc.Unlock() + + // Set the channel state to indicate that the channel is now in a + // contested state. + lc.status = channelDispute + + commitTx, err := lc.getSignedCommitTx() + if err != nil { + return nil, err + } // Locate the output index of the delayed commitment output back to us. // We'll return the details of this output to the caller so they can diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 580e0622..9b1a7b05 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -9,10 +9,12 @@ import ( "github.com/btcsuite/fastsha256" "github.com/davecgh/go-spew/spew" + "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/elkrem" "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/blockchain" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcd/txscript" @@ -253,6 +255,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: aliceCommitTx, + OurCommitSig: bytes.Repeat([]byte{1}, 71), FundingOutpoint: prevOut, OurMultiSigKey: aliceKeyPub, TheirMultiSigKey: bobKeyPub, @@ -276,6 +279,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan OurBalance: channelBal, TheirBalance: channelBal, OurCommitTx: bobCommitTx, + OurCommitSig: bytes.Repeat([]byte{1}, 71), FundingOutpoint: prevOut, OurMultiSigKey: bobKeyPub, TheirMultiSigKey: aliceKeyPub, @@ -592,6 +596,94 @@ func TestSimpleAddSettleWorkflow(t *testing.T) { } } +// TestCheckCommitTxSize checks that estimation size of commitment +// transaction with some degree of error corresponds to the actual size. +func TestCheckCommitTxSize(t *testing.T) { + checkSize := func(channel *LightningChannel, count int) { + // Due to variable size of the signatures (71-73) we may have + // an estimation error. + BaseCommitmentTxSizeEstimationError := 4 + + commitTx, err := channel.getSignedCommitTx() + if err != nil { + t.Fatalf("unable to initiate alice force close: %v", err) + } + + actualCost := blockchain.GetMsgTxCost(commitTx) + estimatedCost := estimateCommitTxCost(count, false) + + diff := int(estimatedCost - actualCost) + if 0 > diff || BaseCommitmentTxSizeEstimationError < diff { + t.Fatalf("estimation is wrong") + } + + } + + createHTLC := func(i int) (*lnwire.HTLCAddRequest, [32]byte) { + preimage := bytes.Repeat([]byte{byte(i)}, 32) + paymentHash := fastsha256.Sum256(preimage) + + var returnPreimage [32]byte + copy(returnPreimage[:], preimage) + + return &lnwire.HTLCAddRequest{ + RedemptionHashes: [][32]byte{paymentHash}, + Amount: lnwire.CreditsAmount(1e7), + Expiry: uint32(5), + }, returnPreimage + } + + // Create a test channel which will be used for the duration of this + // unittest. The channel will be funded evenly with Alice having 5 BTC, + // and Bob having 5 BTC. + aliceChannel, bobChannel, cleanUp, err := createTestChannels(3) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // Check that weight estimation of the commitment transaction without + // HTLCs is right. + checkSize(aliceChannel, 0) + checkSize(bobChannel, 0) + + // Adding HTLCs and check that size stays in allowable estimation + // error window. + for i := 1; i <= 10; i++ { + htlc, _ := createHTLC(i) + + if _, err := aliceChannel.AddHTLC(htlc); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + t.Fatalf("bob unable to receive htlc: %v", err) + } + + forceStateTransition(aliceChannel, bobChannel) + checkSize(aliceChannel, i) + checkSize(bobChannel, i) + } + + // Settle HTLCs and check that estimation is counting cost of settle + // HTLCs properly. + for i := 10; i >= 1; i-- { + _, preimage := createHTLC(i) + + settleIndex, err := bobChannel.SettleHTLC(preimage) + if err != nil { + t.Fatalf("bob unable to settle inbound htlc: %v", err) + } + err = aliceChannel.ReceiveHTLCSettle(preimage, settleIndex) + if err != nil { + t.Fatalf("alice unable to accept settle of outbound htlc: %v", err) + } + + forceStateTransition(aliceChannel, bobChannel) + checkSize(aliceChannel, i-1) + checkSize(bobChannel, i-1) + } +} + func TestCooperativeChannelClosure(t *testing.T) { // Create a test channel which will be used for the duration of this // unittest. The channel will be funded evenly with Alice having 5 BTC, diff --git a/lnwallet/size.go b/lnwallet/size.go new file mode 100644 index 00000000..1c13bd2f --- /dev/null +++ b/lnwallet/size.go @@ -0,0 +1,136 @@ +package lnwallet + +import ( + "github.com/roasbeef/btcd/blockchain" +) + +const ( + WitnessFactor = blockchain.WitnessScaleFactor + MaxTransactionWeightPolicy = blockchain.MaxBlockCost / 10 + + // The weight(cost), which is different from the !size! (see BIP-141), + // is calculated as: + // Weight = 4 * BaseSize + WitnessSize (weight). + // BaseSize - size of the transaction without witness data (bytes). + // WitnessSize - witness size (bytes). + // Weight - the metric for determining the cost of the transaction. + + // P2WSH: 34 bytes + // - OP_0: 1 byte + // - OP_DATA: 1 byte (WitnessScriptSHA256 length) + // - WitnessScriptSHA256: 32 bytes + P2WSHSize = 1 + 1 + 32 + + // P2PKH: 22 bytes + // - OP_0: 1 byte + // - OP_DATA: 1 byte (PublicKeyHASH160 length) + // - PublicKeyHASH160: 20 bytes + P2WPKHSize = 1 + 1 + 20 + + // MultiSig: 71 bytes + // - OP_2: 1 byte + // - OP_DATA: 1 byte (pubKeyAlice length) + // - pubKeyAlice: 33 bytes + // - OP_DATA: 1 byte (pubKeyBob length) + // - pubKeyBob: 33 bytes + // - OP_2: 1 byte + // - OP_CHECKMULTISIG: 1 byte + MultiSigSize = 1 + 1 + 33 + 1 + 33 + 1 + 1 + + // Witness: 222 bytes + // - NumberOfWitnessElements: 1 byte + // - NilLength: 1 byte + // - sigAliceLength: 1 byte + // - sigAlice: 73 bytes + // - sigBobLength: 1 byte + // - sigBob: 73 bytes + // - WitnessScriptLength: 1 byte + // - WitnessScript (MultiSig) + WitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + MultiSigSize + + // FundingInput: 41 bytes + // - PreviousOutPoint: + // - Hash: 32 bytes + // - Index: 4 bytes + // - OP_DATA: 1 byte (ScriptSigLength) + // - ScriptSig: 0 bytes + // - Witness <---- we use "Witness" instead of "ScriptSig" for + // transaction validation, but "Witness" is stored + // separately and cost for it size is smaller. So + // we separate the calculation of ordinary data + // from witness data. + // - Sequence: 4 bytes + FundingInputSize = 32 + 4 + 1 + 4 + + // OutputPayingToUs: 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2WSH) + CommitmentDelayOutput = 8 + 1 + P2WSHSize + + // OutputPayingToThem: 31 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2WPKH) + CommitmentKeyHashOutput = 8 + 1 + P2WPKHSize + + // HTLCOutput: 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (PW2SH) + HTLCSize = 8 + 1 + P2WSHSize + + // WitnessHeader: 2 bytes + // - Flag: 1 byte + // - Marker: 1 byte + WitnessHeaderSize = 1 + 1 + + // CommitmentTransaction: 125 bytes + // - Version: 4 bytes + // - WitnessHeader <---- part of the witness data + // - CountTxIn: 1 byte + // - TxIn: + // FundingInput + // - CountTxOut: 1 byte + // - TxOut: + // OutputPayingToThem, + // OutputPayingToUs, + // ....HTLCOutputs... + // - LockTime: 4 bytes + BaseCommitmentTxSize = 4 + 1 + FundingInputSize + 1 + + CommitmentDelayOutput + CommitmentKeyHashOutput + 4 + + // CommitmentTransactionCost: 500 weight + BaseCommitmentTxCost = WitnessFactor * BaseCommitmentTxSize + + // WitnessCommitmentTxCost: 224 weight + WitnessCommitmentTxCost = WitnessHeaderSize + WitnessSize + + // HTLCCost: 172 weight + HTLCCost = WitnessFactor * HTLCSize + + // MaxHTLCNumber shows as the maximum number HTLCs which can be + // included in commitment transaction. This numbers was calculated by + // Rusty Russel in "BOLT #5: Recommendations for On-chain Transaction + // Handling", based on the fact that we need to sweep all HTLCs within + // one penalty transaction. + MaxHTLCNumber = 1253 +) + + +// estimateCommitTxCost estimate commitment transaction cost depending on the +// precalculated cost of base transaction, witness data, which is needed for +// paying for funding tx, and htlc cost multiplied by their count. +func estimateCommitTxCost(count int, prediction bool) int64 { + // Make prediction about the size of commitment transaction with + // additional HTLC. + if prediction { + count++ + } + + htlcCost := int64(count * HTLCCost) + baseCost := int64(BaseCommitmentTxCost) + witnessCost := int64(WitnessCommitmentTxCost) + + return htlcCost + baseCost + witnessCost +} \ No newline at end of file diff --git a/lnwire/error_generic.go b/lnwire/error_generic.go index d0b6fe88..0c49dc82 100644 --- a/lnwire/error_generic.go +++ b/lnwire/error_generic.go @@ -15,10 +15,6 @@ const ( // ErrorMaxPendingChannels is returned by remote peer when the number // of active pending channels exceeds their maximum policy limit. ErrorMaxPendingChannels ErrorCode = 1 - - // ErrorMaxTransactionWeight is returned by remote peer when transaction - // weight exceed maximum allowable value. - ErrorMaxTransactionWeight ErrorCode = 2 ) // ErrorGeneric represents a generic error bound to an exact channel. The