lnwallet: add new aux resolver interface

This will be used by external callers to modify the way we resolve
contracts on chain. For a given contract, we'll store an extra "blob",
that will later be presented during the sweeping phase.
This commit is contained in:
Olaoluwa Osuntokun 2024-06-03 22:58:57 -07:00
parent 0f2c16de99
commit 4619cefc8f
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
8 changed files with 303 additions and 23 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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