diff --git a/config_builder.go b/config_builder.go index 4b585cb58..7d6235f39 100644 --- a/config_builder.go +++ b/config_builder.go @@ -187,6 +187,10 @@ type AuxComponents struct { // AuxChanCloser is an optional channel closer that can be used to // modify the way a coop-close transaction is constructed. AuxChanCloser fn.Option[chancloser.AuxChanCloser] + + // AuxContractResolver is an optional interface that can be used to + // modify the way contracts are resolved. + AuxContractResolver fn.Option[lnwallet.AuxContractResolver] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go index cf575f153..f3a56e8aa 100644 --- a/contractcourt/breach_arbitrator_test.go +++ b/contractcourt/breach_arbitrator_test.go @@ -1592,6 +1592,9 @@ func testBreachSpends(t *testing.T, test breachTest) { retribution, err := lnwallet.NewBreachRetribution( alice.State(), height, 1, forceCloseTx, fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}), + fn.Some[lnwallet.AuxContractResolver]( + &lnwallet.MockAuxContractResolver{}, + ), ) require.NoError(t, err, "unable to create breach retribution") @@ -1802,6 +1805,9 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) { retribution, err := lnwallet.NewBreachRetribution( alice.State(), height, uint32(blockHeight), forceCloseTx, fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}), + fn.Some[lnwallet.AuxContractResolver]( + &lnwallet.MockAuxContractResolver{}, + ), ) require.NoError(t, err, "unable to create breach retribution") diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index b1e3fc1c2..ef4a4e200 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -196,6 +196,9 @@ type chainWatcherConfig struct { // auxLeafStore can be used to fetch information for custom channels. auxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // auxResolver is used to supplement contract resolution. + auxResolver fn.Option[lnwallet.AuxContractResolver] } // chainWatcher is a system that's assigned to every active channel. The duty @@ -896,7 +899,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, spendHeight := uint32(commitSpend.SpendingHeight) retribution, err := lnwallet.NewBreachRetribution( c.cfg.chanState, broadcastStateNum, spendHeight, - commitSpend.SpendingTx, c.cfg.auxLeafStore, + commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver, ) switch { @@ -1147,7 +1150,7 @@ func (c *chainWatcher) dispatchLocalForceClose( forceClose, err := lnwallet.NewLocalForceCloseSummary( c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum, - c.cfg.auxLeafStore, + c.cfg.auxLeafStore, c.cfg.auxResolver, ) if err != nil { return err @@ -1239,8 +1242,8 @@ func (c *chainWatcher) dispatchRemoteForceClose( // materials required to let each subscriber sweep the funds in the // channel on-chain. uniClose, err := lnwallet.NewUnilateralCloseSummary( - c.cfg.chanState, c.cfg.signer, commitSpend, - remoteCommit, commitPoint, c.cfg.auxLeafStore, + c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit, + commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver, ) if err != nil { return err diff --git a/lnwallet/aux_resolutions.go b/lnwallet/aux_resolutions.go new file mode 100644 index 000000000..e3b04fc5a --- /dev/null +++ b/lnwallet/aux_resolutions.go @@ -0,0 +1,89 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +// CloseType is an enum that represents the type of close that we are trying to +// resolve. +type CloseType uint8 + +const ( + // LocalForceClose represents a local force close. + LocalForceClose CloseType = iota + + // RemoteForceClose represents a remote force close. + RemoteForceClose + + // Breach represents a breach by the remote party. + Breach +) + +// ResolutionReq is used to ask an outside sub-system for additional +// information needed to resolve a contract. +type ResolutionReq struct { + // ChanPoint is the channel point of the channel that we are trying to + // resolve. + ChanPoint wire.OutPoint + + // ShortChanID is the short channel ID of the channel that we are + // trying to resolve. + ShortChanID lnwire.ShortChannelID + + // Initiator is a bool if we're the initiator of the channel. + Initiator bool + + // CommitBlob is an optional commit blob for the channel. + CommitBlob fn.Option[tlv.Blob] + + // FundingBlob is an optional funding blob for the channel. + FundingBlob fn.Option[tlv.Blob] + + // Type is the type of the witness that we are trying to resolve. + Type input.WitnessType + + // CloseType is the type of close that we are trying to resolve. + CloseType CloseType + + // CommitTx is the force close commitment transaction. + CommitTx *wire.MsgTx + + // CommitFee is the fee that was paid for the commitment transaction. + CommitFee btcutil.Amount + + // ContractPoint is the outpoint of the contract we're trying to + // resolve. + ContractPoint wire.OutPoint + + // SignDesc is the sign descriptor for the contract. + SignDesc input.SignDescriptor + + // KeyRing is the key ring for the channel. + KeyRing *CommitmentKeyRing + + // CsvDelay is the CSV delay for the local output for this commitment. + CsvDelay uint32 + + // BreachCsvDelay is the CSV delay for the remote output. This is only + // set when the CloseType is Breach. This indicates the CSV delay to + // use for the remote party's to_local delayed output, that is now + // rightfully ours in a breach situation. + BreachCsvDelay fn.Option[uint32] + + // CltvDelay is the CLTV delay for the outpoint. + CltvDelay fn.Option[uint32] +} + +// AuxContractResolver is an interface that is used to resolve contracts that +// may need additional outside information to resolve correctly. +type AuxContractResolver interface { + // ResolveContract is called to resolve a contract that needs + // additional information to resolve properly. If no extra information + // is required, a nil Result error is returned. + ResolveContract(ResolutionReq) fn.Result[tlv.Blob] +} diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 8f7513f10..f3e076950 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -764,6 +764,10 @@ type LightningChannel struct { // custom channel variants. auxSigner fn.Option[AuxSigner] + // auxResolver is an optional component that can be used to modify the + // way contracts are resolved. + auxResolver fn.Option[AuxContractResolver] + // Capacity is the total capacity of this channel. Capacity btcutil.Amount @@ -824,8 +828,9 @@ type channelOpts struct { localNonce *musig2.Nonces remoteNonce *musig2.Nonces - leafStore fn.Option[AuxLeafStore] - auxSigner fn.Option[AuxSigner] + leafStore fn.Option[AuxLeafStore] + auxSigner fn.Option[AuxSigner] + auxResolver fn.Option[AuxContractResolver] skipNonceInit bool } @@ -871,6 +876,14 @@ func WithAuxSigner(signer AuxSigner) ChannelOpt { } } +// WithAuxResolver is used to specify a custom aux contract resolver for the +// channel. +func WithAuxResolver(resolver AuxContractResolver) ChannelOpt { + return func(o *channelOpts) { + o.auxResolver = fn.Some[AuxContractResolver](resolver) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -924,6 +937,7 @@ func NewLightningChannel(signer input.Signer, Signer: signer, leafStore: opts.leafStore, auxSigner: opts.auxSigner, + auxResolver: opts.auxResolver, sigPool: sigPool, currentHeight: localCommit.CommitHeight, commitChains: commitChains, @@ -1884,6 +1898,10 @@ type HtlcRetribution struct { // this HTLC was offered by us. This flag is used determine the exact // witness type should be used to sweep the output. IsIncoming bool + + // ResolutionBlob is a blob used for aux channels that permits a + // spender of this output to claim all funds. + ResolutionBlob fn.Option[tlv.Blob] } // BreachRetribution contains all the data necessary to bring a channel @@ -1953,6 +1971,14 @@ type BreachRetribution struct { // breaching commitment transaction. This allows downstream clients to // have access to the public keys used in the scripts. KeyRing *CommitmentKeyRing + + // LocalResolutionBlob is a blob used for aux channels that permits an + // honest party to sweep the local commitment output. + LocalResolutionBlob fn.Option[tlv.Blob] + + // RemoteResolutionBlob is a blob used for aux channels that permits an + // honest party to sweep the remote commitment output. + RemoteResolutionBlob fn.Option[tlv.Blob] } // NewBreachRetribution creates a new fully populated BreachRetribution for the @@ -1964,7 +1990,9 @@ type BreachRetribution struct { // the required fields then ErrRevLogDataMissing will be returned. func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, breachHeight uint32, spendTx *wire.MsgTx, - leafStore fn.Option[AuxLeafStore]) (*BreachRetribution, error) { + leafStore fn.Option[AuxLeafStore], + auxResolver fn.Option[AuxContractResolver]) (*BreachRetribution, + error) { // Query the on-disk revocation log for the snapshot which was recorded // at this particular state num. Based on whether a legacy revocation @@ -2127,6 +2155,38 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, return nil, err } } + + // At this point, we'll check to see if we need any extra + // resolution data for this output. + resolveReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + FundingBlob: chanState.CustomBlob, + Type: input.TaprootRemoteCommitSpend, + CloseType: Breach, + CommitTx: spendTx, + SignDesc: *br.LocalOutputSignDesc, + KeyRing: keyRing, + CsvDelay: ourDelay, + BreachCsvDelay: fn.Some(theirDelay), + CommitFee: chanState.RemoteCommitment.CommitFee, + } + if revokedLog != nil { + resolveReq.CommitBlob = revokedLog.CustomBlob.ValOpt() + } + + resolveBlob := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + return a.ResolveContract(resolveReq) + }, + ) + if err := resolveBlob.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + br.LocalResolutionBlob = resolveBlob.Option() } // Similarly, if their balance exceeds the remote party's dust limit, @@ -2174,6 +2234,37 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, return nil, err } } + + // At this point, we'll check to see if we need any extra + // resolution data for this output. + resolveReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + FundingBlob: chanState.CustomBlob, + Type: input.TaprootCommitmentRevoke, + CloseType: Breach, + CommitTx: spendTx, + SignDesc: *br.RemoteOutputSignDesc, + KeyRing: keyRing, + CsvDelay: theirDelay, + BreachCsvDelay: fn.Some(theirDelay), + CommitFee: chanState.RemoteCommitment.CommitFee, + } + if revokedLog != nil { + resolveReq.CommitBlob = revokedLog.CustomBlob.ValOpt() + } + resolveBlob := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + return a.ResolveContract(resolveReq) + }, + ) + if err := resolveBlob.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + br.RemoteResolutionBlob = resolveBlob.Option() } // Finally, with all the necessary data constructed, we can pad the @@ -6473,6 +6564,11 @@ type CommitOutputResolution struct { // that pay to the local party within the broadcast commitment // transaction. MaturityDelay uint32 + + // ResolutionBlob is a blob used for aux channels that permits a + // spender of the output to properly resolve it in the case of a force + // close. + ResolutionBlob fn.Option[tlv.Blob] } // UnilateralCloseSummary describes the details of a detected unilateral @@ -6530,7 +6626,9 @@ type UnilateralCloseSummary struct { func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Signer, commitSpend *chainntnfs.SpendDetail, remoteCommit channeldb.ChannelCommitment, commitPoint *btcec.PublicKey, - leafStore fn.Option[AuxLeafStore]) (*UnilateralCloseSummary, error) { + leafStore fn.Option[AuxLeafStore], + auxResolver fn.Option[AuxContractResolver]) (*UnilateralCloseSummary, + error) { // First, we'll generate the commitment point and the revocation point // so we can re-construct the HTLC state and also our payment key. @@ -6554,11 +6652,17 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, // Next, we'll obtain HTLC resolutions for all the outgoing HTLC's we // had on their commitment transaction. - var leaseExpiry uint32 + var ( + leaseExpiry uint32 + selfPoint *wire.OutPoint + localBalance int64 + isRemoteInitiator = !chanState.IsInitiator + commitTxBroadcast = commitSpend.SpendingTx + ) + if chanState.ChanType.HasLeaseExpiration() { leaseExpiry = chanState.ThawHeight } - isRemoteInitiator := !chanState.IsInitiator htlcResolutions, err := extractHtlcResolutions( chainfee.SatPerKWeight(remoteCommit.FeePerKw), commitType, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, @@ -6567,12 +6671,10 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, auxResult.AuxLeaves, ) if err != nil { - return nil, fmt.Errorf("unable to create htlc "+ - "resolutions: %v", err) + return nil, fmt.Errorf("unable to create htlc resolutions: %w", + err) } - commitTxBroadcast := commitSpend.SpendingTx - // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. @@ -6587,14 +6689,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ - "script: %v", err) + "script: %w", err) } - - var ( - selfPoint *wire.OutPoint - localBalance int64 - ) - for outputIndex, txOut := range commitTxBroadcast.TxOut { if bytes.Equal(txOut.PkScript, selfScript.PkScript()) { selfPoint = &wire.OutPoint{ @@ -6657,6 +6753,35 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, return nil, err } } + + // At this point, we'll check to see if we need any extra + // resolution data for this output. + resolveReq := ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.RemoteCommitment.CustomBlob, + FundingBlob: chanState.CustomBlob, + Type: input.TaprootRemoteCommitSpend, + CloseType: RemoteForceClose, + CommitTx: commitTxBroadcast, + ContractPoint: *selfPoint, + SignDesc: commitResolution.SelfOutputSignDesc, + KeyRing: keyRing, + CsvDelay: maturityDelay, + CommitFee: chanState.RemoteCommitment.CommitFee, + } + resolveBlob := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + return a.ResolveContract(resolveReq) + }, + ) + if err := resolveBlob.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + commitResolution.ResolutionBlob = resolveBlob.Option() } closeSummary := channeldb.ChannelCloseSummary{ @@ -7513,7 +7638,7 @@ func (lc *LightningChannel) ForceClose() (*LocalForceCloseSummary, error) { localCommitment := lc.channelState.LocalCommitment summary, err := NewLocalForceCloseSummary( lc.channelState, lc.Signer, commitTx, - localCommitment.CommitHeight, lc.leafStore, + localCommitment.CommitHeight, lc.leafStore, lc.auxResolver, ) if err != nil { return nil, fmt.Errorf("unable to gen force close "+ @@ -7531,7 +7656,9 @@ func (lc *LightningChannel) ForceClose() (*LocalForceCloseSummary, error) { // transaction corresponding to localCommit. func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, signer input.Signer, commitTx *wire.MsgTx, stateNum uint64, - leafStore fn.Option[AuxLeafStore]) (*LocalForceCloseSummary, error) { + leafStore fn.Option[AuxLeafStore], + auxResolver fn.Option[AuxContractResolver]) (*LocalForceCloseSummary, + error) { // Re-derive the original pkScript for to-self output within the // commitment transaction. We'll need this to find the corresponding @@ -7655,6 +7782,35 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, return nil, err } } + + // At this point, we'll check to see if we need any extra + // resolution data for this output. + resolveBlob := fn.MapOptionZ( + auxResolver, + func(a AuxContractResolver) fn.Result[tlv.Blob] { + //nolint:lll + return a.ResolveContract(ResolutionReq{ + ChanPoint: chanState.FundingOutpoint, + ShortChanID: chanState.ShortChanID(), + Initiator: chanState.IsInitiator, + CommitBlob: chanState.LocalCommitment.CustomBlob, + FundingBlob: chanState.CustomBlob, + Type: input.TaprootLocalCommitSpend, + CloseType: LocalForceClose, + CommitTx: commitTx, + ContractPoint: commitResolution.SelfOutPoint, + SignDesc: commitResolution.SelfOutputSignDesc, + KeyRing: keyRing, + CsvDelay: csvTimeout, + CommitFee: chanState.LocalCommitment.CommitFee, + }) + }, + ) + if err := resolveBlob.Err(); err != nil { + return nil, fmt.Errorf("unable to aux resolve: %w", err) + } + + commitResolution.ResolutionBlob = resolveBlob.Option() } // Once the delay output has been found (if it exists), then we'll also diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index c56bce23b..3adb21636 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -6043,6 +6043,7 @@ func TestChannelUnilateralCloseHtlcResolution(t *testing.T) { aliceChannel.channelState.RemoteCommitment, aliceChannel.channelState.RemoteCurrentRevocation, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err, "unable to create alice close summary") @@ -6193,6 +6194,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) { aliceChannel.channelState.RemoteCommitment, aliceChannel.channelState.RemoteCurrentRevocation, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err, "unable to create alice close summary") @@ -6211,6 +6213,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) { aliceRemoteChainTip.Commitment, aliceChannel.channelState.RemoteNextRevocation, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err, "unable to create alice close summary") @@ -7092,6 +7095,7 @@ func TestNewBreachRetributionSkipsDustHtlcs(t *testing.T) { breachRet, err := NewBreachRetribution( aliceChannel.channelState, revokedStateNum, 100, breachTx, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err, "unable to create breach retribution") @@ -10653,6 +10657,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { _, err = NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, breachTx, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) @@ -10661,6 +10666,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { _, err = NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, nil, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) @@ -10707,6 +10713,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { br, err := NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, breachTx, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err) @@ -10719,6 +10726,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { br, err = NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, nil, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.NoError(t, err) assertRetribution(br, 1, 0) @@ -10728,6 +10736,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { _, err = NewBreachRetribution( aliceChannel.channelState, stateNum+1, breachHeight, breachTx, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) @@ -10736,6 +10745,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { _, err = NewBreachRetribution( aliceChannel.channelState, stateNum+1, breachHeight, nil, fn.Some[AuxLeafStore](&MockAuxLeafStore{}), + fn.Some[AuxContractResolver](&MockAuxContractResolver{}), ) require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) } diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 82b9e19c2..f99f5c0b9 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -493,3 +493,14 @@ func (a *MockAuxSigner) VerifySecondLevelSigs(chanState AuxChanState, return args.Error(0) } + +type MockAuxContractResolver struct{} + +// ResolveContract is called to resolve a contract that needs +// additional information to resolve properly. If no extra information +// is required, a nil Result error is returned. +func (*MockAuxContractResolver) ResolveContract( + ResolutionReq) fn.Result[tlv.Blob] { + + return fn.Ok[tlv.Blob](nil) +} diff --git a/server.go b/server.go index 9a7276948..48f6ee5ed 100644 --- a/server.go +++ b/server.go @@ -1631,6 +1631,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, br, err := lnwallet.NewBreachRetribution( channel, commitHeight, 0, nil, implCfg.AuxLeafStore, + implCfg.AuxContractResolver, ) if err != nil { return nil, 0, err