lnwallet: add commitment transaction estimation
This commit is contained in:
parent
5b9e4ae61e
commit
391d5cd401
@ -25,6 +25,10 @@ var (
|
|||||||
ErrChanClosing = fmt.Errorf("channel is being closed, operation disallowed")
|
ErrChanClosing = fmt.Errorf("channel is being closed, operation disallowed")
|
||||||
ErrNoWindow = fmt.Errorf("unable to sign new commitment, the current" +
|
ErrNoWindow = fmt.Errorf("unable to sign new commitment, the current" +
|
||||||
" revocation window is exhausted")
|
" revocation window is exhausted")
|
||||||
|
ErrMaxWeightCost = fmt.Errorf("commitment transaction exceed max " +
|
||||||
|
"available weight")
|
||||||
|
ErrMaxHTLCNumber = fmt.Errorf("commitment transaction exceed max " +
|
||||||
|
"htlc number")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -1321,7 +1325,6 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.CommitRevocation,
|
|||||||
// commitment, and a log compaction is attempted. In addition, a slice of
|
// commitment, and a log compaction is attempted. In addition, a slice of
|
||||||
// HTLC's which can be forwarded upstream are returned.
|
// HTLC's which can be forwarded upstream are returned.
|
||||||
func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ([]*PaymentDescriptor, error) {
|
func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.CommitRevocation) ([]*PaymentDescriptor, error) {
|
||||||
|
|
||||||
lc.Lock()
|
lc.Lock()
|
||||||
defer lc.Unlock()
|
defer lc.Unlock()
|
||||||
|
|
||||||
@ -1736,24 +1739,9 @@ type ForceCloseSummary struct {
|
|||||||
SelfOutputSignDesc *SignDescriptor
|
SelfOutputSignDesc *SignDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForceClose executes a unilateral closure of the transaction at the current
|
// getSignedCommitTx function take the latest commitment transaction and populate
|
||||||
// lowest commitment height of the channel. Following a force closure, all
|
// it with witness data.
|
||||||
// state transitions, or modifications to the state update logs will be
|
func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) {
|
||||||
// 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
|
|
||||||
|
|
||||||
// Fetch the current commitment transaction, along with their signature
|
// Fetch the current commitment transaction, along with their signature
|
||||||
// for the transaction.
|
// for the transaction.
|
||||||
commitTx := lc.channelState.OurCommitTx
|
commitTx := lc.channelState.OurCommitTx
|
||||||
@ -1773,9 +1761,35 @@ func (lc *LightningChannel) ForceClose() (*ForceCloseSummary, error) {
|
|||||||
// required to spend from the multi-sig output.
|
// required to spend from the multi-sig output.
|
||||||
ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed()
|
ourKey := lc.channelState.OurMultiSigKey.SerializeCompressed()
|
||||||
theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed()
|
theirKey := lc.channelState.TheirMultiSigKey.SerializeCompressed()
|
||||||
witness := SpendMultiSig(lc.FundingWitnessScript, ourKey, ourSig,
|
|
||||||
theirKey, theirSig)
|
commitTx.TxIn[0].Witness = SpendMultiSig(lc.FundingWitnessScript, ourKey,
|
||||||
commitTx.TxIn[0].Witness = witness
|
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.
|
// 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
|
// 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/btcsuite/fastsha256"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/go-errors/errors"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/elkrem"
|
"github.com/lightningnetwork/lnd/elkrem"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/roasbeef/btcd/blockchain"
|
||||||
"github.com/roasbeef/btcd/btcec"
|
"github.com/roasbeef/btcd/btcec"
|
||||||
"github.com/roasbeef/btcd/chaincfg"
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
"github.com/roasbeef/btcd/txscript"
|
"github.com/roasbeef/btcd/txscript"
|
||||||
@ -253,6 +255,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan
|
|||||||
OurBalance: channelBal,
|
OurBalance: channelBal,
|
||||||
TheirBalance: channelBal,
|
TheirBalance: channelBal,
|
||||||
OurCommitTx: aliceCommitTx,
|
OurCommitTx: aliceCommitTx,
|
||||||
|
OurCommitSig: bytes.Repeat([]byte{1}, 71),
|
||||||
FundingOutpoint: prevOut,
|
FundingOutpoint: prevOut,
|
||||||
OurMultiSigKey: aliceKeyPub,
|
OurMultiSigKey: aliceKeyPub,
|
||||||
TheirMultiSigKey: bobKeyPub,
|
TheirMultiSigKey: bobKeyPub,
|
||||||
@ -276,6 +279,7 @@ func createTestChannels(revocationWindow int) (*LightningChannel, *LightningChan
|
|||||||
OurBalance: channelBal,
|
OurBalance: channelBal,
|
||||||
TheirBalance: channelBal,
|
TheirBalance: channelBal,
|
||||||
OurCommitTx: bobCommitTx,
|
OurCommitTx: bobCommitTx,
|
||||||
|
OurCommitSig: bytes.Repeat([]byte{1}, 71),
|
||||||
FundingOutpoint: prevOut,
|
FundingOutpoint: prevOut,
|
||||||
OurMultiSigKey: bobKeyPub,
|
OurMultiSigKey: bobKeyPub,
|
||||||
TheirMultiSigKey: aliceKeyPub,
|
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) {
|
func TestCooperativeChannelClosure(t *testing.T) {
|
||||||
// Create a test channel which will be used for the duration of this
|
// 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,
|
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||||
|
136
lnwallet/size.go
Normal file
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
|
// ErrorMaxPendingChannels is returned by remote peer when the number
|
||||||
// of active pending channels exceeds their maximum policy limit.
|
// of active pending channels exceeds their maximum policy limit.
|
||||||
ErrorMaxPendingChannels ErrorCode = 1
|
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
|
// ErrorGeneric represents a generic error bound to an exact channel. The
|
||||||
|
Loading…
Reference in New Issue
Block a user