lnwallet: add commitment transaction estimation

This commit is contained in:
Andrey Samokhvalov 2016-11-23 11:29:05 +03:00 committed by Olaoluwa Osuntokun
parent 5b9e4ae61e
commit 391d5cd401
4 changed files with 264 additions and 26 deletions

@ -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

@ -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,

136
lnwallet/size.go Normal file

@ -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
}

@ -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