Merge pull request #6347 from yyforyongyu/5388-rm-revc-log

lnwallet+channeldb: store minimal info in revocation log bucket
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2022-05-05 15:52:59 -07:00
commit 0ec88b5346
33 files changed
+3635 -562

No files matched your search

+324 -142
View File
@@ -96,6 +96,17 @@ var (
// both parties can retrieve their funds.
ErrCommitSyncRemoteDataLoss = fmt.Errorf("possible remote commitment " +
"state data loss")
// ErrNoRevocationLogFound is returned when both the returned logs are
// nil from querying the revocation log bucket. In theory this should
// never happen as the query will return `ErrLogEntryNotFound`, yet
// we'd still perform a sanity check to make sure at least one of the
// logs is non-nil.
ErrNoRevocationLogFound = errors.New("no revocation log found")
// ErrOutputIndexOutOfRange is returned when an output index is greater
// than or equal to the length of a given transaction's outputs.
ErrOutputIndexOutOfRange = errors.New("output index is out of range")
)
// ErrCommitSyncLocalDataLoss is returned in the case that we receive a valid
@@ -1036,26 +1047,29 @@ type updateLog struct {
logIndex uint64
// htlcCounter is a monotonically increasing integer that tracks the
// total number of offered HTLC's by the owner of this update log. We
// use a distinct index for this purpose, as update's that remove
// entries from the log will be indexed using this counter.
// total number of offered HTLC's by the owner of this update log,
// hence the `Add` update type. We use a distinct index for this
// purpose, as update's that remove entries from the log will be
// indexed using this counter.
htlcCounter uint64
// List is the updatelog itself, we embed this value so updateLog has
// access to all the method of a list.List.
*list.List
// updateIndex is an index that maps a particular entries index to the
// list element within the list.List above.
// updateIndex maps a `logIndex` to a particular update entry. It
// deals with the four update types:
// `Fail|MalformedFail|Settle|FeeUpdate`
updateIndex map[uint64]*list.Element
// offerIndex is an index that maps the counter for offered HTLC's to
// their list element within the main list.List.
// htlcIndex maps a `htlcCounter` to an offered HTLC entry, hence the
// `Add` update.
htlcIndex map[uint64]*list.Element
// modifiedHtlcs is a set that keeps track of all the current modified
// htlcs. A modified HTLC is one that's present in the log, and has as
// a pending fail or settle that's attempting to consume it.
// htlcs, hence update types `Fail|MalformedFail|Settle`. A modified
// HTLC is one that's present in the log, and has as a pending fail or
// settle that's attempting to consume it.
modifiedHtlcs map[uint64]struct{}
}
@@ -2210,10 +2224,10 @@ type HtlcRetribution struct {
// transaction. The BreachRetribution is then sent over the ContractBreach
// channel in order to allow the subscriber of the channel to dispatch justice.
type BreachRetribution struct {
// BreachTransaction is the transaction which breached the channel
// BreachTxHash is the transaction hash which breached the channel
// contract by spending from the funding multi-sig with a revoked
// commitment transaction.
BreachTransaction *wire.MsgTx
BreachTxHash chainhash.Hash
// BreachHeight records the block height confirming the breach
// transaction, used as a height hint when registering for
@@ -2228,13 +2242,9 @@ type BreachRetribution struct {
// RevokedStateNum is the revoked state number which was broadcast.
RevokedStateNum uint64
// PendingHTLCs is a slice of the HTLCs which were pending at this
// point within the channel's history transcript.
PendingHTLCs []channeldb.HTLC
// LocalOutputSignDesc is a SignDescriptor which is capable of
// generating the signature necessary to sweep the output within the
// BreachTransaction that pays directly us.
// breach transaction that pays directly us.
//
// NOTE: A nil value indicates that the local output is considered dust
// according to the remote party's dust limit.
@@ -2279,16 +2289,22 @@ type BreachRetribution struct {
// passed channel, at a particular revoked state number, and one which targets
// the passed commitment transaction.
func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
breachHeight uint32) (*BreachRetribution, error) {
breachHeight uint32, spendTx *wire.MsgTx) (*BreachRetribution, error) {
// Query the on-disk revocation log for the snapshot which was recorded
// at this particular state num.
revokedSnapshot, err := chanState.FindPreviousState(stateNum)
// at this particular state num. Based on whether a legacy revocation
// log is returned or not, we will process them differently.
revokedLog, revokedLogLegacy, err := chanState.FindPreviousState(
stateNum,
)
if err != nil {
return nil, err
}
commitHash := revokedSnapshot.CommitTx.TxHash()
// Sanity check that at least one of the logs is returned.
if revokedLog == nil && revokedLogLegacy == nil {
return nil, ErrNoRevocationLogFound
}
// With the state number broadcast known, we can now derive/restore the
// proper revocation preimage necessary to sweep the remote party's
@@ -2311,22 +2327,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
// Next, reconstruct the scripts as they were present at this state
// number so we can have the proper witness script to sign and include
// within the final witness.
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
isRemoteInitiator := !chanState.IsInitiator
var leaseExpiry uint32
if chanState.ChanType.HasLeaseExpiration() {
leaseExpiry = chanState.ThawHeight
}
theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry,
)
if err != nil {
return nil, err
}
// Since it is the remote breach we are reconstructing, the output going
// to us will be a to-remote script with our local params.
// Since it is the remote breach we are reconstructing, the output
// going to us will be a to-remote script with our local params.
isRemoteInitiator := !chanState.IsInitiator
ourScript, ourDelay, err := CommitScriptToRemote(
chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey,
leaseExpiry,
@@ -2335,15 +2343,248 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
return nil, err
}
// In order to fully populate the breach retribution struct, we'll need
// to find the exact index of the commitment outputs.
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry,
)
if err != nil {
return nil, err
}
// Define an empty breach retribution that will be overwritten based on
// different version of the revocation log found.
var br *BreachRetribution
// Define our and their amounts, that will be overwritten below.
var ourAmt, theirAmt int64
// If the returned *RevocationLog is non-nil, use it to derive the info
// we need.
if revokedLog != nil {
br, ourAmt, theirAmt, err = createBreachRetribution(
revokedLog, spendTx, chanState, keyRing,
commitmentSecret, leaseExpiry,
)
if err != nil {
return nil, err
}
} else {
// The returned revocation log is in legacy format, which is a
// *ChannelCommitment.
//
// NOTE: this branch is kept for compatibility such that for
// old nodes which refuse to migrate the legacy revocation log
// data can still function. This branch can be deleted once we
// are confident that no legacy format is in use.
br, ourAmt, theirAmt, err = createBreachRetributionLegacy(
revokedLogLegacy, chanState, keyRing, commitmentSecret,
ourScript, theirScript, leaseExpiry,
)
if err != nil {
return nil, err
}
}
// Conditionally instantiate a sign descriptor for each of the
// commitment outputs. If either is considered dust using the remote
// party's dust limit, the respective sign descriptor will be nil.
//
// If our balance exceeds the remote party's dust limit, instantiate
// the sign descriptor for our output.
if ourAmt >= int64(chanState.RemoteChanCfg.DustLimit) {
br.LocalOutputSignDesc = &input.SignDescriptor{
SingleTweak: keyRing.LocalCommitKeyTweak,
KeyDesc: chanState.LocalChanCfg.PaymentBasePoint,
WitnessScript: ourScript.WitnessScript,
Output: &wire.TxOut{
PkScript: ourScript.PkScript,
Value: ourAmt,
},
HashType: txscript.SigHashAll,
}
}
// Similarly, if their balance exceeds the remote party's dust limit,
// assemble the sign descriptor for their output, which we can sweep.
if theirAmt >= int64(chanState.RemoteChanCfg.DustLimit) {
br.RemoteOutputSignDesc = &input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.
RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: theirScript.WitnessScript,
Output: &wire.TxOut{
PkScript: theirScript.PkScript,
Value: theirAmt,
},
HashType: txscript.SigHashAll,
}
}
// Finally, with all the necessary data constructed, we can pad the
// BreachRetribution struct which houses all the data necessary to
// swiftly bring justice to the cheating remote party.
br.BreachHeight = breachHeight
br.RevokedStateNum = stateNum
br.LocalDelay = ourDelay
br.RemoteDelay = theirDelay
return br, nil
}
// createHtlcRetribution is a helper function to construct an HtlcRetribution
// based on the passed params.
func createHtlcRetribution(chanState *channeldb.OpenChannel,
keyRing *CommitmentKeyRing, commitHash chainhash.Hash,
commitmentSecret *btcec.PrivateKey, leaseExpiry uint32,
htlc *channeldb.HTLCEntry) (HtlcRetribution, error) {
var emptyRetribution HtlcRetribution
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
isRemoteInitiator := !chanState.IsInitiator
// We'll generate the original second level witness script now, as
// we'll need it if we're revoking an HTLC output on the remote
// commitment transaction, and *they* go to the second level.
secondLevelScript, err := SecondLevelHtlcScript(
chanState.ChanType, isRemoteInitiator,
keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay,
leaseExpiry,
)
if err != nil {
return emptyRetribution, err
}
// If this is an incoming HTLC, then this means that they were the
// sender of the HTLC (relative to us). So we'll re-generate the sender
// HTLC script. Otherwise, is this was an outgoing HTLC that we sent,
// then from the PoV of the remote commitment state, they're the
// receiver of this HTLC.
htlcPkScript, htlcWitnessScript, err := genHtlcScript(
chanState.ChanType, htlc.Incoming, false,
htlc.RefundTimeout, htlc.RHash, keyRing,
)
if err != nil {
return emptyRetribution, err
}
return HtlcRetribution{
SignDesc: input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.
RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: htlcWitnessScript,
Output: &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlc.Amt),
},
HashType: txscript.SigHashAll,
},
OutPoint: wire.OutPoint{
Hash: commitHash,
Index: uint32(htlc.OutputIndex),
},
SecondLevelWitnessScript: secondLevelScript.WitnessScript,
IsIncoming: htlc.Incoming,
}, nil
}
// createBreachRetribution creates a partially initiated BreachRetribution
// using a RevocationLog. Returns the constructed retribution, our amount,
// their amount, and a possible non-nil error.
func createBreachRetribution(revokedLog *channeldb.RevocationLog,
spendTx *wire.MsgTx, chanState *channeldb.OpenChannel,
keyRing *CommitmentKeyRing, commitmentSecret *btcec.PrivateKey,
leaseExpiry uint32) (*BreachRetribution, int64, int64, error) {
commitHash := revokedLog.CommitTxHash
// Create the htlc retributions.
htlcRetributions := make([]HtlcRetribution, len(revokedLog.HTLCEntries))
for i, htlc := range revokedLog.HTLCEntries {
hr, err := createHtlcRetribution(
chanState, keyRing, commitHash,
commitmentSecret, leaseExpiry, htlc,
)
if err != nil {
return nil, 0, 0, err
}
htlcRetributions[i] = hr
}
var ourAmt, theirAmt int64
// Construct the our outpoint.
ourOutpoint := wire.OutPoint{
Hash: commitHash,
}
if revokedLog.OurOutputIndex != channeldb.OutputIndexEmpty {
ourOutpoint.Index = uint32(revokedLog.OurOutputIndex)
// Sanity check that OurOutputIndex is within range.
if int(ourOutpoint.Index) >= len(spendTx.TxOut) {
return nil, 0, 0, fmt.Errorf("%w: ours=%v, "+
"len(TxOut)=%v", ErrOutputIndexOutOfRange,
ourOutpoint.Index, len(spendTx.TxOut),
)
}
// Read the amounts from the breach transaction.
//
// NOTE: ourAmt here includes commit fee and anchor amount(if
// enabled).
ourAmt = spendTx.TxOut[ourOutpoint.Index].Value
}
// Construct the their outpoint.
theirOutpoint := wire.OutPoint{
Hash: commitHash,
}
if revokedLog.TheirOutputIndex != channeldb.OutputIndexEmpty {
theirOutpoint.Index = uint32(revokedLog.TheirOutputIndex)
// Sanity check that TheirOutputIndex is within range.
if int(revokedLog.TheirOutputIndex) >= len(spendTx.TxOut) {
return nil, 0, 0, fmt.Errorf("%w: theirs=%v, "+
"len(TxOut)=%v", ErrOutputIndexOutOfRange,
revokedLog.TheirOutputIndex, len(spendTx.TxOut),
)
}
// Read the amounts from the breach transaction.
theirAmt = spendTx.TxOut[theirOutpoint.Index].Value
}
return &BreachRetribution{
BreachTxHash: commitHash,
ChainHash: chanState.ChainHash,
LocalOutpoint: ourOutpoint,
RemoteOutpoint: theirOutpoint,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, ourAmt, theirAmt, nil
}
// createBreachRetributionLegacy creates a partially initiated
// BreachRetribution using a ChannelCommitment. Returns the constructed
// retribution, our amount, their amount, and a possible non-nil error.
func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment,
chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing,
commitmentSecret *btcec.PrivateKey,
ourScript, theirScript *ScriptInfo,
leaseExpiry uint32) (*BreachRetribution, int64, int64, error) {
commitHash := revokedLog.CommitTx.TxHash()
ourOutpoint := wire.OutPoint{
Hash: commitHash,
}
theirOutpoint := wire.OutPoint{
Hash: commitHash,
}
for i, txOut := range revokedSnapshot.CommitTx.TxOut {
// In order to fully populate the breach retribution struct, we'll need
// to find the exact index of the commitment outputs.
for i, txOut := range revokedLog.CommitTx.TxOut {
switch {
case bytes.Equal(txOut.PkScript, ourScript.PkScript):
ourOutpoint.Index = uint32(i)
@@ -2352,127 +2593,52 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
}
}
// Conditionally instantiate a sign descriptor for each of the
// commitment outputs. If either is considered dust using the remote
// party's dust limit, the respective sign descriptor will be nil.
var (
ourSignDesc *input.SignDescriptor
theirSignDesc *input.SignDescriptor
)
// Compute the balances in satoshis.
ourAmt := revokedSnapshot.LocalBalance.ToSatoshis()
theirAmt := revokedSnapshot.RemoteBalance.ToSatoshis()
// If our balance exceeds the remote party's dust limit, instantiate
// the sign descriptor for our output.
if ourAmt >= chanState.RemoteChanCfg.DustLimit {
ourSignDesc = &input.SignDescriptor{
SingleTweak: keyRing.LocalCommitKeyTweak,
KeyDesc: chanState.LocalChanCfg.PaymentBasePoint,
WitnessScript: ourScript.WitnessScript,
Output: &wire.TxOut{
PkScript: ourScript.PkScript,
Value: int64(ourAmt),
},
HashType: txscript.SigHashAll,
}
}
// Similarly, if their balance exceeds the remote party's dust limit,
// assemble the sign descriptor for their output, which we can sweep.
if theirAmt >= chanState.RemoteChanCfg.DustLimit {
theirSignDesc = &input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: theirScript.WitnessScript,
Output: &wire.TxOut{
PkScript: theirScript.PkScript,
Value: int64(theirAmt),
},
HashType: txscript.SigHashAll,
}
}
// With the commitment outputs located, we'll now generate all the
// retribution structs for each of the HTLC transactions active on the
// remote commitment transaction.
htlcRetributions := make([]HtlcRetribution, 0, len(revokedSnapshot.Htlcs))
for _, htlc := range revokedSnapshot.Htlcs {
htlcRetributions := make([]HtlcRetribution, len(revokedLog.Htlcs))
for i, htlc := range revokedLog.Htlcs {
// If the HTLC is dust, then we'll skip it as it doesn't have
// an output on the commitment transaction.
if HtlcIsDust(
chanState.ChanType, htlc.Incoming, false,
chainfee.SatPerKWeight(revokedSnapshot.FeePerKw),
htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit,
chainfee.SatPerKWeight(revokedLog.FeePerKw),
htlc.Amt.ToSatoshis(),
chanState.RemoteChanCfg.DustLimit,
) {
continue
}
// We'll generate the original second level witness script now,
// as we'll need it if we're revoking an HTLC output on the
// remote commitment transaction, and *they* go to the second
// level.
secondLevelScript, err := SecondLevelHtlcScript(
chanState.ChanType, isRemoteInitiator,
keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay,
leaseExpiry,
entry := &channeldb.HTLCEntry{
RHash: htlc.RHash,
RefundTimeout: htlc.RefundTimeout,
OutputIndex: uint16(htlc.OutputIndex),
Incoming: htlc.Incoming,
Amt: htlc.Amt.ToSatoshis(),
}
hr, err := createHtlcRetribution(
chanState, keyRing, commitHash,
commitmentSecret, leaseExpiry, entry,
)
if err != nil {
return nil, err
return nil, 0, 0, err
}
// If this is an incoming HTLC, then this means that they were
// the sender of the HTLC (relative to us). So we'll
// re-generate the sender HTLC script. Otherwise, is this was
// an outgoing HTLC that we sent, then from the PoV of the
// remote commitment state, they're the receiver of this HTLC.
htlcPkScript, htlcWitnessScript, err := genHtlcScript(
chanState.ChanType, htlc.Incoming, false,
htlc.RefundTimeout, htlc.RHash, keyRing,
)
if err != nil {
return nil, err
}
htlcRetributions = append(htlcRetributions, HtlcRetribution{
SignDesc: input.SignDescriptor{
KeyDesc: chanState.LocalChanCfg.RevocationBasePoint,
DoubleTweak: commitmentSecret,
WitnessScript: htlcWitnessScript,
Output: &wire.TxOut{
PkScript: htlcPkScript,
Value: int64(htlc.Amt.ToSatoshis()),
},
HashType: txscript.SigHashAll,
},
OutPoint: wire.OutPoint{
Hash: commitHash,
Index: uint32(htlc.OutputIndex),
},
SecondLevelWitnessScript: secondLevelScript.WitnessScript,
IsIncoming: htlc.Incoming,
})
htlcRetributions[i] = hr
}
// Finally, with all the necessary data constructed, we can create the
// BreachRetribution struct which houses all the data necessary to
// swiftly bring justice to the cheating remote party.
// Compute the balances in satoshis.
ourAmt := int64(revokedLog.LocalBalance.ToSatoshis())
theirAmt := int64(revokedLog.RemoteBalance.ToSatoshis())
return &BreachRetribution{
ChainHash: chanState.ChainHash,
BreachTransaction: revokedSnapshot.CommitTx,
BreachHeight: breachHeight,
RevokedStateNum: stateNum,
PendingHTLCs: revokedSnapshot.Htlcs,
LocalOutpoint: ourOutpoint,
LocalOutputSignDesc: ourSignDesc,
LocalDelay: ourDelay,
RemoteOutpoint: theirOutpoint,
RemoteOutputSignDesc: theirSignDesc,
RemoteDelay: theirDelay,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, nil
BreachTxHash: commitHash,
ChainHash: chanState.ChainHash,
LocalOutpoint: ourOutpoint,
RemoteOutpoint: theirOutpoint,
HtlcRetributions: htlcRetributions,
KeyRing: keyRing,
}, ourAmt, theirAmt, nil
}
// HtlcIsDust determines if an HTLC output is dust or not depending on two
@@ -3822,7 +3988,7 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
// but died before the signature was sent. We re-transmit our
// revocation, but also initiate a state transition to re-sync
// them.
if lc.OweCommitment(true) {
if lc.OweCommitment() {
commitSig, htlcSigs, _, err := lc.SignNextCommitment()
switch {
@@ -4536,11 +4702,11 @@ func (lc *LightningChannel) IsChannelClean() bool {
// out a commitment signature because there are outstanding local updates and/or
// updates in the local commit tx that aren't reflected in the remote commit tx
// yet.
func (lc *LightningChannel) OweCommitment(local bool) bool {
func (lc *LightningChannel) OweCommitment() bool {
lc.RLock()
defer lc.RUnlock()
return lc.oweCommitment(local)
return lc.oweCommitment(true)
}
// oweCommitment is the internal version of OweCommitment. This function expects
@@ -4874,12 +5040,28 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
source, remoteChainTail, addUpdates, settleFailUpdates,
)
// We will soon be saving the current remote commitment to revocation
// log bucket, which is `lc.channelState.RemoteCommitment`. After that,
// the `RemoteCommitment` will be replaced with a newer version found
// in `CommitDiff`. Thus we need to compute the output indexes here
// before the change since the indexes are meant for the current,
// revoked remote commitment.
ourOutputIndex, theirOutputIndex, err := findOutputIndexesFromRemote(
revocation, lc.channelState,
)
if err != nil {
return nil, nil, nil, nil, err
}
// At this point, the revocation has been accepted, and we've rotated
// the current revocation key+hash for the remote party. Therefore we
// sync now to ensure the revocation producer state is consistent with
// the current commitment height and also to advance the on-disk
// commitment chain.
err = lc.channelState.AdvanceCommitChainTail(fwdPkg, localPeerUpdates)
err = lc.channelState.AdvanceCommitChainTail(
fwdPkg, localPeerUpdates,
ourOutputIndex, theirOutputIndex,
)
if err != nil {
return nil, nil, nil, nil, err
}
+388 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
@@ -7106,8 +7107,9 @@ func TestNewBreachRetributionSkipsDustHtlcs(t *testing.T) {
// At this point, we'll now simulate a contract breach by Bob using the
// NewBreachRetribution method.
breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx
breachRet, err := NewBreachRetribution(
aliceChannel.channelState, revokedStateNum, 100,
aliceChannel.channelState, revokedStateNum, 100, breachTx,
)
if err != nil {
t.Fatalf("unable to create breach retribution: %v", err)
@@ -10249,3 +10251,388 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) {
checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt+htlc3Amt)
}
}
// deriveDummyRetributionParams is a helper function that derives a list of
// dummy params to assist retribution creation related tests.
func deriveDummyRetributionParams(chanState *channeldb.OpenChannel) (uint32,
*CommitmentKeyRing, chainhash.Hash) {
config := chanState.RemoteChanCfg
commitHash := chanState.RemoteCommitment.CommitTx.TxHash()
keyRing := DeriveCommitmentKeys(
config.RevocationBasePoint.PubKey, false, chanState.ChanType,
&chanState.LocalChanCfg, &chanState.RemoteChanCfg,
)
leaseExpiry := chanState.ThawHeight
return leaseExpiry, keyRing, commitHash
}
// TestCreateHtlcRetribution checks that `createHtlcRetribution` behaves as
// epxected.
func TestCreateHtlcRetribution(t *testing.T) {
t.Parallel()
// Create a dummy private key and an HTLC amount for testing.
dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1})
testAmt := btcutil.Amount(100)
// Create a test channel.
aliceChannel, _, cleanUp, err := CreateTestChannels(
channeldb.ZeroHtlcTxFeeBit,
)
require.NoError(t, err)
defer cleanUp()
// Prepare the params needed to call the function. Note that the values
// here are not necessary "cryptography-correct", we just use them to
// construct the htlc retribution.
leaseExpiry, keyRing, commitHash := deriveDummyRetributionParams(
aliceChannel.channelState,
)
htlc := &channeldb.HTLCEntry{
Amt: testAmt,
Incoming: true,
OutputIndex: 1,
}
// Create the htlc retribution.
hr, err := createHtlcRetribution(
aliceChannel.channelState, keyRing, commitHash,
dummyPrivate, leaseExpiry, htlc,
)
// Expect no error.
require.NoError(t, err)
// Check the fields have expected values.
require.EqualValues(t, testAmt, hr.SignDesc.Output.Value)
require.Equal(t, commitHash, hr.OutPoint.Hash)
require.EqualValues(t, htlc.OutputIndex, hr.OutPoint.Index)
require.Equal(t, htlc.Incoming, hr.IsIncoming)
}
// TestCreateBreachRetribution checks that `createBreachRetribution` behaves as
// epxected.
func TestCreateBreachRetribution(t *testing.T) {
t.Parallel()
// Create dummy values for the test.
dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1})
testAmt := int64(100)
ourAmt := int64(1000)
theirAmt := int64(2000)
localIndex := uint32(0)
remoteIndex := uint32(1)
htlcIndex := uint32(2)
// Create a dummy breach tx, which has our output located at index 0
// and theirs at 1.
spendTx := &wire.MsgTx{
TxOut: []*wire.TxOut{
{Value: ourAmt},
{Value: theirAmt},
{Value: testAmt},
},
}
// Create a test channel.
aliceChannel, _, cleanUp, err := CreateTestChannels(
channeldb.ZeroHtlcTxFeeBit,
)
require.NoError(t, err)
defer cleanUp()
// Prepare the params needed to call the function. Note that the values
// here are not necessary "cryptography-correct", we just use them to
// construct the retribution.
leaseExpiry, keyRing, commitHash := deriveDummyRetributionParams(
aliceChannel.channelState,
)
htlc := &channeldb.HTLCEntry{
Amt: btcutil.Amount(testAmt),
Incoming: true,
OutputIndex: uint16(htlcIndex),
}
// Create a dummy revocation log.
revokedLog := channeldb.RevocationLog{
CommitTxHash: commitHash,
OurOutputIndex: uint16(localIndex),
TheirOutputIndex: uint16(remoteIndex),
HTLCEntries: []*channeldb.HTLCEntry{htlc},
}
// Create a log with an empty local output index.
revokedLogNoLocal := revokedLog
revokedLogNoLocal.OurOutputIndex = channeldb.OutputIndexEmpty
// Create a log with an empty remote output index.
revokedLogNoRemote := revokedLog
revokedLogNoRemote.TheirOutputIndex = channeldb.OutputIndexEmpty
testCases := []struct {
name string
revocationLog *channeldb.RevocationLog
expectedErr error
expectedOurAmt int64
expectedTheirAmt int64
}{
{
name: "create retribution successfully",
revocationLog: &revokedLog,
expectedErr: nil,
expectedOurAmt: ourAmt,
expectedTheirAmt: theirAmt,
},
{
name: "fail due to our index too big",
revocationLog: &channeldb.RevocationLog{
OurOutputIndex: uint16(htlcIndex + 1),
},
expectedErr: ErrOutputIndexOutOfRange,
},
{
name: "fail due to their index too big",
revocationLog: &channeldb.RevocationLog{
TheirOutputIndex: uint16(htlcIndex + 1),
},
expectedErr: ErrOutputIndexOutOfRange,
},
{
name: "empty local output index",
revocationLog: &revokedLogNoLocal,
expectedErr: nil,
expectedOurAmt: 0,
expectedTheirAmt: theirAmt,
},
{
name: "empty remote output index",
revocationLog: &revokedLogNoRemote,
expectedErr: nil,
expectedOurAmt: ourAmt,
expectedTheirAmt: 0,
},
}
// assertRetribution is a helper closure that checks a given breach
// retribution has the expected values on certain fields.
assertRetribution := func(br *BreachRetribution, our, their int64) {
chainHash := aliceChannel.channelState.ChainHash
require.Equal(t, commitHash, br.BreachTxHash)
require.Equal(t, chainHash, br.ChainHash)
// Construct local outpoint, we only have the index when the
// amount is not zero.
local := wire.OutPoint{
Hash: commitHash,
}
if our != 0 {
local.Index = localIndex
}
// Construct remote outpoint, we only have the index when the
// amount is not zero.
remote := wire.OutPoint{
Hash: commitHash,
}
if their != 0 {
remote.Index = remoteIndex
}
require.Equal(t, local, br.LocalOutpoint)
require.Equal(t, remote, br.RemoteOutpoint)
for _, hr := range br.HtlcRetributions {
require.EqualValues(t, testAmt,
hr.SignDesc.Output.Value)
require.Equal(t, commitHash, hr.OutPoint.Hash)
require.EqualValues(t, htlcIndex, hr.OutPoint.Index)
require.Equal(t, htlc.Incoming, hr.IsIncoming)
}
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
br, our, their, err := createBreachRetribution(
tc.revocationLog, spendTx,
aliceChannel.channelState, keyRing,
dummyPrivate, leaseExpiry,
)
// Check the error if expected.
if tc.expectedErr != nil {
require.ErrorIs(t, err, tc.expectedErr)
} else {
// Otherwise we expect no error.
require.NoError(t, err)
// Check the amounts and the contructed partial
// retribution are returned as expected.
require.Equal(t, tc.expectedOurAmt, our)
require.Equal(t, tc.expectedTheirAmt, their)
assertRetribution(br, our, their)
}
})
}
}
// TestCreateBreachRetributionLegacy checks that
// `createBreachRetributionLegacy` behaves as expected.
func TestCreateBreachRetributionLegacy(t *testing.T) {
t.Parallel()
// Create dummy values for the test.
dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1})
// Create a test channel.
aliceChannel, _, cleanUp, err := CreateTestChannels(
channeldb.ZeroHtlcTxFeeBit,
)
require.NoError(t, err)
defer cleanUp()
// Prepare the params needed to call the function. Note that the values
// here are not necessary "cryptography-correct", we just use them to
// construct the retribution.
leaseExpiry, keyRing, _ := deriveDummyRetributionParams(
aliceChannel.channelState,
)
// Use the remote commitment as our revocation log.
revokedLog := aliceChannel.channelState.RemoteCommitment
ourOp := revokedLog.CommitTx.TxOut[0]
theirOp := revokedLog.CommitTx.TxOut[1]
// Create the dummy scripts.
ourScript := &ScriptInfo{
PkScript: ourOp.PkScript,
}
theirScript := &ScriptInfo{
PkScript: theirOp.PkScript,
}
// Create the breach retribution using the legacy format.
br, ourAmt, theirAmt, err := createBreachRetributionLegacy(
&revokedLog, aliceChannel.channelState, keyRing,
dummyPrivate, ourScript, theirScript, leaseExpiry,
)
require.NoError(t, err)
// Check the commitHash and chainHash.
commitHash := revokedLog.CommitTx.TxHash()
chainHash := aliceChannel.channelState.ChainHash
require.Equal(t, commitHash, br.BreachTxHash)
require.Equal(t, chainHash, br.ChainHash)
// Check the outpoints.
local := wire.OutPoint{
Hash: commitHash,
Index: 0,
}
remote := wire.OutPoint{
Hash: commitHash,
Index: 1,
}
require.Equal(t, local, br.LocalOutpoint)
require.Equal(t, remote, br.RemoteOutpoint)
// Validate the amounts, note that in the legacy format, our amount is
// not directly the amount found in the to local output. Rather, it's
// the local output value minus the commit fee and anchor value(if
// present).
require.EqualValues(t, revokedLog.LocalBalance.ToSatoshis(), ourAmt)
require.Equal(t, theirOp.Value, theirAmt)
}
// TestNewBreachRetribution tests that the function `NewBreachRetribution`
// behaves as expected.
func TestNewBreachRetribution(t *testing.T) {
t.Run("non-anchor", func(t *testing.T) {
testNewBreachRetribution(t, channeldb.ZeroHtlcTxFeeBit)
})
t.Run("anchor", func(t *testing.T) {
chanType := channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit
testNewBreachRetribution(t, chanType)
})
}
// testNewBreachRetribution takes a channel type and tests the function
// `NewBreachRetribution`.
func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
t.Parallel()
aliceChannel, bobChannel, cleanUp, err := CreateTestChannels(chanType)
require.NoError(t, err)
defer cleanUp()
breachHeight := uint32(101)
stateNum := uint64(0)
chainHash := aliceChannel.channelState.ChainHash
theirDelay := uint32(aliceChannel.channelState.RemoteChanCfg.CsvDelay)
breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx
// Create a breach retribution at height 0, which should give us an
// error as there are no past delta state saved as revocation logs yet.
_, err = NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, breachTx,
)
require.ErrorIs(t, err, channeldb.ErrNoPastDeltas)
// We now force a state transition which will give us a revocation log
// at height 0.
txid := aliceChannel.channelState.RemoteCommitment.CommitTx.TxHash()
err = ForceStateTransition(aliceChannel, bobChannel)
require.NoError(t, err)
// assertRetribution is a helper closure that checks a given breach
// retribution has the expected values on certain fields.
assertRetribution := func(br *BreachRetribution,
localIndex, remoteIndex uint32) {
require.Equal(t, txid, br.BreachTxHash)
require.Equal(t, chainHash, br.ChainHash)
require.Equal(t, breachHeight, br.BreachHeight)
require.Equal(t, stateNum, br.RevokedStateNum)
require.Equal(t, theirDelay, br.RemoteDelay)
local := wire.OutPoint{
Hash: txid,
Index: localIndex,
}
remote := wire.OutPoint{
Hash: txid,
Index: remoteIndex,
}
if chanType.HasAnchors() {
// For anchor channels, we expect the local delay to be
// 1 otherwise 0.
require.EqualValues(t, 1, br.LocalDelay)
} else {
require.Zero(t, br.LocalDelay)
}
require.Equal(t, local, br.LocalOutpoint)
require.Equal(t, remote, br.RemoteOutpoint)
}
// Create the retribution again and we should expect it to be created
// successfully.
br, err := NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, breachTx,
)
require.NoError(t, err)
// Check the retribution is as expected.
t.Log(spew.Sdump(breachTx))
assertRetribution(br, 1, 0)
// Create the retribution using a stateNum+1 and we should expect an
// error.
_, err = NewBreachRetribution(
aliceChannel.channelState, stateNum+1, breachHeight, breachTx,
)
require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound)
}
+75
View File
@@ -1,11 +1,13 @@
package lnwallet
import (
"bytes"
"fmt"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
@@ -960,3 +962,76 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool,
return nil
}
// findOutputIndexesFromRemote finds the index of our and their outputs from
// the remote commitment transaction. It derives the key ring to compute the
// output scripts and compares them against the outputs inside the commitment
// to find the match.
func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash,
chanState *channeldb.OpenChannel) (uint32, uint32, error) {
// Init the output indexes as empty.
ourIndex := uint32(channeldb.OutputIndexEmpty)
theirIndex := uint32(channeldb.OutputIndexEmpty)
chanCommit := chanState.RemoteCommitment
_, commitmentPoint := btcec.PrivKeyFromBytes(revocationPreimage[:])
// With the commitment point generated, we can now derive the king ring
// which will be used to generate the output scripts.
keyRing := DeriveCommitmentKeys(
commitmentPoint, false, chanState.ChanType,
&chanState.LocalChanCfg, &chanState.RemoteChanCfg,
)
// Since it's remote commitment chain, we'd used the mirrored values.
//
// We use the remote's channel config for the csv delay.
theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay)
// If we are the initiator of this channel, then it's be false from the
// remote's PoV.
isRemoteInitiator := !chanState.IsInitiator
var leaseExpiry uint32
if chanState.ChanType.HasLeaseExpiration() {
leaseExpiry = chanState.ThawHeight
}
// Map the scripts from our PoV. When facing a local commitment, the to
// local output belongs to us and the to remote output belongs to them.
// When facing a remote commitment, the to local output belongs to them
// and the to remote output belongs to us.
// Compute the to local script. From our PoV, when facing a remote
// commitment, the to local output belongs to them.
theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry,
)
if err != nil {
return ourIndex, theirIndex, err
}
// Compute the to remote script. From our PoV, when facing a remote
// commitment, the to remote output belongs to us.
ourScript, _, err := CommitScriptToRemote(
chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey,
leaseExpiry,
)
if err != nil {
return ourIndex, theirIndex, err
}
// Now compare the scripts to find our/their output index.
for i, txOut := range chanCommit.CommitTx.TxOut {
switch {
case bytes.Equal(txOut.PkScript, ourScript.PkScript):
ourIndex = uint32(i)
case bytes.Equal(txOut.PkScript, theirScript.PkScript):
theirIndex = uint32(i)
}
}
return ourIndex, theirIndex, nil
}
+4 -2
View File
@@ -403,8 +403,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
FeePerKw: btcutil.Amount(commitFeePerKw),
CommitFee: commitFee,
},
ThawHeight: thawHeight,
Db: wallet.Cfg.Database,
ThawHeight: thawHeight,
Db: wallet.Cfg.Database,
InitialLocalBalance: ourBalance,
InitialRemoteBalance: theirBalance,
},
pushMSat: pushMSat,
pendingChanID: pendingChanID,