mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
Merge pull request #9072 from lightningnetwork/extract-part3-from-staging-branch
[custom channels 3/5]: Extract PART3 from mega staging branch
This commit is contained in:
commit
cdad5d988d
@ -68,6 +68,10 @@ type Config struct {
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// BlockCache is the main cache for storing block information.
|
||||
BlockCache *blockcache.BlockCache
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
)
|
||||
|
||||
// ChannelEdgeInfo represents a fully authenticated channel along with all its
|
||||
@ -62,6 +63,11 @@ type ChannelEdgeInfo struct {
|
||||
// the value output in the outpoint that created this channel.
|
||||
Capacity btcutil.Amount
|
||||
|
||||
// TapscriptRoot is the optional Merkle root of the tapscript tree if
|
||||
// this channel is a taproot channel that also commits to a tapscript
|
||||
// tree (custom channel).
|
||||
TapscriptRoot fn.Option[chainhash.Hash]
|
||||
|
||||
// ExtraOpaqueData is the set of data that was appended to this
|
||||
// message, some of which we may not actually know how to iterate or
|
||||
// parse. By holding onto this data, we ensure that we're able to
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -167,6 +168,20 @@ type AuxComponents struct {
|
||||
// MsgRouter is an optional message router that if set will be used in
|
||||
// place of a new blank default message router.
|
||||
MsgRouter fn.Option[msgmux.Router]
|
||||
|
||||
// AuxFundingController is an optional controller that can be used to
|
||||
// modify the way we handle certain custom channel types. It's also
|
||||
// able to automatically handle new custom protocol messages related to
|
||||
// the funding process.
|
||||
AuxFundingController fn.Option[funding.AuxFundingController]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// AuxDataParser is an optional data parser that can be used to parse
|
||||
// auxiliary data for certain custom channel types.
|
||||
AuxDataParser fn.Option[AuxDataParser]
|
||||
}
|
||||
|
||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||
@ -573,6 +588,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
||||
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
||||
NeutrinoCS: neutrinoCS,
|
||||
AuxLeafStore: aux.AuxLeafStore,
|
||||
AuxSigner: aux.AuxSigner,
|
||||
ActiveNetParams: d.cfg.ActiveNetParams,
|
||||
FeeURL: d.cfg.FeeURL,
|
||||
Fee: &lncfg.Fee{
|
||||
@ -730,6 +746,7 @@ func (d *DefaultWalletImpl) BuildChainControl(
|
||||
NetParams: *walletConfig.NetParams,
|
||||
CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
|
||||
AuxLeafStore: partialChainControl.Cfg.AuxLeafStore,
|
||||
AuxSigner: partialChainControl.Cfg.AuxSigner,
|
||||
}
|
||||
|
||||
// The broadcast is already always active for neutrino nodes, so we
|
||||
@ -912,10 +929,6 @@ type DatabaseInstances struct {
|
||||
// for native SQL queries for tables that already support it. This may
|
||||
// be nil if the use-native-sql flag was not set.
|
||||
NativeSQLStore *sqldb.BaseDB
|
||||
|
||||
// AuxLeafStore is an optional data source that can be used by custom
|
||||
// channels to fetch+store various data.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
}
|
||||
|
||||
// DefaultDatabaseBuilder is a type that builds the default database backends
|
||||
|
@ -2360,9 +2360,12 @@ func createInitChannels(t *testing.T) (
|
||||
)
|
||||
bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
|
||||
|
||||
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||
alicePool := lnwallet.NewSigPool(1, aliceSigner)
|
||||
channelAlice, err := lnwallet.NewLightningChannel(
|
||||
aliceSigner, aliceChannelState, alicePool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -2375,6 +2378,8 @@ func createInitChannels(t *testing.T) (
|
||||
bobPool := lnwallet.NewSigPool(1, bobSigner)
|
||||
channelBob, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, bobChannelState, bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -221,6 +221,10 @@ type ChainArbitratorConfig struct {
|
||||
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
}
|
||||
|
||||
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all
|
||||
@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
||||
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
|
||||
chanMachine, err := lnwallet.NewLightningChannel(
|
||||
a.c.cfg.Signer, channel, nil, chanOpts...,
|
||||
@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
|
||||
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
|
||||
// Finally, we'll force close the channel completing
|
||||
// the force close workflow.
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/graph"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -82,9 +83,10 @@ var (
|
||||
// can provide that serve useful when processing a specific network
|
||||
// announcement.
|
||||
type optionalMsgFields struct {
|
||||
capacity *btcutil.Amount
|
||||
channelPoint *wire.OutPoint
|
||||
remoteAlias *lnwire.ShortChannelID
|
||||
capacity *btcutil.Amount
|
||||
channelPoint *wire.OutPoint
|
||||
remoteAlias *lnwire.ShortChannelID
|
||||
tapscriptRoot fn.Option[chainhash.Hash]
|
||||
}
|
||||
|
||||
// apply applies the optional fields within the functional options.
|
||||
@ -115,6 +117,14 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField {
|
||||
}
|
||||
}
|
||||
|
||||
// TapscriptRoot is an optional field that lets the gossiper know of the root of
|
||||
// the tapscript tree for a custom channel.
|
||||
func TapscriptRoot(root fn.Option[chainhash.Hash]) OptionalMsgField {
|
||||
return func(f *optionalMsgFields) {
|
||||
f.tapscriptRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAlias is an optional field that lets the gossiper know that a locally
|
||||
// sent channel update is actually an update for the peer that should replace
|
||||
// the ShortChannelID field with the remote's alias. This is only used for
|
||||
@ -2598,6 +2608,9 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
||||
cp := *nMsg.optionalMsgFields.channelPoint
|
||||
edge.ChannelPoint = cp
|
||||
}
|
||||
|
||||
// Optional tapscript root for custom channels.
|
||||
edge.TapscriptRoot = nMsg.optionalMsgFields.tapscriptRoot
|
||||
}
|
||||
|
||||
log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64())
|
||||
|
51
funding/aux_funding.go
Normal file
51
funding/aux_funding.go
Normal file
@ -0,0 +1,51 @@
|
||||
package funding
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/msgmux"
|
||||
)
|
||||
|
||||
// AuxFundingDescResult is a type alias for a function that returns an optional
|
||||
// aux funding desc.
|
||||
type AuxFundingDescResult = fn.Result[fn.Option[lnwallet.AuxFundingDesc]]
|
||||
|
||||
// AuxTapscriptResult is a type alias for a function that returns an optional
|
||||
// tapscript root.
|
||||
type AuxTapscriptResult = fn.Result[fn.Option[chainhash.Hash]]
|
||||
|
||||
// AuxFundingController permits the implementation of the funding of custom
|
||||
// channels types. The controller serves as a MsgEndpoint which allows it to
|
||||
// intercept custom messages, or even the regular funding messages. The
|
||||
// controller might also pass along an aux funding desc based on an existing
|
||||
// pending channel ID.
|
||||
type AuxFundingController interface {
|
||||
// Endpoint is the embedded interface that signals that the funding
|
||||
// controller is also a message endpoint. This'll allow it to handle
|
||||
// custom messages specific to the funding type.
|
||||
msgmux.Endpoint
|
||||
|
||||
// DescFromPendingChanID takes a pending channel ID, that may already be
|
||||
// known due to prior custom channel messages, and maybe returns an aux
|
||||
// funding desc which can be used to modify how a channel is funded.
|
||||
DescFromPendingChanID(pid PendingChanID, openChan lnwallet.AuxChanState,
|
||||
keyRing lntypes.Dual[lnwallet.CommitmentKeyRing],
|
||||
initiator bool) AuxFundingDescResult
|
||||
|
||||
// DeriveTapscriptRoot takes a pending channel ID and maybe returns a
|
||||
// tapscript root that should be used when creating any MuSig2 sessions
|
||||
// for a channel.
|
||||
DeriveTapscriptRoot(PendingChanID) AuxTapscriptResult
|
||||
|
||||
// ChannelReady is called when a channel has been fully opened (multiple
|
||||
// confirmations) and is ready to be used. This can be used to perform
|
||||
// any final setup or cleanup.
|
||||
ChannelReady(openChan lnwallet.AuxChanState) error
|
||||
|
||||
// ChannelFinalized is called when a channel has been fully finalized.
|
||||
// In this state, we've received the commitment sig from the remote
|
||||
// party, so we are safe to broadcast the funding transaction.
|
||||
ChannelFinalized(PendingChanID) error
|
||||
}
|
@ -99,7 +99,6 @@ const (
|
||||
// you and limitless channel size (apart from 21 million cap).
|
||||
MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000)
|
||||
|
||||
// TODO(roasbeef): tune.
|
||||
msgBufferSize = 50
|
||||
|
||||
// MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait
|
||||
@ -549,6 +548,16 @@ type Config struct {
|
||||
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxFundingController is an optional controller that can be used to
|
||||
// modify the way we handle certain custom channel types. It's also
|
||||
// able to automatically handle new custom protocol messages related to
|
||||
// the funding process.
|
||||
AuxFundingController fn.Option[AuxFundingController]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
}
|
||||
|
||||
// Manager acts as an orchestrator/bridge between the wallet's
|
||||
@ -1078,6 +1087,9 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
||||
f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
|
||||
// We create the state-machine object which wraps the database state.
|
||||
lnChannel, err := lnwallet.NewLightningChannel(
|
||||
@ -1256,8 +1268,8 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel,
|
||||
|
||||
// advancePendingChannelState waits for a pending channel's funding tx to
|
||||
// confirm, and marks it open in the database when that happens.
|
||||
func (f *Manager) advancePendingChannelState(
|
||||
channel *channeldb.OpenChannel, pendingChanID PendingChanID) error {
|
||||
func (f *Manager) advancePendingChannelState(channel *channeldb.OpenChannel,
|
||||
pendingChanID PendingChanID) error {
|
||||
|
||||
if channel.IsZeroConf() {
|
||||
// Persist the alias to the alias database.
|
||||
@ -1626,6 +1638,23 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, if we have an AuxFundingController active, we'll
|
||||
// check to see if we have a special tapscript root to use in our
|
||||
// MuSig funding output.
|
||||
tapscriptRoot, err := fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(c AuxFundingController) AuxTapscriptResult {
|
||||
return c.DeriveTapscriptRoot(msg.PendingChannelID)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error deriving tapscript root: %w", err)
|
||||
log.Error(err)
|
||||
f.failFundingFlow(peer, cid, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
req := &lnwallet.InitFundingReserveMsg{
|
||||
ChainHash: &msg.ChainHash,
|
||||
PendingChanID: msg.PendingChannelID,
|
||||
@ -1642,6 +1671,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
|
||||
ZeroConf: zeroConf,
|
||||
OptionScidAlias: scid,
|
||||
ScidAliasFeature: scidFeatureVal,
|
||||
TapscriptRoot: tapscriptRoot,
|
||||
}
|
||||
|
||||
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
|
||||
@ -1898,6 +1928,8 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
|
||||
log.Debugf("Remote party accepted commitment rendering params: %v",
|
||||
lnutils.SpewLogClosure(params))
|
||||
|
||||
reservation.SetState(lnwallet.SentAcceptChannel)
|
||||
|
||||
// With the initiator's contribution recorded, respond with our
|
||||
// contribution in the next message of the workflow.
|
||||
fundingAccept := lnwire.AcceptChannel{
|
||||
@ -1958,6 +1990,10 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
|
||||
// Update the timestamp once the fundingAcceptMsg has been handled.
|
||||
defer resCtx.updateTimestamp()
|
||||
|
||||
if resCtx.reservation.State() != lnwallet.SentOpenChannel {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Recv'd fundingResponse for pending_id(%x)",
|
||||
pendingChanID[:])
|
||||
|
||||
@ -2261,10 +2297,34 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent,
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, we'll see if there's an AuxFundingDesc we
|
||||
// need to deliver so the funding process can continue
|
||||
// properly.
|
||||
auxFundingDesc, err := fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(c AuxFundingController) AuxFundingDescResult {
|
||||
return c.DescFromPendingChanID(
|
||||
cid.tempChanID,
|
||||
lnwallet.NewAuxChanState(
|
||||
resCtx.reservation.ChanState(),
|
||||
),
|
||||
resCtx.reservation.CommitmentKeyRings(),
|
||||
true,
|
||||
)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
failFlow("error continuing PSBT flow", err)
|
||||
return
|
||||
}
|
||||
|
||||
// A non-nil error means we can continue the funding flow.
|
||||
// Notify the wallet so it can prepare everything we need to
|
||||
// continue.
|
||||
err = resCtx.reservation.ProcessPsbt()
|
||||
//
|
||||
// We'll also pass along the aux funding controller as well,
|
||||
// which may be used to help process the finalized PSBT.
|
||||
err = resCtx.reservation.ProcessPsbt(auxFundingDesc)
|
||||
if err != nil {
|
||||
failFlow("error continuing PSBT flow", err)
|
||||
return
|
||||
@ -2359,6 +2419,8 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx,
|
||||
}
|
||||
}
|
||||
|
||||
resCtx.reservation.SetState(lnwallet.SentFundingCreated)
|
||||
|
||||
if err := resCtx.peer.SendMessage(true, fundingCreated); err != nil {
|
||||
log.Errorf("Unable to send funding complete message: %v", err)
|
||||
f.failFundingFlow(resCtx.peer, cid, err)
|
||||
@ -2390,11 +2452,14 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
|
||||
// final funding transaction, as well as a signature for our version of
|
||||
// the commitment transaction. So at this point, we can validate the
|
||||
// initiator's commitment transaction, then send our own if it's valid.
|
||||
// TODO(roasbeef): make case (p vs P) consistent throughout
|
||||
fundingOut := msg.FundingPoint
|
||||
log.Infof("completing pending_id(%x) with ChannelPoint(%v)",
|
||||
pendingChanID[:], fundingOut)
|
||||
|
||||
if resCtx.reservation.State() != lnwallet.SentAcceptChannel {
|
||||
return
|
||||
}
|
||||
|
||||
// Create the channel identifier without setting the active channel ID.
|
||||
cid := newChanIdentifier(pendingChanID)
|
||||
|
||||
@ -2422,16 +2487,38 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we'll see if there's an AuxFundingDesc we need to
|
||||
// deliver so the funding process can continue properly.
|
||||
auxFundingDesc, err := fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(c AuxFundingController) AuxFundingDescResult {
|
||||
return c.DescFromPendingChanID(
|
||||
cid.tempChanID, lnwallet.NewAuxChanState(
|
||||
resCtx.reservation.ChanState(),
|
||||
), resCtx.reservation.CommitmentKeyRings(),
|
||||
true,
|
||||
)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
log.Errorf("error continuing PSBT flow: %v", err)
|
||||
f.failFundingFlow(peer, cid, err)
|
||||
return
|
||||
}
|
||||
|
||||
// With all the necessary data available, attempt to advance the
|
||||
// funding workflow to the next stage. If this succeeds then the
|
||||
// funding transaction will broadcast after our next message.
|
||||
// CompleteReservationSingle will also mark the channel as 'IsPending'
|
||||
// in the database.
|
||||
//
|
||||
// We'll also directly pass in the AuxFunding controller as well,
|
||||
// which may be used by the reservation system to finalize funding our
|
||||
// side.
|
||||
completeChan, err := resCtx.reservation.CompleteReservationSingle(
|
||||
&fundingOut, commitSig,
|
||||
&fundingOut, commitSig, auxFundingDesc,
|
||||
)
|
||||
if err != nil {
|
||||
// TODO(roasbeef): better error logging: peerID, channelID, etc.
|
||||
log.Errorf("unable to complete single reservation: %v", err)
|
||||
f.failFundingFlow(peer, cid, err)
|
||||
return
|
||||
@ -2632,6 +2719,14 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
|
||||
return
|
||||
}
|
||||
|
||||
if resCtx.reservation.State() != lnwallet.SentFundingCreated {
|
||||
err := fmt.Errorf("unable to find reservation for chan_id=%x",
|
||||
msg.ChanID)
|
||||
f.failFundingFlow(peer, cid, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Create an entry in the local discovery map so we can ensure that we
|
||||
// process the channel confirmation fully before we receive a
|
||||
// channel_ready message.
|
||||
@ -2727,6 +2822,21 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
|
||||
}
|
||||
}
|
||||
|
||||
// Before we proceed, if we have a funding hook that wants a
|
||||
// notification that it's safe to broadcast the funding transaction,
|
||||
// then we'll send that now.
|
||||
err = fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(controller AuxFundingController) error {
|
||||
return controller.ChannelFinalized(cid.tempChanID)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to inform aux funding controller about "+
|
||||
"ChannelPoint(%v) being finalized: %v", fundingPoint,
|
||||
err)
|
||||
}
|
||||
|
||||
// Now that we have a finalized reservation for this funding flow,
|
||||
// we'll send the to be active channel to the ChainArbitrator so it can
|
||||
// watch for any on-chain actions before the channel has fully
|
||||
@ -2742,9 +2852,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
|
||||
|
||||
// Send an update to the upstream client that the negotiation process
|
||||
// is over.
|
||||
//
|
||||
// TODO(roasbeef): add abstraction over updates to accommodate
|
||||
// long-polling, or SSE, etc.
|
||||
upd := &lnrpc.OpenStatusUpdate{
|
||||
Update: &lnrpc.OpenStatusUpdate_ChanPending{
|
||||
ChanPending: &lnrpc.PendingUpdate{
|
||||
@ -3450,6 +3557,7 @@ func (f *Manager) addToGraph(completeChan *channeldb.OpenChannel,
|
||||
errChan := f.cfg.SendAnnouncement(
|
||||
ann.chanAnn, discovery.ChannelCapacity(completeChan.Capacity),
|
||||
discovery.ChannelPoint(completeChan.FundingOutpoint),
|
||||
discovery.TapscriptRoot(completeChan.TapscriptRoot),
|
||||
)
|
||||
select {
|
||||
case err := <-errChan:
|
||||
@ -3646,7 +3754,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel,
|
||||
|
||||
// waitForZeroConfChannel is called when the state is addedToGraph with
|
||||
// a zero-conf channel. This will wait for the real confirmation, add the
|
||||
// confirmed SCID to the graph, and then announce after six confs.
|
||||
// confirmed SCID to the router graph, and then announce after six confs.
|
||||
func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error {
|
||||
// First we'll check whether the channel is confirmed on-chain. If it
|
||||
// is already confirmed, the chainntnfs subsystem will return with the
|
||||
@ -3977,6 +4085,26 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
|
||||
PubNonce: remoteNonce,
|
||||
}),
|
||||
)
|
||||
|
||||
// Inform the aux funding controller that the liquidity in the
|
||||
// custom channel is now ready to be advertised. We potentially
|
||||
// haven't sent our own channel ready message yet, but other
|
||||
// than that the channel is ready to count toward available
|
||||
// liquidity.
|
||||
err = fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(controller AuxFundingController) error {
|
||||
return controller.ChannelReady(
|
||||
lnwallet.NewAuxChanState(channel),
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
cid := newChanIdentifier(msg.ChanID)
|
||||
f.sendWarning(peer, cid, err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The channel_ready message contains the next commitment point we'll
|
||||
@ -4063,6 +4191,19 @@ func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
|
||||
log.Debugf("Channel(%v) with ShortChanID %v: successfully "+
|
||||
"added to graph", chanID, scid)
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(controller AuxFundingController) error {
|
||||
return controller.ChannelReady(
|
||||
lnwallet.NewAuxChanState(channel),
|
||||
)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed notifying aux funding controller "+
|
||||
"about channel ready: %w", err)
|
||||
}
|
||||
|
||||
// Give the caller a final update notifying them that the channel is
|
||||
fundingPoint := channel.FundingOutpoint
|
||||
cp := &lnrpc.ChannelPoint{
|
||||
@ -4376,9 +4517,9 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
|
||||
//
|
||||
// We can pass in zeroes for the min and max htlc policy, because we
|
||||
// only use the channel announcement message from the returned struct.
|
||||
ann, err := f.newChanAnnouncement(localIDKey, remoteIDKey,
|
||||
localFundingKey, remoteFundingKey, shortChanID, chanID,
|
||||
0, 0, nil, chanType,
|
||||
ann, err := f.newChanAnnouncement(
|
||||
localIDKey, remoteIDKey, localFundingKey, remoteFundingKey,
|
||||
shortChanID, chanID, 0, 0, nil, chanType,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("can't generate channel announcement: %v", err)
|
||||
@ -4444,7 +4585,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
|
||||
|
||||
// InitFundingWorkflow sends a message to the funding manager instructing it
|
||||
// to initiate a single funder workflow with the source peer.
|
||||
// TODO(roasbeef): re-visit blocking nature..
|
||||
func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) {
|
||||
f.fundingRequests <- msg
|
||||
}
|
||||
@ -4634,6 +4774,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
|
||||
scidFeatureVal = true
|
||||
}
|
||||
|
||||
// At this point, if we have an AuxFundingController active, we'll check
|
||||
// to see if we have a special tapscript root to use in our MuSig2
|
||||
// funding output.
|
||||
tapscriptRoot, err := fn.MapOptionZ(
|
||||
f.cfg.AuxFundingController,
|
||||
func(c AuxFundingController) AuxTapscriptResult {
|
||||
return c.DeriveTapscriptRoot(chanID)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error deriving tapscript root: %w", err)
|
||||
log.Error(err)
|
||||
msg.Err <- err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
req := &lnwallet.InitFundingReserveMsg{
|
||||
ChainHash: &msg.ChainHash,
|
||||
PendingChanID: chanID,
|
||||
@ -4673,6 +4830,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
|
||||
OptionScidAlias: scid,
|
||||
ScidAliasFeature: scidFeatureVal,
|
||||
Memo: msg.Memo,
|
||||
TapscriptRoot: tapscriptRoot,
|
||||
}
|
||||
|
||||
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
|
||||
@ -4824,6 +4982,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
|
||||
log.Infof("Starting funding workflow with %v for pending_id(%x), "+
|
||||
"committype=%v", msg.Peer.Address(), chanID, commitType)
|
||||
|
||||
reservation.SetState(lnwallet.SentOpenChannel)
|
||||
|
||||
fundingOpen := lnwire.OpenChannel{
|
||||
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
|
||||
PendingChannelID: chanID,
|
||||
|
@ -567,6 +567,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
|
||||
AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](
|
||||
&lnwallet.MockAuxLeafStore{},
|
||||
),
|
||||
AuxSigner: fn.Some[lnwallet.AuxSigner](
|
||||
&lnwallet.MockAuxSigner{},
|
||||
),
|
||||
}
|
||||
|
||||
for _, op := range options {
|
||||
@ -677,6 +680,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
|
||||
DeleteAliasEdge: oldCfg.DeleteAliasEdge,
|
||||
AliasManager: oldCfg.AliasManager,
|
||||
AuxLeafStore: oldCfg.AuxLeafStore,
|
||||
AuxSigner: oldCfg.AuxSigner,
|
||||
})
|
||||
require.NoError(t, err, "failed recreating aliceFundingManager")
|
||||
|
||||
|
@ -1093,8 +1093,8 @@ func (b *Builder) addZombieEdge(chanID uint64) error {
|
||||
// segwit v1 (taproot) channels.
|
||||
//
|
||||
// TODO(roasbeef: export and use elsewhere?
|
||||
func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
|
||||
chanFeatures []byte) ([]byte, error) {
|
||||
func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, chanFeatures []byte,
|
||||
tapscriptRoot fn.Option[chainhash.Hash]) ([]byte, error) {
|
||||
|
||||
legacyFundingScript := func() ([]byte, error) {
|
||||
witnessScript, err := input.GenMultiSigScript(
|
||||
@ -1141,7 +1141,7 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
|
||||
}
|
||||
|
||||
fundingScript, _, err := input.GenTaprootFundingScript(
|
||||
pubKey1, pubKey2, 0, fn.None[chainhash.Hash](),
|
||||
pubKey1, pubKey2, 0, tapscriptRoot,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1275,7 +1275,7 @@ func (b *Builder) processUpdate(msg interface{},
|
||||
// reality.
|
||||
fundingPkScript, err := makeFundingScript(
|
||||
msg.BitcoinKey1Bytes[:], msg.BitcoinKey2Bytes[:],
|
||||
msg.Features,
|
||||
msg.Features, msg.TapscriptRoot,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2192,10 +2192,20 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
|
||||
// We just received a new updates to our local commitment
|
||||
// chain, validate this new commitment, closing the link if
|
||||
// invalid.
|
||||
auxSigBlob, err := msg.CustomRecords.Serialize()
|
||||
if err != nil {
|
||||
l.failf(
|
||||
LinkFailureError{code: ErrInvalidCommitment},
|
||||
"unable to serialize custom records: %v", err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{
|
||||
CommitSig: msg.CommitSig,
|
||||
HtlcSigs: msg.HtlcSigs,
|
||||
PartialSig: msg.PartialSig,
|
||||
AuxSigBlob: auxSigBlob,
|
||||
})
|
||||
if err != nil {
|
||||
// If we were unable to reconstruct their proposed
|
||||
@ -2622,11 +2632,17 @@ func (l *channelLink) updateCommitTx() error {
|
||||
default:
|
||||
}
|
||||
|
||||
auxBlobRecords, err := lnwire.ParseCustomRecords(newCommit.AuxSigBlob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing aux sigs: %w", err)
|
||||
}
|
||||
|
||||
commitSig := &lnwire.CommitSig{
|
||||
ChanID: l.ChanID(),
|
||||
CommitSig: newCommit.CommitSig,
|
||||
HtlcSigs: newCommit.HtlcSigs,
|
||||
PartialSig: newCommit.PartialSig,
|
||||
ChanID: l.ChanID(),
|
||||
CommitSig: newCommit.CommitSig,
|
||||
HtlcSigs: newCommit.HtlcSigs,
|
||||
PartialSig: newCommit.PartialSig,
|
||||
CustomRecords: auxBlobRecords,
|
||||
}
|
||||
l.cfg.Peer.SendMessage(false, commitSig)
|
||||
|
||||
@ -3778,7 +3794,18 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC,
|
||||
// As we're the exit hop, we'll double check the hop-payload included in
|
||||
// the HTLC to ensure that it was crafted correctly by the sender and
|
||||
// is compatible with the HTLC we were extended.
|
||||
if add.Amount < fwdInfo.AmountToForward {
|
||||
//
|
||||
// For a special case, if the fwdInfo doesn't have any blinded path
|
||||
// information, and the incoming HTLC had special extra data, then
|
||||
// we'll skip this amount check. The invoice acceptor will make sure we
|
||||
// reject the HTLC if it's not containing the correct amount after
|
||||
// examining the custom data.
|
||||
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
|
||||
customHTLC := len(add.CustomRecords) > 0 && !hasBlindedPath
|
||||
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
|
||||
hasBlindedPath, customHTLC)
|
||||
|
||||
if !customHTLC && add.Amount < fwdInfo.AmountToForward {
|
||||
l.log.Errorf("onion payload of incoming htlc(%x) has "+
|
||||
"incompatible value: expected <=%v, got %v",
|
||||
add.PaymentHash, add.Amount, fwdInfo.AmountToForward)
|
||||
|
@ -268,9 +268,12 @@ func TestChannelLinkRevThenSig(t *testing.T) {
|
||||
|
||||
// Restart Bob as well by calling NewLightningChannel.
|
||||
bobSigner := harness.bobChannel.Signer
|
||||
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
|
||||
bobChannel, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, harness.bobChannel.State(), bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = bobPool.Start()
|
||||
@ -403,9 +406,12 @@ func TestChannelLinkSigThenRev(t *testing.T) {
|
||||
|
||||
// Restart Bob as well by calling NewLightningChannel.
|
||||
bobSigner := harness.bobChannel.Signer
|
||||
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
|
||||
bobChannel, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, harness.bobChannel.State(), bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = bobPool.Start()
|
||||
|
@ -353,8 +353,11 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
|
||||
)
|
||||
|
||||
alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner)
|
||||
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||
channelAlice, err := lnwallet.NewLightningChannel(
|
||||
aliceSigner, aliceChannelState, alicePool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -364,6 +367,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
|
||||
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
|
||||
channelBob, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, bobChannelState, bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -425,6 +430,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
|
||||
|
||||
newAliceChannel, err := lnwallet.NewLightningChannel(
|
||||
aliceSigner, aliceStoredChannel, alicePool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("unable to create new channel: %v",
|
||||
@ -471,6 +478,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
|
||||
|
||||
newBobChannel, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, bobStoredChannel, bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("unable to create new channel: %v",
|
||||
|
@ -177,6 +177,17 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode,
|
||||
},
|
||||
)
|
||||
|
||||
// If this is a taproot channel, then we'll decode the PSBT to assert
|
||||
// that an internal key is included.
|
||||
if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT {
|
||||
decodedPSBT, err := psbt.NewFromRawBytes(
|
||||
bytes.NewReader(tempPsbt), false,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32)
|
||||
}
|
||||
|
||||
// Let's add a second channel to the batch. This time between Carol and
|
||||
// Alice. We will publish the batch TX once this channel funding is
|
||||
// complete.
|
||||
|
@ -4703,6 +4703,8 @@ type Channel struct {
|
||||
// useful information. This is only ever stored locally and in no way impacts
|
||||
// the channel's operation.
|
||||
Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"`
|
||||
// Custom channel data that might be populated in custom channels.
|
||||
CustomChannelData []byte `protobuf:"bytes,37,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Channel) Reset() {
|
||||
@ -4993,6 +4995,13 @@ func (x *Channel) GetMemo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Channel) GetCustomChannelData() []byte {
|
||||
if x != nil {
|
||||
return x.CustomChannelData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListChannelsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@ -9505,6 +9514,9 @@ type ChannelBalanceResponse struct {
|
||||
PendingOpenLocalBalance *Amount `protobuf:"bytes,7,opt,name=pending_open_local_balance,json=pendingOpenLocalBalance,proto3" json:"pending_open_local_balance,omitempty"`
|
||||
// Sum of channels pending remote balances.
|
||||
PendingOpenRemoteBalance *Amount `protobuf:"bytes,8,opt,name=pending_open_remote_balance,json=pendingOpenRemoteBalance,proto3" json:"pending_open_remote_balance,omitempty"`
|
||||
// Custom channel data that might be populated if there are custom channels
|
||||
// present.
|
||||
CustomChannelData []byte `protobuf:"bytes,9,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ChannelBalanceResponse) Reset() {
|
||||
@ -9597,6 +9609,13 @@ func (x *ChannelBalanceResponse) GetPendingOpenRemoteBalance() *Amount {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ChannelBalanceResponse) GetCustomChannelData() []byte {
|
||||
if x != nil {
|
||||
return x.CustomChannelData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type QueryRoutesRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@ -17642,6 +17661,8 @@ type PendingChannelsResponse_PendingChannel struct {
|
||||
// useful information. This is only ever stored locally and in no way
|
||||
// impacts the channel's operation.
|
||||
Memo string `protobuf:"bytes,13,opt,name=memo,proto3" json:"memo,omitempty"`
|
||||
// Custom channel data that might be populated in custom channels.
|
||||
CustomChannelData []byte `protobuf:"bytes,34,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
|
||||
}
|
||||
|
||||
func (x *PendingChannelsResponse_PendingChannel) Reset() {
|
||||
@ -17767,6 +17788,13 @@ func (x *PendingChannelsResponse_PendingChannel) GetMemo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PendingChannelsResponse_PendingChannel) GetCustomChannelData() []byte {
|
||||
if x != nil {
|
||||
return x.CustomChannelData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PendingChannelsResponse_PendingOpenChannel struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@ -18652,7 +18680,7 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x61, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74,
|
||||
0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10,
|
||||
0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73,
|
||||
0x22, 0xad, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06,
|
||||
0x22, 0xdd, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06,
|
||||
0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63,
|
||||
0x74, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70,
|
||||
0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6d,
|
||||
@ -18743,6 +18771,9 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x69, 0x61, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0d, 0x70,
|
||||
0x65, 0x65, 0x72, 0x53, 0x63, 0x69, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f,
|
||||
0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63,
|
||||
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61,
|
||||
0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
|
||||
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69,
|
||||
0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61,
|
||||
@ -19326,7 +19357,7 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a,
|
||||
0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x61, 0x77, 0x5f, 0x74, 0x78, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x61,
|
||||
0x77, 0x54, 0x78, 0x22, 0xe1, 0x13, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43,
|
||||
0x77, 0x54, 0x78, 0x22, 0x91, 0x14, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43,
|
||||
0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x62, 0x6f, 0x5f, 0x62,
|
||||
0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x74, 0x6f,
|
||||
@ -19358,7 +19389,7 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x69,
|
||||
0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x14,
|
||||
0x77, 0x61, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e,
|
||||
0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xb3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67,
|
||||
0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xe3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67,
|
||||
0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74,
|
||||
0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x12,
|
||||
@ -19393,7 +19424,10 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07,
|
||||
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70,
|
||||
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x0d,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75,
|
||||
0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74,
|
||||
0x61, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43,
|
||||
0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50,
|
||||
0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65,
|
||||
0x6c, 0x12, 0x47, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69,
|
||||
@ -19571,7 +19605,7 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x04, 0x52, 0x03, 0x73, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x68,
|
||||
0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x22, 0x80, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42,
|
||||
0x65, 0x73, 0x74, 0x22, 0xb0, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42,
|
||||
0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c,
|
||||
0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42,
|
||||
0x02, 0x18, 0x01, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x14,
|
||||
@ -19603,7 +19637,10 @@ var file_lightning_proto_rawDesc = []byte{
|
||||
0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e,
|
||||
0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x18, 0x70, 0x65,
|
||||
0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x42,
|
||||
0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79,
|
||||
0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
|
||||
0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79,
|
||||
0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20,
|
||||
|
@ -1592,6 +1592,11 @@ message Channel {
|
||||
the channel's operation.
|
||||
*/
|
||||
string memo = 36;
|
||||
|
||||
/*
|
||||
Custom channel data that might be populated in custom channels.
|
||||
*/
|
||||
bytes custom_channel_data = 37;
|
||||
}
|
||||
|
||||
message ListChannelsRequest {
|
||||
@ -2709,6 +2714,11 @@ message PendingChannelsResponse {
|
||||
impacts the channel's operation.
|
||||
*/
|
||||
string memo = 13;
|
||||
|
||||
/*
|
||||
Custom channel data that might be populated in custom channels.
|
||||
*/
|
||||
bytes custom_channel_data = 34;
|
||||
}
|
||||
|
||||
message PendingOpenChannel {
|
||||
@ -2968,6 +2978,12 @@ message ChannelBalanceResponse {
|
||||
|
||||
// Sum of channels pending remote balances.
|
||||
Amount pending_open_remote_balance = 8;
|
||||
|
||||
/*
|
||||
Custom channel data that might be populated if there are custom channels
|
||||
present.
|
||||
*/
|
||||
bytes custom_channel_data = 9;
|
||||
}
|
||||
|
||||
message QueryRoutesRequest {
|
||||
|
@ -3127,6 +3127,11 @@
|
||||
"memo": {
|
||||
"type": "string",
|
||||
"description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way\nimpacts the channel's operation."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Custom channel data that might be populated in custom channels."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3849,6 +3854,11 @@
|
||||
"memo": {
|
||||
"type": "string",
|
||||
"description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Custom channel data that might be populated in custom channels."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4052,6 +4062,11 @@
|
||||
"pending_open_remote_balance": {
|
||||
"$ref": "#/definitions/lnrpcAmount",
|
||||
"description": "Sum of channels pending remote balances."
|
||||
},
|
||||
"custom_channel_data": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Custom channel data that might be populated if there are custom channels\npresent."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/subscribe"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -104,6 +105,10 @@ type RouterBackend struct {
|
||||
// TODO(yy): remove this config after the new status code is fully
|
||||
// deployed to the network(v0.20.0).
|
||||
UseStatusInitiated bool
|
||||
|
||||
// ParseCustomChannelData is a function that can be used to parse custom
|
||||
// channel data from the first hop of a route.
|
||||
ParseCustomChannelData func(message proto.Message) error
|
||||
}
|
||||
|
||||
// MissionControl defines the mission control dependencies of routerrpc.
|
||||
@ -596,8 +601,14 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
|
||||
|
||||
resp.CustomChannelData = customData
|
||||
|
||||
// TODO(guggero): Feed the route into the custom data parser
|
||||
// (part 3 of the mega PR series).
|
||||
// Allow the aux data parser to parse the custom records into
|
||||
// a human-readable JSON (if available).
|
||||
if r.ParseCustomChannelData != nil {
|
||||
err := r.ParseCustomChannelData(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
incomingAmt := route.TotalAmount
|
||||
|
@ -964,6 +964,11 @@ func (h *HarnessTest) AssertChannelBalanceResp(hn *node.HarnessNode,
|
||||
expected *lnrpc.ChannelBalanceResponse) {
|
||||
|
||||
resp := hn.RPC.ChannelBalance()
|
||||
|
||||
// Ignore custom channel data of both expected and actual responses.
|
||||
expected.CustomChannelData = nil
|
||||
resp.CustomChannelData = nil
|
||||
|
||||
require.True(h, proto.Equal(expected, resp), "balance is incorrect "+
|
||||
"got: %v, want: %v", resp, expected)
|
||||
}
|
||||
|
250
lnwallet/aux_signer.go
Normal file
250
lnwallet/aux_signer.go
Normal file
@ -0,0 +1,250 @@
|
||||
package lnwallet
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// htlcCustomSigType is the TLV type that is used to encode the custom HTLC
|
||||
// signatures within the custom data for an existing HTLC.
|
||||
var htlcCustomSigType tlv.TlvType65543
|
||||
|
||||
// AuxHtlcDescriptor is a struct that contains the information needed to sign or
|
||||
// verify an HTLC for custom channels.
|
||||
type AuxHtlcDescriptor struct {
|
||||
// ChanID is the ChannelID of the LightningChannel that this
|
||||
// paymentDescriptor belongs to. We track this here so we can
|
||||
// reconstruct the Messages that this paymentDescriptor is built from.
|
||||
ChanID lnwire.ChannelID
|
||||
|
||||
// RHash is the payment hash for this HTLC. The HTLC can be settled iff
|
||||
// the preimage to this hash is presented.
|
||||
RHash PaymentHash
|
||||
|
||||
// Timeout is the absolute timeout in blocks, after which this HTLC
|
||||
// expires.
|
||||
Timeout uint32
|
||||
|
||||
// Amount is the HTLC amount in milli-satoshis.
|
||||
Amount lnwire.MilliSatoshi
|
||||
|
||||
// HtlcIndex is the index within the main update log for this HTLC.
|
||||
// Entries within the log of type Add will have this field populated,
|
||||
// as other entries will point to the entry via this counter.
|
||||
//
|
||||
// NOTE: This field will only be populated if EntryType is Add.
|
||||
HtlcIndex uint64
|
||||
|
||||
// ParentIndex is the HTLC index of the entry that this update settles
|
||||
// or times out.
|
||||
//
|
||||
// NOTE: This field will only be populated if EntryType is Fail or
|
||||
// Settle.
|
||||
ParentIndex uint64
|
||||
|
||||
// EntryType denotes the exact type of the paymentDescriptor. In the
|
||||
// case of a Timeout, or Settle type, then the Parent field will point
|
||||
// into the log to the HTLC being modified.
|
||||
EntryType updateType
|
||||
|
||||
// CustomRecords also stores the set of optional custom records that
|
||||
// may have been attached to a sent HTLC.
|
||||
CustomRecords lnwire.CustomRecords
|
||||
|
||||
// addCommitHeight[Remote|Local] encodes the height of the commitment
|
||||
// which included this HTLC on either the remote or local commitment
|
||||
// chain. This value is used to determine when an HTLC is fully
|
||||
// "locked-in".
|
||||
addCommitHeightRemote uint64
|
||||
addCommitHeightLocal uint64
|
||||
|
||||
// removeCommitHeight[Remote|Local] encodes the height of the
|
||||
// commitment which removed the parent pointer of this
|
||||
// paymentDescriptor either due to a timeout or a settle. Once both
|
||||
// these heights are below the tail of both chains, the log entries can
|
||||
// safely be removed.
|
||||
removeCommitHeightRemote uint64
|
||||
removeCommitHeightLocal uint64
|
||||
}
|
||||
|
||||
// AddHeight returns the height at which the HTLC was added to the commitment
|
||||
// chain. The height is returned based on the chain the HTLC is being added to
|
||||
// (local or remote chain).
|
||||
func (a *AuxHtlcDescriptor) AddHeight(
|
||||
whoseCommitChain lntypes.ChannelParty) uint64 {
|
||||
|
||||
if whoseCommitChain.IsRemote() {
|
||||
return a.addCommitHeightRemote
|
||||
}
|
||||
|
||||
return a.addCommitHeightLocal
|
||||
}
|
||||
|
||||
// RemoveHeight returns the height at which the HTLC was removed from the
|
||||
// commitment chain. The height is returned based on the chain the HTLC is being
|
||||
// removed from (local or remote chain).
|
||||
func (a *AuxHtlcDescriptor) RemoveHeight(
|
||||
whoseCommitChain lntypes.ChannelParty) uint64 {
|
||||
|
||||
if whoseCommitChain.IsRemote() {
|
||||
return a.removeCommitHeightRemote
|
||||
}
|
||||
|
||||
return a.removeCommitHeightLocal
|
||||
}
|
||||
|
||||
// newAuxHtlcDescriptor creates a new AuxHtlcDescriptor from a payment
|
||||
// descriptor.
|
||||
func newAuxHtlcDescriptor(p *paymentDescriptor) AuxHtlcDescriptor {
|
||||
return AuxHtlcDescriptor{
|
||||
ChanID: p.ChanID,
|
||||
RHash: p.RHash,
|
||||
Timeout: p.Timeout,
|
||||
Amount: p.Amount,
|
||||
HtlcIndex: p.HtlcIndex,
|
||||
ParentIndex: p.ParentIndex,
|
||||
EntryType: p.EntryType,
|
||||
CustomRecords: p.CustomRecords.Copy(),
|
||||
addCommitHeightRemote: p.addCommitHeightRemote,
|
||||
addCommitHeightLocal: p.addCommitHeightLocal,
|
||||
removeCommitHeightRemote: p.removeCommitHeightRemote,
|
||||
removeCommitHeightLocal: p.removeCommitHeightLocal,
|
||||
}
|
||||
}
|
||||
|
||||
// BaseAuxJob is a struct that contains the common fields that are shared among
|
||||
// the aux sign/verify jobs.
|
||||
type BaseAuxJob struct {
|
||||
// OutputIndex is the output index of the HTLC on the commitment
|
||||
// transaction being signed.
|
||||
//
|
||||
// NOTE: If the output is dust from the PoV of the commitment chain,
|
||||
// then this value will be -1.
|
||||
OutputIndex int32
|
||||
|
||||
// KeyRing is the commitment key ring that contains the keys needed to
|
||||
// generate the second level HTLC signatures.
|
||||
KeyRing CommitmentKeyRing
|
||||
|
||||
// HTLC is the HTLC that is being signed or verified.
|
||||
HTLC AuxHtlcDescriptor
|
||||
|
||||
// Incoming is a boolean that indicates if the HTLC is incoming or
|
||||
// outgoing.
|
||||
Incoming bool
|
||||
|
||||
// CommitBlob is the commitment transaction blob that contains the aux
|
||||
// information for this channel.
|
||||
CommitBlob fn.Option[tlv.Blob]
|
||||
|
||||
// HtlcLeaf is the aux tap leaf that corresponds to the HTLC being
|
||||
// signed/verified.
|
||||
HtlcLeaf input.AuxTapLeaf
|
||||
}
|
||||
|
||||
// AuxSigJob is a struct that contains all the information needed to sign an
|
||||
// HTLC for custom channels.
|
||||
type AuxSigJob struct {
|
||||
// SignDesc is the sign desc for this HTLC.
|
||||
SignDesc input.SignDescriptor
|
||||
|
||||
BaseAuxJob
|
||||
|
||||
// Resp is a channel that will be used to send the result of the sign
|
||||
// job. This channel MUST be buffered.
|
||||
Resp chan AuxSigJobResp
|
||||
|
||||
// Cancel is a channel that is closed by the caller if they wish to
|
||||
// abandon all pending sign jobs part of a single batch. This should
|
||||
// never be closed by the validator.
|
||||
Cancel <-chan struct{}
|
||||
}
|
||||
|
||||
// NewAuxSigJob creates a new AuxSigJob.
|
||||
func NewAuxSigJob(sigJob SignJob, keyRing CommitmentKeyRing, incoming bool,
|
||||
htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob],
|
||||
htlcLeaf input.AuxTapLeaf, cancelChan <-chan struct{}) AuxSigJob {
|
||||
|
||||
return AuxSigJob{
|
||||
SignDesc: sigJob.SignDesc,
|
||||
BaseAuxJob: BaseAuxJob{
|
||||
OutputIndex: sigJob.OutputIndex,
|
||||
KeyRing: keyRing,
|
||||
HTLC: htlc,
|
||||
Incoming: incoming,
|
||||
CommitBlob: commitBlob,
|
||||
HtlcLeaf: htlcLeaf,
|
||||
},
|
||||
Resp: make(chan AuxSigJobResp, 1),
|
||||
Cancel: cancelChan,
|
||||
}
|
||||
}
|
||||
|
||||
// AuxSigJobResp is a struct that contains the result of a sign job.
|
||||
type AuxSigJobResp struct {
|
||||
// SigBlob is the signature blob that was generated for the HTLC. This
|
||||
// is an opaque TLV field that may contain the signature and other data.
|
||||
SigBlob fn.Option[tlv.Blob]
|
||||
|
||||
// HtlcIndex is the index of the HTLC that was signed.
|
||||
HtlcIndex uint64
|
||||
|
||||
// Err is the error that occurred when executing the specified
|
||||
// signature job. In the case that no error occurred, this value will
|
||||
// be nil.
|
||||
Err error
|
||||
}
|
||||
|
||||
// AuxVerifyJob is a struct that contains all the information needed to verify
|
||||
// an HTLC for custom channels.
|
||||
type AuxVerifyJob struct {
|
||||
// SigBlob is the signature blob that was generated for the HTLC. This
|
||||
// is an opaque TLV field that may contain the signature and other data.
|
||||
SigBlob fn.Option[tlv.Blob]
|
||||
|
||||
BaseAuxJob
|
||||
}
|
||||
|
||||
// NewAuxVerifyJob creates a new AuxVerifyJob.
|
||||
func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing,
|
||||
incoming bool, htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob],
|
||||
htlcLeaf input.AuxTapLeaf) AuxVerifyJob {
|
||||
|
||||
return AuxVerifyJob{
|
||||
SigBlob: sig,
|
||||
BaseAuxJob: BaseAuxJob{
|
||||
KeyRing: keyRing,
|
||||
HTLC: htlc,
|
||||
Incoming: incoming,
|
||||
CommitBlob: commitBlob,
|
||||
HtlcLeaf: htlcLeaf,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AuxSigner is an interface that is used to sign and verify HTLCs for custom
|
||||
// channels. It is similar to the existing SigPool, but uses opaque blobs to
|
||||
// shuffle around signature information and other metadata.
|
||||
type AuxSigner interface {
|
||||
// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and
|
||||
// processes them asynchronously.
|
||||
SubmitSecondLevelSigBatch(chanState AuxChanState, commitTx *wire.MsgTx,
|
||||
sigJob []AuxSigJob) error
|
||||
|
||||
// PackSigs takes a series of aux signatures and packs them into a
|
||||
// single blob that can be sent alongside the CommitSig messages.
|
||||
PackSigs([]fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]]
|
||||
|
||||
// UnpackSigs takes a packed blob of signatures and returns the
|
||||
// original signatures for each HTLC, keyed by HTLC index.
|
||||
UnpackSigs(fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]]
|
||||
|
||||
// VerifySecondLevelSigs attempts to synchronously verify a batch of aux
|
||||
// sig jobs.
|
||||
VerifySecondLevelSigs(chanState AuxChanState, commitTx *wire.MsgTx,
|
||||
verifyJob []AuxVerifyJob) error
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@ -98,6 +99,26 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
|
||||
)
|
||||
}
|
||||
|
||||
// TaprootInternalKey may return the internal key for a MuSig2 funding output,
|
||||
// but only if this is actually a MuSig2 channel.
|
||||
func (s *ShimIntent) TaprootInternalKey() fn.Option[*btcec.PublicKey] {
|
||||
if !s.musig2 {
|
||||
return fn.None[*btcec.PublicKey]()
|
||||
}
|
||||
|
||||
// Similar to the existing p2wsh script, we'll always ensure the keys
|
||||
// are sorted before use. Since we're only interested in the internal
|
||||
// key, we don't need to take into account any tapscript root.
|
||||
//
|
||||
// We ignore the error here as this is only called after FundingOutput
|
||||
// is called.
|
||||
combinedKey, _, _, _ := musig2.AggregateKeys(
|
||||
[]*btcec.PublicKey{s.localKey.PubKey, s.remoteKey}, true,
|
||||
)
|
||||
|
||||
return fn.Some(combinedKey.PreTweakedKey)
|
||||
}
|
||||
|
||||
// Cancel allows the caller to cancel a funding Intent at any time. This will
|
||||
// return any resources such as coins back to the eligible pool to be used in
|
||||
// order channel fundings.
|
||||
|
@ -6,11 +6,14 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
)
|
||||
@ -162,6 +165,13 @@ func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor,
|
||||
i.State = PsbtOutputKnown
|
||||
}
|
||||
|
||||
// BindTapscriptRoot takes an optional tapscript root and binds it to the
|
||||
// underlying funding intent. This only applies to musig2 channels, and will be
|
||||
// used to make the musig2 funding output.
|
||||
func (i *PsbtIntent) BindTapscriptRoot(root fn.Option[chainhash.Hash]) {
|
||||
i.tapscriptRoot = root
|
||||
}
|
||||
|
||||
// FundingParams returns the parameters that are necessary to start funding the
|
||||
// channel output this intent was created for. It returns the P2WSH funding
|
||||
// address, the exact funding amount and a PSBT packet that contains exactly one
|
||||
@ -208,7 +218,18 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet,
|
||||
}
|
||||
}
|
||||
packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out)
|
||||
packet.Outputs = append(packet.Outputs, psbt.POutput{})
|
||||
|
||||
var pOut psbt.POutput
|
||||
|
||||
// If this is a MuSig2 channel, we also need to communicate the internal
|
||||
// key to the caller. Otherwise, they cannot verify the construction of
|
||||
// the P2TR output script.
|
||||
pOut.TaprootInternalKey = fn.MapOptionZ(
|
||||
i.TaprootInternalKey(), schnorr.SerializePubKey,
|
||||
)
|
||||
|
||||
packet.Outputs = append(packet.Outputs, pOut)
|
||||
|
||||
return addr, out.Value, packet, nil
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
@ -760,6 +760,10 @@ type LightningChannel struct {
|
||||
// signatures, of which there may be hundreds.
|
||||
sigPool *SigPool
|
||||
|
||||
// auxSigner is a special signer used to obtain opaque signatures for
|
||||
// custom channel variants.
|
||||
auxSigner fn.Option[AuxSigner]
|
||||
|
||||
// Capacity is the total capacity of this channel.
|
||||
Capacity btcutil.Amount
|
||||
|
||||
@ -821,6 +825,7 @@ type channelOpts struct {
|
||||
remoteNonce *musig2.Nonces
|
||||
|
||||
leafStore fn.Option[AuxLeafStore]
|
||||
auxSigner fn.Option[AuxSigner]
|
||||
|
||||
skipNonceInit bool
|
||||
}
|
||||
@ -859,6 +864,13 @@ func WithLeafStore(store AuxLeafStore) ChannelOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuxSigner is used to specify a custom aux signer for the channel.
|
||||
func WithAuxSigner(signer AuxSigner) ChannelOpt {
|
||||
return func(o *channelOpts) {
|
||||
o.auxSigner = fn.Some[AuxSigner](signer)
|
||||
}
|
||||
}
|
||||
|
||||
// defaultChannelOpts returns the set of default options for a new channel.
|
||||
func defaultChannelOpts() *channelOpts {
|
||||
return &channelOpts{}
|
||||
@ -911,6 +923,7 @@ func NewLightningChannel(signer input.Signer,
|
||||
lc := &LightningChannel{
|
||||
Signer: signer,
|
||||
leafStore: opts.leafStore,
|
||||
auxSigner: opts.auxSigner,
|
||||
sigPool: sigPool,
|
||||
currentHeight: localCommit.CommitHeight,
|
||||
commitChains: commitChains,
|
||||
@ -2545,6 +2558,18 @@ type HtlcView struct {
|
||||
FeePerKw chainfee.SatPerKWeight
|
||||
}
|
||||
|
||||
// AuxOurUpdates returns the outgoing HTLCs as a read-only copy of
|
||||
// AuxHtlcDescriptors.
|
||||
func (v *HtlcView) AuxOurUpdates() []AuxHtlcDescriptor {
|
||||
return fn.Map(newAuxHtlcDescriptor, v.OurUpdates)
|
||||
}
|
||||
|
||||
// AuxTheirUpdates returns the incoming HTLCs as a read-only copy of
|
||||
// AuxHtlcDescriptors.
|
||||
func (v *HtlcView) AuxTheirUpdates() []AuxHtlcDescriptor {
|
||||
return fn.Map(newAuxHtlcDescriptor, v.TheirUpdates)
|
||||
}
|
||||
|
||||
// fetchHTLCView returns all the candidate HTLC updates which should be
|
||||
// considered for inclusion within a commitment based on the passed HTLC log
|
||||
// indexes.
|
||||
@ -3064,7 +3089,8 @@ func processFeeUpdate(feeUpdate *paymentDescriptor, nextHeight uint64,
|
||||
func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
chanState *channeldb.OpenChannel, leaseExpiry uint32,
|
||||
remoteCommitView *commitment,
|
||||
leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) {
|
||||
leafStore fn.Option[AuxLeafStore]) ([]SignJob, []AuxSigJob,
|
||||
chan struct{}, error) {
|
||||
|
||||
var (
|
||||
isRemoteInitiator = !chanState.IsInitiator
|
||||
@ -3084,6 +3110,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
numSigs := len(remoteCommitView.incomingHTLCs) +
|
||||
len(remoteCommitView.outgoingHTLCs)
|
||||
sigBatch := make([]SignJob, 0, numSigs)
|
||||
auxSigBatch := make([]AuxSigJob, 0, numSigs)
|
||||
|
||||
var err error
|
||||
cancelChan := make(chan struct{})
|
||||
@ -3098,8 +3125,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w",
|
||||
err)
|
||||
return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
// For each outgoing and incoming HTLC, if the HTLC isn't considered a
|
||||
@ -3148,12 +3175,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
auxLeaf,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// TODO(roasbeef): hook up signer interface here (later commit
|
||||
// in this PR).
|
||||
|
||||
// Construct a full hash cache as we may be signing a segwit v1
|
||||
// sighash.
|
||||
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
|
||||
@ -3185,6 +3209,11 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
}
|
||||
|
||||
sigBatch = append(sigBatch, sigJob)
|
||||
|
||||
auxSigBatch = append(auxSigBatch, NewAuxSigJob(
|
||||
sigJob, *keyRing, true, newAuxHtlcDescriptor(&htlc),
|
||||
remoteCommitView.customBlob, auxLeaf, cancelChan,
|
||||
))
|
||||
}
|
||||
for _, htlc := range remoteCommitView.outgoingHTLCs {
|
||||
if HtlcIsDust(
|
||||
@ -3228,7 +3257,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
auxLeaf,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Construct a full hash cache as we may be signing a segwit v1
|
||||
@ -3257,13 +3286,19 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
// If this is a taproot channel, then we'll need to set the
|
||||
// method type to ensure we generate a valid signature.
|
||||
if chanType.IsTaproot() {
|
||||
sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll
|
||||
//nolint:lll
|
||||
sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod
|
||||
}
|
||||
|
||||
sigBatch = append(sigBatch, sigJob)
|
||||
|
||||
auxSigBatch = append(auxSigBatch, NewAuxSigJob(
|
||||
sigJob, *keyRing, false, newAuxHtlcDescriptor(&htlc),
|
||||
remoteCommitView.customBlob, auxLeaf, cancelChan,
|
||||
))
|
||||
}
|
||||
|
||||
return sigBatch, cancelChan, nil
|
||||
return sigBatch, auxSigBatch, cancelChan, nil
|
||||
}
|
||||
|
||||
// createCommitDiff will create a commit diff given a new pending commitment
|
||||
@ -3272,7 +3307,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
// new commitment to the remote party. The commit diff returned contains all
|
||||
// information necessary for retransmission.
|
||||
func (lc *LightningChannel) createCommitDiff(newCommit *commitment,
|
||||
commitSig lnwire.Sig, htlcSigs []lnwire.Sig) *channeldb.CommitDiff {
|
||||
commitSig lnwire.Sig, htlcSigs []lnwire.Sig,
|
||||
auxSigs []fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) {
|
||||
|
||||
var (
|
||||
logUpdates []channeldb.LogUpdate
|
||||
@ -3341,21 +3377,71 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment,
|
||||
// disk.
|
||||
diskCommit := newCommit.toDiskCommit(lntypes.Remote)
|
||||
|
||||
return &channeldb.CommitDiff{
|
||||
Commitment: *diskCommit,
|
||||
CommitSig: &lnwire.CommitSig{
|
||||
ChanID: lnwire.NewChanIDFromOutPoint(
|
||||
lc.channelState.FundingOutpoint,
|
||||
),
|
||||
CommitSig: commitSig,
|
||||
HtlcSigs: htlcSigs,
|
||||
// We prepare the commit sig message to be sent to the remote party.
|
||||
commitSigMsg := &lnwire.CommitSig{
|
||||
ChanID: lnwire.NewChanIDFromOutPoint(
|
||||
lc.channelState.FundingOutpoint,
|
||||
),
|
||||
CommitSig: commitSig,
|
||||
HtlcSigs: htlcSigs,
|
||||
}
|
||||
|
||||
// Encode and check the size of the custom records now.
|
||||
auxCustomRecords, err := fn.MapOptionZ(
|
||||
lc.auxSigner,
|
||||
func(s AuxSigner) fn.Result[lnwire.CustomRecords] {
|
||||
blobOption, err := s.PackSigs(auxSigs).Unpack()
|
||||
if err != nil {
|
||||
return fn.Err[lnwire.CustomRecords](err)
|
||||
}
|
||||
|
||||
// We now serialize the commit sig message without the
|
||||
// custom records to make sure we have space for them.
|
||||
var buf bytes.Buffer
|
||||
err = commitSigMsg.Encode(&buf, 0)
|
||||
if err != nil {
|
||||
return fn.Err[lnwire.CustomRecords](err)
|
||||
}
|
||||
|
||||
// The number of available bytes is the max message size
|
||||
// minus the size of the message without the custom
|
||||
// records. We also subtract 8 bytes for encoding
|
||||
// overhead of the custom records (just some safety
|
||||
// padding).
|
||||
available := lnwire.MaxMsgBody - buf.Len() - 8
|
||||
|
||||
blob := blobOption.UnwrapOr(nil)
|
||||
if len(blob) > available {
|
||||
err = fmt.Errorf("aux sigs size %d exceeds "+
|
||||
"max allowed size of %d", len(blob),
|
||||
available)
|
||||
|
||||
return fn.Err[lnwire.CustomRecords](err)
|
||||
}
|
||||
|
||||
records, err := lnwire.ParseCustomRecords(blob)
|
||||
if err != nil {
|
||||
return fn.Err[lnwire.CustomRecords](err)
|
||||
}
|
||||
|
||||
return fn.Ok(records)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error packing aux sigs: %w", err)
|
||||
}
|
||||
|
||||
commitSigMsg.CustomRecords = auxCustomRecords
|
||||
|
||||
return &channeldb.CommitDiff{
|
||||
Commitment: *diskCommit,
|
||||
CommitSig: commitSigMsg,
|
||||
LogUpdates: logUpdates,
|
||||
OpenedCircuitKeys: openCircuitKeys,
|
||||
ClosedCircuitKeys: closedCircuitKeys,
|
||||
AddAcks: ackAddRefs,
|
||||
SettleFailAcks: settleFailRefs,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getUnsignedAckedUpdates returns all remote log updates that we haven't
|
||||
@ -3748,6 +3834,10 @@ type CommitSigs struct {
|
||||
// PartialSig is the musig2 partial signature for taproot commitment
|
||||
// transactions.
|
||||
PartialSig lnwire.OptPartialSigWithNonceTLV
|
||||
|
||||
// AuxSigBlob is the blob containing all the auxiliary signatures for
|
||||
// this new commitment state.
|
||||
AuxSigBlob tlv.Blob
|
||||
}
|
||||
|
||||
// NewCommitState wraps the various signatures needed to properly
|
||||
@ -3772,6 +3862,8 @@ type NewCommitState struct {
|
||||
// any). The HTLC signatures are sorted according to the BIP 69 order of the
|
||||
// HTLC's on the commitment transaction. Finally, the new set of pending HTLCs
|
||||
// for the remote party's commitment are also returned.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
lc.Lock()
|
||||
defer lc.Unlock()
|
||||
@ -3864,15 +3956,37 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
if lc.channelState.ChanType.HasLeaseExpiration() {
|
||||
leaseExpiry = lc.channelState.ThawHeight
|
||||
}
|
||||
sigBatch, cancelChan, err := genRemoteHtlcSigJobs(
|
||||
sigBatch, auxSigBatch, cancelChan, err := genRemoteHtlcSigJobs(
|
||||
keyRing, lc.channelState, leaseExpiry, newCommitView,
|
||||
lc.leafStore,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We'll need to send over the signatures to the remote party in the
|
||||
// order as they appear on the commitment transaction after BIP 69
|
||||
// sorting.
|
||||
slices.SortFunc(sigBatch, func(i, j SignJob) int {
|
||||
return int(i.OutputIndex - j.OutputIndex)
|
||||
})
|
||||
slices.SortFunc(auxSigBatch, func(i, j AuxSigJob) int {
|
||||
return int(i.OutputIndex - j.OutputIndex)
|
||||
})
|
||||
|
||||
lc.sigPool.SubmitSignBatch(sigBatch)
|
||||
|
||||
err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error {
|
||||
return a.SubmitSecondLevelSigBatch(
|
||||
NewAuxChanState(lc.channelState), newCommitView.txn,
|
||||
auxSigBatch,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error submitting second level sig "+
|
||||
"batch: %w", err)
|
||||
}
|
||||
|
||||
// While the jobs are being carried out, we'll Sign their version of
|
||||
// the new commitment transaction while we're waiting for the rest of
|
||||
// the HTLC signatures to be processed.
|
||||
@ -3910,17 +4024,12 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// We'll need to send over the signatures to the remote party in the
|
||||
// order as they appear on the commitment transaction after BIP 69
|
||||
// sorting.
|
||||
sort.Slice(sigBatch, func(i, j int) bool {
|
||||
return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex
|
||||
})
|
||||
|
||||
// With the jobs sorted, we'll now iterate through all the responses to
|
||||
// gather each of the signatures in order.
|
||||
// Iterate through all the responses to gather each of the signatures
|
||||
// in the order they were submitted.
|
||||
htlcSigs = make([]lnwire.Sig, 0, len(sigBatch))
|
||||
for _, htlcSigJob := range sigBatch {
|
||||
auxSigs := make([]fn.Option[tlv.Blob], 0, len(auxSigBatch))
|
||||
for i := range sigBatch {
|
||||
htlcSigJob := sigBatch[i]
|
||||
jobResp := <-htlcSigJob.Resp
|
||||
|
||||
// If an error occurred, then we'll cancel any other active
|
||||
@ -3931,12 +4040,34 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
}
|
||||
|
||||
htlcSigs = append(htlcSigs, jobResp.Sig)
|
||||
|
||||
if lc.auxSigner.IsNone() {
|
||||
continue
|
||||
}
|
||||
|
||||
auxHtlcSigJob := auxSigBatch[i]
|
||||
auxJobResp := <-auxHtlcSigJob.Resp
|
||||
|
||||
// If an error occurred, then we'll cancel any other active
|
||||
// jobs.
|
||||
if auxJobResp.Err != nil {
|
||||
close(cancelChan)
|
||||
return nil, auxJobResp.Err
|
||||
}
|
||||
|
||||
auxSigs = append(auxSigs, auxJobResp.SigBlob)
|
||||
}
|
||||
|
||||
// As we're about to proposer a new commitment state for the remote
|
||||
// party, we'll write this pending state to disk before we exit, so we
|
||||
// can retransmit it if necessary.
|
||||
commitDiff := lc.createCommitDiff(newCommitView, sig, htlcSigs)
|
||||
commitDiff, err := lc.createCommitDiff(
|
||||
newCommitView, sig, htlcSigs, auxSigs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = lc.channelState.AppendRemoteCommitChain(commitDiff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -3950,11 +4081,18 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
// latest commitment update.
|
||||
lc.commitChains.Remote.addCommitment(newCommitView)
|
||||
|
||||
auxSigBlob, err := commitDiff.CommitSig.CustomRecords.Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize aux sig blob: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
return &NewCommitState{
|
||||
CommitSigs: &CommitSigs{
|
||||
CommitSig: sig,
|
||||
HtlcSigs: htlcSigs,
|
||||
PartialSig: lnwire.MaybePartialSigWithNonce(partialSig),
|
||||
AuxSigBlob: auxSigBlob,
|
||||
},
|
||||
PendingHTLCs: commitDiff.Commitment.Htlcs,
|
||||
}, nil
|
||||
@ -3966,8 +4104,8 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
|
||||
// each time. After we receive the channel reestablish message, we learn the
|
||||
// nonce we need to use for the remote party. As a result, we need to generate
|
||||
// the partial signature again with the new nonce.
|
||||
func (lc *LightningChannel) resignMusigCommit(commitTx *wire.MsgTx,
|
||||
) (lnwire.OptPartialSigWithNonceTLV, error) {
|
||||
func (lc *LightningChannel) resignMusigCommit(
|
||||
commitTx *wire.MsgTx) (lnwire.OptPartialSigWithNonceTLV, error) {
|
||||
|
||||
remoteSession := lc.musigSessions.RemoteSession
|
||||
musig, err := remoteSession.SignCommit(commitTx)
|
||||
@ -4172,13 +4310,23 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
|
||||
// If we signed this state, then we'll accumulate
|
||||
// another update to send over.
|
||||
case err == nil:
|
||||
customRecords, err := lnwire.ParseCustomRecords(
|
||||
newCommit.AuxSigBlob,
|
||||
)
|
||||
if err != nil {
|
||||
sErr := fmt.Errorf("error parsing aux "+
|
||||
"sigs: %w", err)
|
||||
return nil, nil, nil, sErr
|
||||
}
|
||||
|
||||
commitSig := &lnwire.CommitSig{
|
||||
ChanID: lnwire.NewChanIDFromOutPoint(
|
||||
lc.channelState.FundingOutpoint,
|
||||
),
|
||||
CommitSig: newCommit.CommitSig,
|
||||
HtlcSigs: newCommit.HtlcSigs,
|
||||
PartialSig: newCommit.PartialSig,
|
||||
CommitSig: newCommit.CommitSig,
|
||||
HtlcSigs: newCommit.HtlcSigs,
|
||||
PartialSig: newCommit.PartialSig,
|
||||
CustomRecords: customRecords,
|
||||
}
|
||||
|
||||
updates = append(updates, commitSig)
|
||||
@ -4396,6 +4544,7 @@ func (lc *LightningChannel) computeView(view *HtlcView,
|
||||
// need this to determine which HTLCs are dust, and also the final fee
|
||||
// rate.
|
||||
view.FeePerKw = commitChain.tip().feePerKw
|
||||
view.NextHeight = nextHeight
|
||||
|
||||
// We evaluate the view at this stage, meaning settled and failed HTLCs
|
||||
// will remove their corresponding added HTLCs. The resulting filtered
|
||||
@ -4471,7 +4620,8 @@ func (lc *LightningChannel) computeView(view *HtlcView,
|
||||
func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
localCommitmentView *commitment, keyRing *CommitmentKeyRing,
|
||||
htlcSigs []lnwire.Sig, leaseExpiry uint32,
|
||||
leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) {
|
||||
leafStore fn.Option[AuxLeafStore], auxSigner fn.Option[AuxSigner],
|
||||
sigBlob fn.Option[tlv.Blob]) ([]VerifyJob, []AuxVerifyJob, error) {
|
||||
|
||||
var (
|
||||
isLocalInitiator = chanState.IsInitiator
|
||||
@ -4490,6 +4640,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
numHtlcs := len(localCommitmentView.incomingHTLCs) +
|
||||
len(localCommitmentView.outgoingHTLCs)
|
||||
verifyJobs := make([]VerifyJob, 0, numHtlcs)
|
||||
auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs)
|
||||
|
||||
diskCommit := localCommitmentView.toDiskCommit(lntypes.Local)
|
||||
auxResult, err := fn.MapOptionZ(
|
||||
@ -4501,7 +4652,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch aux leaves: %w", err)
|
||||
return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// If we have a sig blob, then we'll attempt to map that to individual
|
||||
// blobs for each HTLC we might need a signature for.
|
||||
auxHtlcSigs, err := fn.MapOptionZ(
|
||||
auxSigner, func(a AuxSigner) fn.Result[[]fn.Option[tlv.Blob]] {
|
||||
return a.UnpackSigs(sigBlob)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error unpacking aux sigs: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// We'll iterate through each output in the commitment transaction,
|
||||
@ -4514,6 +4678,9 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
htlcIndex uint64
|
||||
sigHash func() ([]byte, error)
|
||||
sig input.Signature
|
||||
htlc *paymentDescriptor
|
||||
incoming bool
|
||||
auxLeaf input.AuxTapLeaf
|
||||
err error
|
||||
)
|
||||
|
||||
@ -4523,10 +4690,12 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
// If this output index is found within the incoming HTLC
|
||||
// index, then this means that we need to generate an HTLC
|
||||
// success transaction in order to validate the signature.
|
||||
//nolint:lll
|
||||
case localCommitmentView.incomingHTLCIndex[outputIndex] != nil:
|
||||
htlc := localCommitmentView.incomingHTLCIndex[outputIndex]
|
||||
htlc = localCommitmentView.incomingHTLCIndex[outputIndex]
|
||||
|
||||
htlcIndex = htlc.HtlcIndex
|
||||
incoming = true
|
||||
|
||||
sigHash = func() ([]byte, error) {
|
||||
op := wire.OutPoint{
|
||||
@ -4592,7 +4761,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
|
||||
// Make sure there are more signatures left.
|
||||
if i >= len(htlcSigs) {
|
||||
return nil, fmt.Errorf("not enough HTLC " +
|
||||
return nil, nil, fmt.Errorf("not enough HTLC " +
|
||||
"signatures")
|
||||
}
|
||||
|
||||
@ -4608,15 +4777,16 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
// is valid.
|
||||
sig, err = htlcSigs[i].ToSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
htlc.sig = sig
|
||||
|
||||
// Otherwise, if this is an outgoing HTLC, then we'll need to
|
||||
// generate a timeout transaction so we can verify the
|
||||
// signature presented.
|
||||
//nolint:lll
|
||||
case localCommitmentView.outgoingHTLCIndex[outputIndex] != nil:
|
||||
htlc := localCommitmentView.outgoingHTLCIndex[outputIndex]
|
||||
htlc = localCommitmentView.outgoingHTLCIndex[outputIndex]
|
||||
|
||||
htlcIndex = htlc.HtlcIndex
|
||||
|
||||
@ -4687,7 +4857,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
|
||||
// Make sure there are more signatures left.
|
||||
if i >= len(htlcSigs) {
|
||||
return nil, fmt.Errorf("not enough HTLC " +
|
||||
return nil, nil, fmt.Errorf("not enough HTLC " +
|
||||
"signatures")
|
||||
}
|
||||
|
||||
@ -4703,7 +4873,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
// is valid.
|
||||
sig, err = htlcSigs[i].ToSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
htlc.sig = sig
|
||||
@ -4719,17 +4889,40 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
|
||||
SigHash: sigHash,
|
||||
})
|
||||
|
||||
if len(auxHtlcSigs) > i {
|
||||
auxSig := auxHtlcSigs[i]
|
||||
auxVerifyJob := NewAuxVerifyJob(
|
||||
auxSig, *keyRing, incoming,
|
||||
newAuxHtlcDescriptor(htlc),
|
||||
localCommitmentView.customBlob, auxLeaf,
|
||||
)
|
||||
|
||||
if htlc.CustomRecords == nil {
|
||||
htlc.CustomRecords = make(lnwire.CustomRecords)
|
||||
}
|
||||
|
||||
// As this HTLC has a custom signature associated with
|
||||
// it, store it in the custom records map so we can
|
||||
// write to disk later.
|
||||
sigType := htlcCustomSigType.TypeVal()
|
||||
htlc.CustomRecords[uint64(sigType)] = auxSig.UnwrapOr(
|
||||
nil,
|
||||
)
|
||||
|
||||
auxVerifyJobs = append(auxVerifyJobs, auxVerifyJob)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
// If we received a number of HTLC signatures that doesn't match our
|
||||
// commitment, we'll return an error now.
|
||||
if len(htlcSigs) != i {
|
||||
return nil, fmt.Errorf("number of htlc sig mismatch. "+
|
||||
return nil, nil, fmt.Errorf("number of htlc sig mismatch. "+
|
||||
"Expected %v sigs, got %v", i, len(htlcSigs))
|
||||
}
|
||||
|
||||
return verifyJobs, nil
|
||||
return verifyJobs, auxVerifyJobs, nil
|
||||
}
|
||||
|
||||
// InvalidCommitSigError is a struct that implements the error interface to
|
||||
@ -4888,6 +5081,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
|
||||
localCommitmentView.ourBalance, localCommitmentView.theirBalance,
|
||||
lnutils.SpewLogClosure(localCommitmentView.txn))
|
||||
|
||||
var auxSigBlob fn.Option[tlv.Blob]
|
||||
if commitSigs.AuxSigBlob != nil {
|
||||
auxSigBlob = fn.Some(commitSigs.AuxSigBlob)
|
||||
}
|
||||
|
||||
// As an optimization, we'll generate a series of jobs for the worker
|
||||
// pool to verify each of the HTLC signatures presented. Once
|
||||
// generated, we'll submit these jobs to the worker pool.
|
||||
@ -4895,9 +5093,10 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
|
||||
if lc.channelState.ChanType.HasLeaseExpiration() {
|
||||
leaseExpiry = lc.channelState.ThawHeight
|
||||
}
|
||||
verifyJobs, err := genHtlcSigValidationJobs(
|
||||
verifyJobs, auxVerifyJobs, err := genHtlcSigValidationJobs(
|
||||
lc.channelState, localCommitmentView, keyRing,
|
||||
commitSigs.HtlcSigs, leaseExpiry, lc.leafStore,
|
||||
commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, lc.auxSigner,
|
||||
auxSigBlob,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -5050,6 +5249,18 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know all the normal sigs are valid, we'll also verify
|
||||
// the aux jobs, if any exist.
|
||||
err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error {
|
||||
return a.VerifySecondLevelSigs(
|
||||
NewAuxChanState(lc.channelState), localCommitTx,
|
||||
auxVerifyJobs,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to validate aux sigs: %w", err)
|
||||
}
|
||||
|
||||
// The signature checks out, so we can now add the new commitment to
|
||||
// our local commitment chain. For regular channels, we can just
|
||||
// serialize the ECDSA sig. For taproot channels, we'll serialize the
|
||||
@ -5782,8 +5993,9 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
|
||||
defer lc.Unlock()
|
||||
|
||||
if htlc.ID != lc.updateLogs.Remote.htlcCounter {
|
||||
return 0, fmt.Errorf("ID %d on HTLC add does not match expected next "+
|
||||
"ID %d", htlc.ID, lc.updateLogs.Remote.htlcCounter)
|
||||
return 0, fmt.Errorf("ID %d on HTLC add does not match "+
|
||||
"expected next ID %d", htlc.ID,
|
||||
lc.updateLogs.Remote.htlcCounter)
|
||||
}
|
||||
|
||||
pd := &paymentDescriptor{
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -702,6 +703,68 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) {
|
||||
require.NoError(t, err, "unable to receive bob's commitment")
|
||||
}
|
||||
|
||||
// TestCommitHTLCSigCustomRecordSize asserts that custom records produced for
|
||||
// a commitment_signed message are properly limited in size.
|
||||
func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
|
||||
aliceChannel, bobChannel, err := CreateTestChannels(
|
||||
t, channeldb.SimpleTaprootFeatureBit|
|
||||
channeldb.TapscriptRootBit,
|
||||
)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
const (
|
||||
htlcAmt = lnwire.MilliSatoshi(20000000)
|
||||
numHtlcs = 2
|
||||
)
|
||||
|
||||
largeRecords := lnwire.CustomRecords{
|
||||
lnwire.MinCustomRecordsTlvType: bytes.Repeat([]byte{0}, 65_500),
|
||||
}
|
||||
largeBlob, err := largeRecords.Serialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
aliceChannel.auxSigner.WhenSome(func(a AuxSigner) {
|
||||
mockSigner, ok := a.(*MockAuxSigner)
|
||||
require.True(t, ok, "expected MockAuxSigner")
|
||||
|
||||
// Replace the default PackSigs implementation to return a
|
||||
// large custom records blob.
|
||||
mockSigner.ExpectedCalls = fn.Filter(func(c *mock.Call) bool {
|
||||
return c.Method != "PackSigs"
|
||||
}, mockSigner.ExpectedCalls)
|
||||
mockSigner.On("PackSigs", mock.Anything).
|
||||
Return(fn.Ok(fn.Some(largeBlob)))
|
||||
})
|
||||
|
||||
// Add HTLCs with identical payment hashes and amounts, but descending
|
||||
// CLTV values. We will expect the signatures to appear in the reverse
|
||||
// order that the HTLCs are added due to the commitment sorting.
|
||||
for i := 0; i < numHtlcs; i++ {
|
||||
var (
|
||||
preimage lntypes.Preimage
|
||||
hash = preimage.Hash()
|
||||
)
|
||||
|
||||
htlc := &lnwire.UpdateAddHTLC{
|
||||
ID: uint64(i),
|
||||
PaymentHash: hash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(numHtlcs - i),
|
||||
}
|
||||
|
||||
if _, err := aliceChannel.AddHTLC(htlc, nil); 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)
|
||||
}
|
||||
}
|
||||
|
||||
// We expect an error because of the large custom records blob.
|
||||
_, err = aliceChannel.SignNextCommitment()
|
||||
require.ErrorContains(t, err, "exceeds max allowed size")
|
||||
}
|
||||
|
||||
// TestCooperativeChannelClosure checks that the coop close process finishes
|
||||
// with an agreement from both parties, and that the final balances of the
|
||||
// close tx check out.
|
||||
@ -3046,6 +3109,10 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) {
|
||||
return channelNew, nil
|
||||
}
|
||||
|
||||
// testChanSyncOweCommitment tests that if Bob restarts (and then Alice) before
|
||||
// he receives Alice's CommitSig message, then Alice concludes that she needs
|
||||
// to re-send the CommitDiff. After the diff has been sent, both nodes should
|
||||
// resynchronize and be able to complete the dangling commit.
|
||||
func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
// 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,
|
||||
@ -3210,8 +3277,10 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
|
||||
len(commitSigMsg.HtlcSigs))
|
||||
}
|
||||
for i, htlcSig := range commitSigMsg.HtlcSigs {
|
||||
if !bytes.Equal(htlcSig.RawBytes(),
|
||||
aliceNewCommit.HtlcSigs[i].RawBytes()) {
|
||||
if !bytes.Equal(
|
||||
htlcSig.RawBytes(),
|
||||
aliceNewCommit.HtlcSigs[i].RawBytes(),
|
||||
) {
|
||||
|
||||
t.Fatalf("htlc sig msgs don't match: "+
|
||||
"expected %v got %v",
|
||||
@ -3389,6 +3458,100 @@ func TestChanSyncOweCommitment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type testSigBlob struct {
|
||||
BlobInt tlv.RecordT[tlv.TlvType65634, uint16]
|
||||
}
|
||||
|
||||
// TestChanSyncOweCommitmentAuxSigner tests that when one party owes a
|
||||
// signature after a channel reest, if an aux signer is present, then the
|
||||
// signature message sent includes the additional aux sigs as extra data.
|
||||
func TestChanSyncOweCommitmentAuxSigner(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 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.
|
||||
chanType := channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit |
|
||||
channeldb.TapscriptRootBit
|
||||
|
||||
aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
// We'll now manually attach an aux signer to Alice's channel.
|
||||
auxSigner := &MockAuxSigner{}
|
||||
aliceChannel.auxSigner = fn.Some[AuxSigner](auxSigner)
|
||||
|
||||
var fakeOnionBlob [lnwire.OnionPacketSize]byte
|
||||
copy(
|
||||
fakeOnionBlob[:],
|
||||
bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize),
|
||||
)
|
||||
|
||||
// To kick things off, we'll have Alice send a single HTLC to Bob.
|
||||
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
|
||||
var bobPreimage [32]byte
|
||||
copy(bobPreimage[:], bytes.Repeat([]byte{0}, 32))
|
||||
rHash := sha256.Sum256(bobPreimage[:])
|
||||
h := &lnwire.UpdateAddHTLC{
|
||||
PaymentHash: rHash,
|
||||
Amount: htlcAmt,
|
||||
Expiry: uint32(10),
|
||||
OnionBlob: fakeOnionBlob,
|
||||
}
|
||||
|
||||
_, err = aliceChannel.AddHTLC(h, nil)
|
||||
require.NoError(t, err, "unable to recv bob's htlc: %v", err)
|
||||
|
||||
// We'll set up the mock to expect calls to PackSigs and also
|
||||
// SubmitSubmitSecondLevelSigBatch.
|
||||
var sigBlobBuf bytes.Buffer
|
||||
sigBlob := testSigBlob{
|
||||
BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5),
|
||||
}
|
||||
tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record())
|
||||
require.NoError(t, err, "unable to create tlv stream")
|
||||
require.NoError(t, tlvStream.Encode(&sigBlobBuf))
|
||||
|
||||
auxSigner.On(
|
||||
"SubmitSecondLevelSigBatch", mock.Anything, mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(nil).Twice()
|
||||
auxSigner.On(
|
||||
"PackSigs", mock.Anything,
|
||||
).Return(
|
||||
fn.Ok(fn.Some(sigBlobBuf.Bytes())), nil,
|
||||
)
|
||||
|
||||
_, err = aliceChannel.SignNextCommitment()
|
||||
require.NoError(t, err, "unable to sign commitment")
|
||||
|
||||
_, err = aliceChannel.GenMusigNonces()
|
||||
require.NoError(t, err, "unable to generate musig nonces")
|
||||
|
||||
// Next we'll simulate a restart, by having Bob send over a chan sync
|
||||
// message to Alice.
|
||||
bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
|
||||
require.NoError(t, err, "unable to produce chan sync msg")
|
||||
|
||||
aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(
|
||||
bobSyncMsg,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aliceMsgsToSend, 2)
|
||||
|
||||
// The first message should be an update add HTLC.
|
||||
require.IsType(t, &lnwire.UpdateAddHTLC{}, aliceMsgsToSend[0])
|
||||
|
||||
// The second should be a commit sig message.
|
||||
sigMsg, ok := aliceMsgsToSend[1].(*lnwire.CommitSig)
|
||||
require.True(t, ok)
|
||||
require.True(t, sigMsg.PartialSig.IsSome())
|
||||
|
||||
// The signature should have the CustomRecords field set.
|
||||
require.NotEmpty(t, sigMsg.CustomRecords)
|
||||
}
|
||||
|
||||
func testChanSyncOweCommitmentPendingRemote(t *testing.T,
|
||||
chanType channeldb.ChannelType) {
|
||||
|
||||
@ -3398,7 +3561,10 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T,
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
var fakeOnionBlob [lnwire.OnionPacketSize]byte
|
||||
copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
|
||||
copy(
|
||||
fakeOnionBlob[:],
|
||||
bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize),
|
||||
)
|
||||
|
||||
// We'll start off the scenario where Bob send two htlcs to Alice in a
|
||||
// single state update.
|
||||
@ -3437,7 +3603,9 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T,
|
||||
|
||||
// Next, Alice settles the HTLCs from Bob in distinct state updates.
|
||||
for i := 0; i < numHtlcs; i++ {
|
||||
err = aliceChannel.SettleHTLC(preimages[i], uint64(i), nil, nil, nil)
|
||||
err = aliceChannel.SettleHTLC(
|
||||
preimages[i], uint64(i), nil, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to settle htlc: %v", err)
|
||||
}
|
||||
@ -3727,7 +3895,7 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) {
|
||||
}
|
||||
|
||||
// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before
|
||||
// he receiver's Alice's RevokeAndAck message, then Alice concludes that she
|
||||
// he received Alice's RevokeAndAck message, then Alice concludes that she
|
||||
// needs to re-send the RevokeAndAck. After the revocation has been sent, both
|
||||
// nodes should be able to successfully complete another state transition.
|
||||
func TestChanSyncOweRevocation(t *testing.T) {
|
||||
|
@ -67,4 +67,8 @@ type Config struct {
|
||||
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[AuxSigner]
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -441,3 +442,54 @@ func (*MockAuxLeafStore) ApplyHtlcView(
|
||||
|
||||
return fn.Ok(fn.None[tlv.Blob]())
|
||||
}
|
||||
|
||||
// MockAuxSigner is a mock implementation of the AuxSigner interface.
|
||||
type MockAuxSigner struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and
|
||||
// processes them asynchronously.
|
||||
func (a *MockAuxSigner) SubmitSecondLevelSigBatch(chanState AuxChanState,
|
||||
tx *wire.MsgTx, jobs []AuxSigJob) error {
|
||||
|
||||
args := a.Called(chanState, tx, jobs)
|
||||
|
||||
// While we return, we'll also send back an instant response for the
|
||||
// set of jobs.
|
||||
for _, sigJob := range jobs {
|
||||
sigJob.Resp <- AuxSigJobResp{}
|
||||
}
|
||||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// PackSigs takes a series of aux signatures and packs them into a
|
||||
// single blob that can be sent alongside the CommitSig messages.
|
||||
func (a *MockAuxSigner) PackSigs(
|
||||
sigs []fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]] {
|
||||
|
||||
args := a.Called(sigs)
|
||||
|
||||
return args.Get(0).(fn.Result[fn.Option[tlv.Blob]])
|
||||
}
|
||||
|
||||
// UnpackSigs takes a packed blob of signatures and returns the
|
||||
// original signatures for each HTLC, keyed by HTLC index.
|
||||
func (a *MockAuxSigner) UnpackSigs(
|
||||
sigs fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]] {
|
||||
|
||||
args := a.Called(sigs)
|
||||
|
||||
return args.Get(0).(fn.Result[[]fn.Option[tlv.Blob]])
|
||||
}
|
||||
|
||||
// VerifySecondLevelSigs attempts to synchronously verify a batch of aux
|
||||
// sig jobs.
|
||||
func (a *MockAuxSigner) VerifySecondLevelSigs(chanState AuxChanState,
|
||||
tx *wire.MsgTx, jobs []AuxVerifyJob) error {
|
||||
|
||||
args := a.Called(chanState, tx, jobs)
|
||||
|
||||
return args.Error(0)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
@ -25,7 +26,7 @@ type CommitmentType int
|
||||
const (
|
||||
// CommitmentTypeLegacy is the legacy commitment format with a tweaked
|
||||
// to_remote key.
|
||||
CommitmentTypeLegacy = iota
|
||||
CommitmentTypeLegacy CommitmentType = iota
|
||||
|
||||
// CommitmentTypeTweakless is a newer commitment format where the
|
||||
// to_remote key is static.
|
||||
@ -100,6 +101,28 @@ func (c CommitmentType) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// ReservationState is a type that represents the current state of a channel
|
||||
// reservation within the funding workflow.
|
||||
type ReservationState int
|
||||
|
||||
const (
|
||||
// WaitingToSend is the state either the funder/fundee is in after
|
||||
// creating a reservation, but hasn't sent a message yet.
|
||||
WaitingToSend ReservationState = iota
|
||||
|
||||
// SentOpenChannel is the state the funder is in after sending the
|
||||
// OpenChannel message.
|
||||
SentOpenChannel
|
||||
|
||||
// SentAcceptChannel is the state the fundee is in after sending the
|
||||
// AcceptChannel message.
|
||||
SentAcceptChannel
|
||||
|
||||
// SentFundingCreated is the state the funder is in after sending the
|
||||
// FundingCreated message.
|
||||
SentFundingCreated
|
||||
)
|
||||
|
||||
// ChannelContribution is the primary constituent of the funding workflow
|
||||
// within lnwallet. Each side first exchanges their respective contributions
|
||||
// along with channel specific parameters like the min fee/KB. Once
|
||||
@ -223,6 +246,8 @@ type ChannelReservation struct {
|
||||
nextRevocationKeyLoc keychain.KeyLocator
|
||||
|
||||
musigSessions *MusigPairSession
|
||||
|
||||
state ReservationState
|
||||
}
|
||||
|
||||
// NewChannelReservation creates a new channel reservation. This function is
|
||||
@ -459,6 +484,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
||||
reservationID: id,
|
||||
wallet: wallet,
|
||||
chanFunder: req.ChanFunder,
|
||||
state: WaitingToSend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -470,6 +496,22 @@ func (r *ChannelReservation) AddAlias(scid lnwire.ShortChannelID) {
|
||||
r.partialState.ShortChannelID = scid
|
||||
}
|
||||
|
||||
// SetState sets the ReservationState.
|
||||
func (r *ChannelReservation) SetState(state ReservationState) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
r.state = state
|
||||
}
|
||||
|
||||
// State returns the current ReservationState.
|
||||
func (r *ChannelReservation) State() ReservationState {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
return r.state
|
||||
}
|
||||
|
||||
// SetNumConfsRequired sets the number of confirmations that are required for
|
||||
// the ultimate funding transaction before the channel can be considered open.
|
||||
// This is distinct from the main reservation workflow as it allows
|
||||
@ -610,12 +652,15 @@ func (r *ChannelReservation) IsCannedShim() bool {
|
||||
}
|
||||
|
||||
// ProcessPsbt continues a previously paused funding flow that involves PSBT to
|
||||
// construct the funding transaction. This method can be called once the PSBT is
|
||||
// finalized and the signed transaction is available.
|
||||
func (r *ChannelReservation) ProcessPsbt() error {
|
||||
// construct the funding transaction. This method can be called once the PSBT
|
||||
// is finalized and the signed transaction is available.
|
||||
func (r *ChannelReservation) ProcessPsbt(
|
||||
auxFundingDesc fn.Option[AuxFundingDesc]) error {
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
r.wallet.msgChan <- &continueContributionMsg{
|
||||
auxFundingDesc: auxFundingDesc,
|
||||
pendingFundingID: r.reservationID,
|
||||
err: errChan,
|
||||
}
|
||||
@ -717,8 +762,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*input.Sc
|
||||
// available via the .OurSignatures() method. As this method should only be
|
||||
// called as a response to a single funder channel, only a commitment signature
|
||||
// will be populated.
|
||||
func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoint,
|
||||
commitSig input.Signature) (*channeldb.OpenChannel, error) {
|
||||
func (r *ChannelReservation) CompleteReservationSingle(
|
||||
fundingPoint *wire.OutPoint, commitSig input.Signature,
|
||||
auxFundingDesc fn.Option[AuxFundingDesc]) (*channeldb.OpenChannel,
|
||||
error) {
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
completeChan := make(chan *channeldb.OpenChannel, 1)
|
||||
@ -728,6 +775,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi
|
||||
fundingOutpoint: fundingPoint,
|
||||
theirCommitmentSig: commitSig,
|
||||
completeChan: completeChan,
|
||||
auxFundingDesc: auxFundingDesc,
|
||||
err: errChan,
|
||||
}
|
||||
|
||||
@ -813,6 +861,42 @@ func (r *ChannelReservation) Cancel() error {
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
// ChanState the current open channel state.
|
||||
func (r *ChannelReservation) ChanState() *channeldb.OpenChannel {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
return r.partialState
|
||||
}
|
||||
|
||||
// CommitmentKeyRings returns the local+remote key ring used for the very first
|
||||
// commitment transaction both parties.
|
||||
//
|
||||
//nolint:lll
|
||||
func (r *ChannelReservation) CommitmentKeyRings() lntypes.Dual[CommitmentKeyRing] {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
chanType := r.partialState.ChanType
|
||||
ourChanCfg := r.ourContribution.ChannelConfig
|
||||
theirChanCfg := r.theirContribution.ChannelConfig
|
||||
|
||||
localKeys := DeriveCommitmentKeys(
|
||||
r.ourContribution.FirstCommitmentPoint, lntypes.Local, chanType,
|
||||
ourChanCfg, theirChanCfg,
|
||||
)
|
||||
|
||||
remoteKeys := DeriveCommitmentKeys(
|
||||
r.theirContribution.FirstCommitmentPoint, lntypes.Remote,
|
||||
chanType, ourChanCfg, theirChanCfg,
|
||||
)
|
||||
|
||||
return lntypes.Dual[CommitmentKeyRing]{
|
||||
Local: *localKeys,
|
||||
Remote: *remoteKeys,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyConstraints is a helper function that can be used to check the sanity
|
||||
// of various channel constraints.
|
||||
func VerifyConstraints(bounds *channeldb.ChannelStateBounds,
|
||||
|
@ -45,17 +45,17 @@ type VerifyJob struct {
|
||||
// party's update log.
|
||||
HtlcIndex uint64
|
||||
|
||||
// Cancel is a channel that should be closed if the caller wishes to
|
||||
// Cancel is a channel that is closed by the caller if they wish to
|
||||
// cancel all pending verification jobs part of a single batch. This
|
||||
// channel is to be closed in the case that a single signature in a
|
||||
// batch has been returned as invalid, as there is no need to verify
|
||||
// the remainder of the signatures.
|
||||
Cancel chan struct{}
|
||||
// channel is closed in the case that a single signature in a batch has
|
||||
// been returned as invalid, as there is no need to verify the remainder
|
||||
// of the signatures.
|
||||
Cancel <-chan struct{}
|
||||
|
||||
// ErrResp is the channel that the result of the signature verification
|
||||
// is to be sent over. In the see that the signature is valid, a nil
|
||||
// error will be passed. Otherwise, a concrete error detailing the
|
||||
// issue will be passed.
|
||||
// issue will be passed. This channel MUST be buffered.
|
||||
ErrResp chan *HtlcIndexErr
|
||||
}
|
||||
|
||||
@ -86,12 +86,13 @@ type SignJob struct {
|
||||
// transaction being signed.
|
||||
OutputIndex int32
|
||||
|
||||
// Cancel is a channel that should be closed if the caller wishes to
|
||||
// abandon all pending sign jobs part of a single batch.
|
||||
Cancel chan struct{}
|
||||
// Cancel is a channel that is closed by the caller if they wish to
|
||||
// abandon all pending sign jobs part of a single batch. This should
|
||||
// never be closed by the validator.
|
||||
Cancel <-chan struct{}
|
||||
|
||||
// Resp is the channel that the response to this particular SignJob
|
||||
// will be sent over.
|
||||
// will be sent over. This channel MUST be buffered.
|
||||
//
|
||||
// TODO(roasbeef): actually need to allow caller to set, need to retain
|
||||
// order mark commit sig as special
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -940,6 +941,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
||||
fundingPoint := aliceChanReservation.FundingOutpoint()
|
||||
_, err = bobChanReservation.CompleteReservationSingle(
|
||||
fundingPoint, aliceCommitSig,
|
||||
fn.None[lnwallet.AuxFundingDesc](),
|
||||
)
|
||||
require.NoError(t, err, "bob unable to consume single reservation")
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package lnwallet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
@ -21,6 +22,8 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -369,9 +372,13 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
|
||||
|
||||
// TODO(roasbeef): make mock version of pre-image store
|
||||
|
||||
auxSigner := NewDefaultAuxSignerMock(t)
|
||||
|
||||
alicePool := NewSigPool(1, aliceSigner)
|
||||
channelAlice, err := NewLightningChannel(
|
||||
aliceSigner, aliceChannelState, alicePool,
|
||||
WithLeafStore(&MockAuxLeafStore{}),
|
||||
WithAuxSigner(auxSigner),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -386,6 +393,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
|
||||
bobPool := NewSigPool(1, bobSigner)
|
||||
channelBob, err := NewLightningChannel(
|
||||
bobSigner, bobChannelState, bobPool,
|
||||
WithLeafStore(&MockAuxLeafStore{}),
|
||||
WithAuxSigner(auxSigner),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -586,3 +595,38 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDefaultAuxSignerMock(t *testing.T) *MockAuxSigner {
|
||||
auxSigner := &MockAuxSigner{}
|
||||
|
||||
type testSigBlob struct {
|
||||
BlobInt tlv.RecordT[tlv.TlvType65634, uint16]
|
||||
}
|
||||
|
||||
var sigBlobBuf bytes.Buffer
|
||||
sigBlob := testSigBlob{
|
||||
BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5),
|
||||
}
|
||||
tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record())
|
||||
require.NoError(t, err, "unable to create tlv stream")
|
||||
require.NoError(t, tlvStream.Encode(&sigBlobBuf))
|
||||
|
||||
auxSigner.On(
|
||||
"SubmitSecondLevelSigBatch", mock.Anything, mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(nil)
|
||||
auxSigner.On(
|
||||
"PackSigs", mock.Anything,
|
||||
).Return(fn.Ok(fn.Some(sigBlobBuf.Bytes())))
|
||||
auxSigner.On(
|
||||
"UnpackSigs", mock.Anything,
|
||||
).Return(fn.Ok([]fn.Option[tlv.Blob]{
|
||||
fn.Some(sigBlobBuf.Bytes()),
|
||||
}))
|
||||
auxSigner.On(
|
||||
"VerifySecondLevelSigs", mock.Anything, mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(nil)
|
||||
|
||||
return auxSigner
|
||||
}
|
||||
|
@ -1018,9 +1018,12 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
|
||||
tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2,
|
||||
}, nil)
|
||||
|
||||
auxSigner := NewDefaultAuxSignerMock(t)
|
||||
remotePool := NewSigPool(1, remoteSigner)
|
||||
channelRemote, err := NewLightningChannel(
|
||||
remoteSigner, remoteChannelState, remotePool,
|
||||
WithLeafStore(&MockAuxLeafStore{}),
|
||||
WithAuxSigner(auxSigner),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, remotePool.Start())
|
||||
@ -1028,6 +1031,8 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
|
||||
localPool := NewSigPool(1, localSigner)
|
||||
channelLocal, err := NewLightningChannel(
|
||||
localSigner, localChannelState, localPool,
|
||||
WithLeafStore(&MockAuxLeafStore{}),
|
||||
WithAuxSigner(auxSigner),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, localPool.Start())
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -90,6 +91,33 @@ func (p *PsbtFundingRequired) Error() string {
|
||||
return ErrPsbtFundingRequired.Error()
|
||||
}
|
||||
|
||||
// AuxFundingDesc stores a series of attributes that may be used to modify the
|
||||
// way the channel funding occurs. This struct contains information that can
|
||||
// only be derived once both sides have received and sent their contributions
|
||||
// to the channel (keys, etc.).
|
||||
type AuxFundingDesc struct {
|
||||
// CustomFundingBlob is a custom blob that'll be stored in the database
|
||||
// within the OpenChannel struct. This should represent information
|
||||
// static to the channel lifetime.
|
||||
CustomFundingBlob tlv.Blob
|
||||
|
||||
// CustomLocalCommitBlob is a custom blob that'll be stored in the
|
||||
// first commitment entry for the local party.
|
||||
CustomLocalCommitBlob tlv.Blob
|
||||
|
||||
// CustomRemoteCommitBlob is a custom blob that'll be stored in the
|
||||
// first commitment entry for the remote party.
|
||||
CustomRemoteCommitBlob tlv.Blob
|
||||
|
||||
// LocalInitAuxLeaves is the set of aux leaves that'll be used for our
|
||||
// very first commitment state.
|
||||
LocalInitAuxLeaves CommitAuxLeaves
|
||||
|
||||
// RemoteInitAuxLeaves is the set of aux leaves that'll be used for the
|
||||
// very first commitment state for the remote party.
|
||||
RemoteInitAuxLeaves CommitAuxLeaves
|
||||
}
|
||||
|
||||
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
||||
// required to open a payment channel with a remote peer. The initial required
|
||||
// parameters are configurable across channels. These parameters are to be
|
||||
@ -211,9 +239,8 @@ type InitFundingReserveMsg struct {
|
||||
// channel that will be useful to our future selves.
|
||||
Memo []byte
|
||||
|
||||
// TapscriptRoot is the root of the tapscript tree that will be used to
|
||||
// create the funding output. This is an optional field that should
|
||||
// only be set for taproot channels.
|
||||
// TapscriptRoot is an optional tapscript root that if provided, will
|
||||
// be used to create the combined key for musig2 based channels.
|
||||
TapscriptRoot fn.Option[chainhash.Hash]
|
||||
|
||||
// err is a channel in which all errors will be sent across. Will be
|
||||
@ -251,7 +278,6 @@ type fundingReserveCancelMsg struct {
|
||||
type addContributionMsg struct {
|
||||
pendingFundingID uint64
|
||||
|
||||
// TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode
|
||||
contribution *ChannelContribution
|
||||
|
||||
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
||||
@ -264,6 +290,10 @@ type addContributionMsg struct {
|
||||
type continueContributionMsg struct {
|
||||
pendingFundingID uint64
|
||||
|
||||
// auxFundingDesc is an optional descriptor that contains information
|
||||
// about the custom channel funding flow.
|
||||
auxFundingDesc fn.Option[AuxFundingDesc]
|
||||
|
||||
// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
|
||||
err chan error
|
||||
}
|
||||
@ -319,6 +349,10 @@ type addCounterPartySigsMsg struct {
|
||||
type addSingleFunderSigsMsg struct {
|
||||
pendingFundingID uint64
|
||||
|
||||
// auxFundingDesc is an optional descriptor that contains information
|
||||
// about the custom channel funding flow.
|
||||
auxFundingDesc fn.Option[AuxFundingDesc]
|
||||
|
||||
// fundingOutpoint is the outpoint of the completed funding
|
||||
// transaction as assembled by the workflow initiator.
|
||||
fundingOutpoint *wire.OutPoint
|
||||
@ -422,8 +456,6 @@ type LightningWallet struct {
|
||||
quit chan struct{}
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
// TODO(roasbeef): handle wallet lock/unlock
|
||||
}
|
||||
|
||||
// NewLightningWallet creates/opens and initializes a LightningWallet instance.
|
||||
@ -468,7 +500,6 @@ func (l *LightningWallet) Startup() error {
|
||||
}
|
||||
|
||||
l.wg.Add(1)
|
||||
// TODO(roasbeef): multiple request handlers?
|
||||
go l.requestHandler()
|
||||
|
||||
return nil
|
||||
@ -1424,7 +1455,6 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
|
||||
// transaction via coin selection are freed allowing future reservations to
|
||||
// include them.
|
||||
func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) {
|
||||
// TODO(roasbeef): holding lock too long
|
||||
l.limboMtx.Lock()
|
||||
defer l.limboMtx.Unlock()
|
||||
|
||||
@ -1449,11 +1479,6 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(roasbeef): is it even worth it to keep track of unused keys?
|
||||
|
||||
// TODO(roasbeef): Is it possible to mark the unused change also as
|
||||
// available?
|
||||
|
||||
delete(l.fundingLimbo, req.pendingFundingID)
|
||||
|
||||
pid := pendingReservation.pendingChanID
|
||||
@ -1473,7 +1498,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
|
||||
// createCommitOpts is a struct that holds the options for creating a new
|
||||
// commitment transaction.
|
||||
type createCommitOpts struct {
|
||||
auxLeaves fn.Option[CommitAuxLeaves]
|
||||
localAuxLeaves fn.Option[CommitAuxLeaves]
|
||||
remoteAuxLeaves fn.Option[CommitAuxLeaves]
|
||||
}
|
||||
|
||||
// defaultCommitOpts returns a new createCommitOpts with default values.
|
||||
@ -1481,6 +1507,17 @@ func defaultCommitOpts() createCommitOpts {
|
||||
return createCommitOpts{}
|
||||
}
|
||||
|
||||
// WithAuxLeaves is a functional option that can be used to set the aux leaves
|
||||
// for a new commitment transaction.
|
||||
func WithAuxLeaves(localLeaves,
|
||||
remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt {
|
||||
|
||||
return func(o *createCommitOpts) {
|
||||
o.localAuxLeaves = localLeaves
|
||||
o.remoteAuxLeaves = remoteLeaves
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCommitOpt is a functional option that can be used to modify the way a
|
||||
// new commitment transaction is created.
|
||||
type CreateCommitOpt func(*createCommitOpts)
|
||||
@ -1514,7 +1551,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
|
||||
ourCommitTx, err := CreateCommitTx(
|
||||
chanType, fundingTxIn, localCommitmentKeys, ourChanCfg,
|
||||
theirChanCfg, localBalance, remoteBalance, 0, initiator,
|
||||
leaseExpiry, options.auxLeaves,
|
||||
leaseExpiry, options.localAuxLeaves,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -1528,7 +1565,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
|
||||
theirCommitTx, err := CreateCommitTx(
|
||||
chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg,
|
||||
ourChanCfg, remoteBalance, localBalance, 0, !initiator,
|
||||
leaseExpiry, options.auxLeaves,
|
||||
leaseExpiry, options.remoteAuxLeaves,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -1621,16 +1658,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
||||
// and remote key which will be needed to calculate the multisig
|
||||
// funding output in a next step.
|
||||
pendingChanID := pendingReservation.pendingChanID
|
||||
|
||||
walletLog.Debugf("Advancing PSBT funding flow for "+
|
||||
"pending_id(%x), binding keys local_key=%v, "+
|
||||
"remote_key=%x", pendingChanID,
|
||||
&ourContribution.MultiSigKey,
|
||||
theirContribution.MultiSigKey.PubKey.SerializeCompressed())
|
||||
|
||||
fundingIntent.BindKeys(
|
||||
&ourContribution.MultiSigKey,
|
||||
theirContribution.MultiSigKey.PubKey,
|
||||
)
|
||||
|
||||
// We might have a tapscript root, so we'll bind that now to
|
||||
// ensure we make the proper funding output.
|
||||
fundingIntent.BindTapscriptRoot(
|
||||
pendingReservation.partialState.TapscriptRoot,
|
||||
)
|
||||
|
||||
// Exit early because we can't continue the funding flow yet.
|
||||
req.err <- &PsbtFundingRequired{
|
||||
Intent: fundingIntent,
|
||||
@ -1703,16 +1748,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
||||
// the commitment transaction for the remote party, and verify their incoming
|
||||
// partial signature.
|
||||
func genMusigSession(ourContribution, theirContribution *ChannelContribution,
|
||||
signer input.MuSig2Signer,
|
||||
fundingOutput *wire.TxOut) *MusigPairSession {
|
||||
signer input.MuSig2Signer, fundingOutput *wire.TxOut,
|
||||
tapscriptRoot fn.Option[chainhash.Hash]) *MusigPairSession {
|
||||
|
||||
return NewMusigPairSession(&MusigSessionCfg{
|
||||
LocalKey: ourContribution.MultiSigKey,
|
||||
RemoteKey: theirContribution.MultiSigKey,
|
||||
LocalNonce: *ourContribution.LocalNonce,
|
||||
RemoteNonce: *theirContribution.LocalNonce,
|
||||
Signer: signer,
|
||||
InputTxOut: fundingOutput,
|
||||
LocalKey: ourContribution.MultiSigKey,
|
||||
RemoteKey: theirContribution.MultiSigKey,
|
||||
LocalNonce: *ourContribution.LocalNonce,
|
||||
RemoteNonce: *theirContribution.LocalNonce,
|
||||
Signer: signer,
|
||||
InputTxOut: fundingOutput,
|
||||
TapscriptTweak: tapscriptRoot,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1762,6 +1808,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation,
|
||||
musigSessions := genMusigSession(
|
||||
ourContribution, theirContribution,
|
||||
l.Cfg.Signer, fundingOutput,
|
||||
pendingReservation.partialState.TapscriptRoot,
|
||||
)
|
||||
pendingReservation.musigSessions = musigSessions
|
||||
}
|
||||
@ -1797,6 +1844,26 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
return
|
||||
}
|
||||
|
||||
chanState := pendingReservation.partialState
|
||||
|
||||
// If we have an aux funding desc, then we can use it to populate some
|
||||
// of the optional, but opaque TLV blobs we'll carry for the channel.
|
||||
chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomFundingBlob
|
||||
})(req.auxFundingDesc)
|
||||
|
||||
chanState.LocalCommitment.CustomBlob = fn.MapOption(
|
||||
func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomLocalCommitBlob
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
|
||||
chanState.RemoteCommitment.CustomBlob = fn.MapOption(
|
||||
func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomRemoteCommitBlob
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
|
||||
ourContribution := pendingReservation.ourContribution
|
||||
theirContribution := pendingReservation.theirContribution
|
||||
chanPoint := pendingReservation.partialState.FundingOutpoint
|
||||
@ -1855,7 +1922,6 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
// Store their current commitment point. We'll need this after the
|
||||
// first state transition in order to verify the authenticity of the
|
||||
// revocation.
|
||||
chanState := pendingReservation.partialState
|
||||
chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint
|
||||
|
||||
// Create the txin to our commitment transaction; required to construct
|
||||
@ -1871,6 +1937,18 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
|
||||
leaseExpiry = pendingReservation.partialState.ThawHeight
|
||||
}
|
||||
|
||||
localAuxLeaves := fn.MapOption(
|
||||
func(desc AuxFundingDesc) CommitAuxLeaves {
|
||||
return desc.LocalInitAuxLeaves
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
remoteAuxLeaves := fn.MapOption(
|
||||
func(desc AuxFundingDesc) CommitAuxLeaves {
|
||||
return desc.RemoteInitAuxLeaves
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
|
||||
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
|
||||
localBalance, remoteBalance, ourContribution.ChannelConfig,
|
||||
theirContribution.ChannelConfig,
|
||||
@ -1878,6 +1956,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
|
||||
theirContribution.FirstCommitmentPoint, fundingTxIn,
|
||||
pendingReservation.partialState.ChanType,
|
||||
pendingReservation.partialState.IsInitiator, leaseExpiry,
|
||||
WithAuxLeaves(localAuxLeaves, remoteAuxLeaves),
|
||||
)
|
||||
if err != nil {
|
||||
req.err <- err
|
||||
@ -2138,6 +2217,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation,
|
||||
res.musigSessions = genMusigSession(
|
||||
res.ourContribution, res.theirContribution,
|
||||
l.Cfg.Signer, fundingOutput,
|
||||
res.partialState.TapscriptRoot,
|
||||
)
|
||||
}
|
||||
|
||||
@ -2228,9 +2308,6 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
|
||||
|
||||
// As we're about to broadcast the funding transaction, we'll take note
|
||||
// of the current height for record keeping purposes.
|
||||
//
|
||||
// TODO(roasbeef): this info can also be piped into light client's
|
||||
// basic fee estimation?
|
||||
_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()
|
||||
if err != nil {
|
||||
msg.err <- err
|
||||
@ -2291,6 +2368,23 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
||||
defer pendingReservation.Unlock()
|
||||
|
||||
chanState := pendingReservation.partialState
|
||||
|
||||
// If we have an aux funding desc, then we can use it to populate some
|
||||
// of the optional, but opaque TLV blobs we'll carry for the channel.
|
||||
chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomFundingBlob
|
||||
})(req.auxFundingDesc)
|
||||
chanState.LocalCommitment.CustomBlob = fn.MapOption(
|
||||
func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomLocalCommitBlob
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
chanState.RemoteCommitment.CustomBlob = fn.MapOption(
|
||||
func(desc AuxFundingDesc) tlv.Blob {
|
||||
return desc.CustomRemoteCommitBlob
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
|
||||
chanType := pendingReservation.partialState.ChanType
|
||||
chanState.FundingOutpoint = *req.fundingOutpoint
|
||||
fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil)
|
||||
@ -2304,6 +2398,18 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
||||
if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
|
||||
leaseExpiry = pendingReservation.partialState.ThawHeight
|
||||
}
|
||||
|
||||
localAuxLeaves := fn.MapOption(
|
||||
func(desc AuxFundingDesc) CommitAuxLeaves {
|
||||
return desc.LocalInitAuxLeaves
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
remoteAuxLeaves := fn.MapOption(
|
||||
func(desc AuxFundingDesc) CommitAuxLeaves {
|
||||
return desc.RemoteInitAuxLeaves
|
||||
},
|
||||
)(req.auxFundingDesc)
|
||||
|
||||
ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
|
||||
localBalance, remoteBalance,
|
||||
pendingReservation.ourContribution.ChannelConfig,
|
||||
@ -2312,6 +2418,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
||||
pendingReservation.theirContribution.FirstCommitmentPoint,
|
||||
*fundingTxIn, chanType,
|
||||
pendingReservation.partialState.IsInitiator, leaseExpiry,
|
||||
WithAuxLeaves(localAuxLeaves, remoteAuxLeaves),
|
||||
)
|
||||
if err != nil {
|
||||
req.err <- err
|
||||
@ -2500,6 +2607,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
|
||||
l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, WithLeafStore(s))
|
||||
})
|
||||
l.Cfg.AuxSigner.WhenSome(func(s AuxSigner) {
|
||||
chanOpts = append(chanOpts, WithAuxSigner(s))
|
||||
})
|
||||
|
||||
// First, we'll obtain a fully signed commitment transaction so we can
|
||||
// pass into it on the chanvalidate package for verification.
|
||||
|
@ -45,6 +45,10 @@ type CommitSig struct {
|
||||
// being signed for. In this case, the above Sig type MUST be blank.
|
||||
PartialSig OptPartialSigWithNonceTLV
|
||||
|
||||
// CustomRecords maps TLV types to byte slices, storing arbitrary data
|
||||
// intended for inclusion in the ExtraData field.
|
||||
CustomRecords CustomRecords
|
||||
|
||||
// ExtraData is the set of data that was appended to this message to
|
||||
// fill out the full maximum transport message size. These fields can
|
||||
// be used to specify optional data such as custom TLV fields.
|
||||
@ -53,9 +57,7 @@ type CommitSig struct {
|
||||
|
||||
// NewCommitSig creates a new empty CommitSig message.
|
||||
func NewCommitSig() *CommitSig {
|
||||
return &CommitSig{
|
||||
ExtraData: make([]byte, 0),
|
||||
}
|
||||
return &CommitSig{}
|
||||
}
|
||||
|
||||
// A compile time check to ensure CommitSig implements the lnwire.Message
|
||||
@ -67,34 +69,37 @@ var _ Message = (*CommitSig)(nil)
|
||||
//
|
||||
// This is part of the lnwire.Message interface.
|
||||
func (c *CommitSig) Decode(r io.Reader, pver uint32) error {
|
||||
// msgExtraData is a temporary variable used to read the message extra
|
||||
// data field from the reader.
|
||||
var msgExtraData ExtraOpaqueData
|
||||
|
||||
err := ReadElements(r,
|
||||
&c.ChanID,
|
||||
&c.CommitSig,
|
||||
&c.HtlcSigs,
|
||||
&msgExtraData,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tlvRecords ExtraOpaqueData
|
||||
if err := ReadElements(r, &tlvRecords); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract TLV records from the extra data field.
|
||||
partialSig := c.PartialSig.Zero()
|
||||
typeMap, err := tlvRecords.ExtractRecords(&partialSig)
|
||||
|
||||
customRecords, parsed, extraData, err := ParseAndExtractCustomRecords(
|
||||
msgExtraData, &partialSig,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the corresponding TLV types if they were included in the stream.
|
||||
if val, ok := typeMap[c.PartialSig.TlvType()]; ok && val == nil {
|
||||
if _, ok := parsed[partialSig.TlvType()]; ok {
|
||||
c.PartialSig = tlv.SomeRecordT(partialSig)
|
||||
}
|
||||
|
||||
if len(tlvRecords) != 0 {
|
||||
c.ExtraData = tlvRecords
|
||||
}
|
||||
c.CustomRecords = customRecords
|
||||
c.ExtraData = extraData
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -108,7 +113,10 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error {
|
||||
c.PartialSig.WhenSome(func(sig PartialSigWithNonceTLV) {
|
||||
recordProducers = append(recordProducers, &sig)
|
||||
})
|
||||
err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
|
||||
|
||||
extraData, err := MergeAndEncode(
|
||||
recordProducers, c.ExtraData, c.CustomRecords,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -125,7 +133,7 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return WriteBytes(w, c.ExtraData)
|
||||
return WriteBytes(w, extraData)
|
||||
}
|
||||
|
||||
// MsgType returns the integer uniquely identifying this message type on the
|
||||
|
168
lnwire/commit_sig_test.go
Normal file
168
lnwire/commit_sig_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
package lnwire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testCase is a test case for the CommitSig message.
|
||||
type commitSigTestCase struct {
|
||||
// Msg is the message to be encoded and decoded.
|
||||
Msg CommitSig
|
||||
|
||||
// ExpectEncodeError is a flag that indicates whether we expect the
|
||||
// encoding of the message to fail.
|
||||
ExpectEncodeError bool
|
||||
}
|
||||
|
||||
// generateCommitSigTestCases generates a set of CommitSig message test cases.
|
||||
func generateCommitSigTestCases(t *testing.T) []commitSigTestCase {
|
||||
// Firstly, we'll set basic values for the message fields.
|
||||
//
|
||||
// Generate random channel ID.
|
||||
chanIDBytes, err := generateRandomBytes(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
var chanID ChannelID
|
||||
copy(chanID[:], chanIDBytes)
|
||||
|
||||
// Generate random commit sig.
|
||||
commitSigBytes, err := generateRandomBytes(64)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig, err := NewSigFromSchnorrRawSignature(commitSigBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
sigScalar := new(btcec.ModNScalar)
|
||||
sigScalar.SetByteSlice(sig.RawBytes())
|
||||
|
||||
var nonce [musig2.PubNonceSize]byte
|
||||
copy(nonce[:], commitSigBytes)
|
||||
|
||||
sigWithNonce := NewPartialSigWithNonce(nonce, *sigScalar)
|
||||
partialSig := MaybePartialSigWithNonce(sigWithNonce)
|
||||
|
||||
// Define custom records.
|
||||
recordKey1 := uint64(MinCustomRecordsTlvType + 1)
|
||||
recordValue1, err := generateRandomBytes(10)
|
||||
require.NoError(t, err)
|
||||
|
||||
recordKey2 := uint64(MinCustomRecordsTlvType + 2)
|
||||
recordValue2, err := generateRandomBytes(10)
|
||||
require.NoError(t, err)
|
||||
|
||||
customRecords := CustomRecords{
|
||||
recordKey1: recordValue1,
|
||||
recordKey2: recordValue2,
|
||||
}
|
||||
|
||||
// Construct an instance of extra data that contains records with TLV
|
||||
// types below the minimum custom records threshold and that lack
|
||||
// corresponding fields in the message struct. Content should persist in
|
||||
// the extra data field after encoding and decoding.
|
||||
var (
|
||||
recordBytes45 = []byte("recordBytes45")
|
||||
tlvRecord45 = tlv.NewPrimitiveRecord[tlv.TlvType45](
|
||||
recordBytes45,
|
||||
)
|
||||
|
||||
recordBytes55 = []byte("recordBytes55")
|
||||
tlvRecord55 = tlv.NewPrimitiveRecord[tlv.TlvType55](
|
||||
recordBytes55,
|
||||
)
|
||||
)
|
||||
|
||||
var extraData ExtraOpaqueData
|
||||
err = extraData.PackRecords(
|
||||
[]tlv.RecordProducer{&tlvRecord45, &tlvRecord55}...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidCustomRecords := CustomRecords{
|
||||
MinCustomRecordsTlvType - 1: recordValue1,
|
||||
}
|
||||
|
||||
return []commitSigTestCase{
|
||||
{
|
||||
Msg: CommitSig{
|
||||
ChanID: chanID,
|
||||
CommitSig: sig,
|
||||
PartialSig: partialSig,
|
||||
CustomRecords: customRecords,
|
||||
ExtraData: extraData,
|
||||
},
|
||||
},
|
||||
// Add a test case where the blinding point field is not
|
||||
// populated.
|
||||
{
|
||||
Msg: CommitSig{
|
||||
ChanID: chanID,
|
||||
CommitSig: sig,
|
||||
CustomRecords: customRecords,
|
||||
},
|
||||
},
|
||||
// Add a test case where the custom records field is not
|
||||
// populated.
|
||||
{
|
||||
Msg: CommitSig{
|
||||
ChanID: chanID,
|
||||
CommitSig: sig,
|
||||
PartialSig: partialSig,
|
||||
},
|
||||
},
|
||||
// Add a case where the custom records are invalid.
|
||||
{
|
||||
Msg: CommitSig{
|
||||
ChanID: chanID,
|
||||
CommitSig: sig,
|
||||
PartialSig: partialSig,
|
||||
CustomRecords: invalidCustomRecords,
|
||||
},
|
||||
ExpectEncodeError: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitSigEncodeDecode tests CommitSig message encoding and decoding for
|
||||
// all supported field values.
|
||||
func TestCommitSigEncodeDecode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Generate test cases.
|
||||
testCases := generateCommitSigTestCases(t)
|
||||
|
||||
// Execute test cases.
|
||||
for tcIdx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("testcase-%d", tcIdx), func(t *testing.T) {
|
||||
// Encode test case message.
|
||||
var buf bytes.Buffer
|
||||
err := tc.Msg.Encode(&buf, 0)
|
||||
|
||||
// Check if we expect an encoding error.
|
||||
if tc.ExpectEncodeError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decode the encoded message bytes message.
|
||||
var actualMsg CommitSig
|
||||
decodeReader := bytes.NewReader(buf.Bytes())
|
||||
err = actualMsg.Decode(decodeReader, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The signature type isn't serialized.
|
||||
actualMsg.CommitSig.ForceSchnorr()
|
||||
|
||||
// Compare the two messages to ensure equality.
|
||||
require.Equal(t, tc.Msg, actualMsg)
|
||||
})
|
||||
}
|
||||
}
|
@ -965,6 +965,8 @@ func TestLightningWireProtocol(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
req.CustomRecords = randCustomRecords(t, r)
|
||||
|
||||
// 50/50 chance to attach a partial sig.
|
||||
if r.Int31()%2 == 0 {
|
||||
req.PartialSig = somePartialSigWithNonce(t, r)
|
||||
|
@ -376,6 +376,10 @@ type Config struct {
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// PongBuf is a slice we'll reuse instead of allocating memory on the
|
||||
// heap. Since only reads will occur and no writes, there is no need
|
||||
// for any synchronization primitives. As a result, it's safe to share
|
||||
@ -952,6 +956,9 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
|
||||
p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
lnChan, err := lnwallet.NewLightningChannel(
|
||||
p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts...,
|
||||
)
|
||||
@ -4164,6 +4171,9 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error {
|
||||
p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
|
||||
// If not already active, we'll add this channel to the set of active
|
||||
// channels, so we can look it up later easily according to its channel
|
||||
|
@ -304,6 +304,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
|
||||
alicePool := lnwallet.NewSigPool(1, aliceSigner)
|
||||
channelAlice, err := lnwallet.NewLightningChannel(
|
||||
aliceSigner, aliceChannelState, alicePool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -316,6 +318,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
|
||||
bobPool := lnwallet.NewSigPool(1, bobSigner)
|
||||
channelBob, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, bobChannelState, bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
130
rpcserver.go
130
rpcserver.go
@ -87,6 +87,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
)
|
||||
|
||||
@ -579,6 +580,17 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
|
||||
}
|
||||
}
|
||||
|
||||
// AuxDataParser is an interface that is used to parse auxiliary custom data
|
||||
// within RPC messages. This is used to transform binary blobs to human-readable
|
||||
// JSON representations.
|
||||
type AuxDataParser interface {
|
||||
// InlineParseCustomData replaces any custom data binary blob in the
|
||||
// given RPC message with its corresponding JSON formatted data. This
|
||||
// transforms the binary (likely TLV encoded) data to a human-readable
|
||||
// JSON representation (still as byte slice).
|
||||
InlineParseCustomData(msg proto.Message) error
|
||||
}
|
||||
|
||||
// rpcServer is a gRPC, RPC front end to the lnd daemon.
|
||||
// TODO(roasbeef): pagination support for the list-style calls
|
||||
type rpcServer struct {
|
||||
@ -731,6 +743,20 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
|
||||
},
|
||||
SetChannelAuto: s.chanStatusMgr.RequestAuto,
|
||||
UseStatusInitiated: subServerCgs.RouterRPC.UseStatusInitiated,
|
||||
ParseCustomChannelData: func(msg proto.Message) error {
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(msg)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing custom data: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
genInvoiceFeatures := func() *lnwire.FeatureVector {
|
||||
@ -3529,6 +3555,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
unsettledRemoteBalance lnwire.MilliSatoshi
|
||||
pendingOpenLocalBalance lnwire.MilliSatoshi
|
||||
pendingOpenRemoteBalance lnwire.MilliSatoshi
|
||||
customDataBuf bytes.Buffer
|
||||
)
|
||||
|
||||
openChannels, err := r.server.chanStateDB.FetchAllOpenChannels()
|
||||
@ -3536,6 +3563,12 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode the number of open channels to the custom data buffer.
|
||||
err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(openChannels)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, channel := range openChannels {
|
||||
c := channel.LocalCommitment
|
||||
localBalance += c.LocalBalance
|
||||
@ -3549,6 +3582,13 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
unsettledRemoteBalance += htlc.Amt
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the custom data for this open channel.
|
||||
openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil)
|
||||
err = wire.WriteVarBytes(&customDataBuf, 0, openChanData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pendingChannels, err := r.server.chanStateDB.FetchPendingChannels()
|
||||
@ -3556,10 +3596,23 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode the number of pending channels to the custom data buffer.
|
||||
err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(pendingChannels)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, channel := range pendingChannels {
|
||||
c := channel.LocalCommitment
|
||||
pendingOpenLocalBalance += c.LocalBalance
|
||||
pendingOpenRemoteBalance += c.RemoteBalance
|
||||
|
||||
// Encode the custom data for this pending channel.
|
||||
openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil)
|
||||
err = wire.WriteVarBytes(&customDataBuf, 0, openChanData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
rpcsLog.Debugf("[channelbalance] local_balance=%v remote_balance=%v "+
|
||||
@ -3569,7 +3622,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
unsettledRemoteBalance, pendingOpenLocalBalance,
|
||||
pendingOpenRemoteBalance)
|
||||
|
||||
return &lnrpc.ChannelBalanceResponse{
|
||||
resp := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(localBalance.ToSatoshis()),
|
||||
Msat: uint64(localBalance),
|
||||
@ -3594,11 +3647,24 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
|
||||
Sat: uint64(pendingOpenRemoteBalance.ToSatoshis()),
|
||||
Msat: uint64(pendingOpenRemoteBalance),
|
||||
},
|
||||
CustomChannelData: customDataBuf.Bytes(),
|
||||
|
||||
// Deprecated fields.
|
||||
Balance: int64(localBalance.ToSatoshis()),
|
||||
PendingOpenBalance: int64(pendingOpenLocalBalance.ToSatoshis()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(resp)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type (
|
||||
@ -3654,6 +3720,12 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
|
||||
pendingChan.BroadcastHeight()
|
||||
fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight
|
||||
|
||||
customChanBytes, err := encodeCustomChanData(pendingChan)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encode open chan "+
|
||||
"data: %w", err)
|
||||
}
|
||||
|
||||
result[i] = &lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
||||
Channel: &lnrpc.PendingChannelsResponse_PendingChannel{
|
||||
RemoteNodePub: hex.EncodeToString(pub),
|
||||
@ -3667,6 +3739,7 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
|
||||
CommitmentType: rpcCommitmentType(pendingChan.ChanType),
|
||||
Private: isPrivate(pendingChan),
|
||||
Memo: string(pendingChan.Memo),
|
||||
CustomChannelData: customChanBytes,
|
||||
},
|
||||
CommitWeight: commitWeight,
|
||||
CommitFee: int64(localCommitment.CommitFee),
|
||||
@ -4033,6 +4106,16 @@ func (r *rpcServer) PendingChannels(ctx context.Context,
|
||||
resp.WaitingCloseChannels = waitingCloseChannels
|
||||
resp.TotalLimboBalance += limbo
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(resp)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@ -4347,6 +4430,16 @@ func (r *rpcServer) ListChannels(ctx context.Context,
|
||||
resp.Channels = append(resp.Channels, channel)
|
||||
}
|
||||
|
||||
err = fn.MapOptionZ(
|
||||
r.server.implCfg.AuxDataParser,
|
||||
func(parser AuxDataParser) error {
|
||||
return parser.InlineParseCustomData(resp)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing custom data: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@ -4397,6 +4490,30 @@ func isPrivate(dbChannel *channeldb.OpenChannel) bool {
|
||||
return dbChannel.ChannelFlags&lnwire.FFAnnounceChannel != 1
|
||||
}
|
||||
|
||||
// encodeCustomChanData encodes the custom channel data for the open channel.
|
||||
// It encodes that data as a pair of var bytes blobs.
|
||||
func encodeCustomChanData(lnChan *channeldb.OpenChannel) ([]byte, error) {
|
||||
customOpenChanData := lnChan.CustomBlob.UnwrapOr(nil)
|
||||
customLocalCommitData := lnChan.LocalCommitment.CustomBlob.UnwrapOr(nil)
|
||||
|
||||
// We'll encode our custom channel data as two blobs. The first is a
|
||||
// set of var bytes encoding of the open chan data, the second is an
|
||||
// encoding of the local commitment data.
|
||||
var customChanDataBuf bytes.Buffer
|
||||
err := wire.WriteVarBytes(&customChanDataBuf, 0, customOpenChanData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encode open chan "+
|
||||
"data: %w", err)
|
||||
}
|
||||
err = wire.WriteVarBytes(&customChanDataBuf, 0, customLocalCommitData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encode local commit "+
|
||||
"data: %w", err)
|
||||
}
|
||||
|
||||
return customChanDataBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// createRPCOpenChannel creates an *lnrpc.Channel from the *channeldb.Channel.
|
||||
func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
|
||||
isActive, peerAliasLookup bool) (*lnrpc.Channel, error) {
|
||||
@ -4451,6 +4568,14 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
|
||||
// is returned and peerScidAlias will be an empty ShortChannelID.
|
||||
peerScidAlias, _ := r.server.aliasMgr.GetPeerAlias(chanID)
|
||||
|
||||
// Finally we'll attempt to encode the custom channel data if any
|
||||
// exists.
|
||||
customChanBytes, err := encodeCustomChanData(dbChannel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encode open chan data: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
channel := &lnrpc.Channel{
|
||||
Active: isActive,
|
||||
Private: isPrivate(dbChannel),
|
||||
@ -4483,6 +4608,7 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
|
||||
ZeroConf: dbChannel.IsZeroConf(),
|
||||
ZeroConfConfirmedScid: dbChannel.ZeroConfRealScid().ToUint64(),
|
||||
Memo: string(dbChannel.Memo),
|
||||
CustomChannelData: customChanBytes,
|
||||
// TODO: remove the following deprecated fields
|
||||
CsvDelay: uint32(dbChannel.LocalChanCfg.CsvDelay),
|
||||
LocalChanReserveSat: int64(dbChannel.LocalChanCfg.ChanReserve),
|
||||
|
@ -1,14 +1,79 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestGetAllPermissions(t *testing.T) {
|
||||
perms := GetAllPermissions()
|
||||
|
||||
// Currently there are there are 16 entity:action pairs in use.
|
||||
// Currently there are 16 entity:action pairs in use.
|
||||
assert.Equal(t, len(perms), 16)
|
||||
}
|
||||
|
||||
// mockDataParser is a mock implementation of the AuxDataParser interface.
|
||||
type mockDataParser struct {
|
||||
}
|
||||
|
||||
// InlineParseCustomData replaces any custom data binary blob in the given RPC
|
||||
// message with its corresponding JSON formatted data. This transforms the
|
||||
// binary (likely TLV encoded) data to a human-readable JSON representation
|
||||
// (still as byte slice).
|
||||
func (m *mockDataParser) InlineParseCustomData(msg proto.Message) error {
|
||||
switch m := msg.(type) {
|
||||
case *lnrpc.ChannelBalanceResponse:
|
||||
m.CustomChannelData = []byte(`{"foo": "bar"}`)
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("mock only supports ChannelBalanceResponse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuxDataParser(t *testing.T) {
|
||||
// We create an empty channeldb, so we can fetch some channels.
|
||||
cdb, err := channeldb.Open(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, cdb.Close())
|
||||
})
|
||||
|
||||
r := &rpcServer{
|
||||
server: &server{
|
||||
chanStateDB: cdb.ChannelStateDB(),
|
||||
implCfg: &ImplementationCfg{
|
||||
AuxComponents: AuxComponents{
|
||||
AuxDataParser: fn.Some[AuxDataParser](
|
||||
&mockDataParser{},
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// With the aux data parser in place, we should get a formatted JSON
|
||||
// in the custom channel data field.
|
||||
resp, err := r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, []byte(`{"foo": "bar"}`), resp.CustomChannelData)
|
||||
|
||||
// If we don't supply the aux data parser, we should get the raw binary
|
||||
// data. Which in this case is just two VarInt fields (1 byte each) that
|
||||
// represent the value of 0 (zero active and zero pending channels).
|
||||
r.server.implCfg.AuxComponents.AuxDataParser = fn.None[AuxDataParser]()
|
||||
|
||||
resp, err = r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, []byte{0x00, 0x00}, resp.CustomChannelData)
|
||||
}
|
||||
|
10
server.go
10
server.go
@ -1287,6 +1287,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
||||
return &pc.Incoming
|
||||
},
|
||||
AuxLeafStore: implCfg.AuxLeafStore,
|
||||
AuxSigner: implCfg.AuxSigner,
|
||||
}, dbs.ChanStateDB)
|
||||
|
||||
// Select the configuration and funding parameters for Bitcoin.
|
||||
@ -1531,9 +1532,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
||||
EnableUpfrontShutdown: cfg.EnableUpfrontShutdown,
|
||||
MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(
|
||||
s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(),
|
||||
DeleteAliasEdge: deleteAliasEdge,
|
||||
AliasManager: s.aliasMgr,
|
||||
IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint,
|
||||
DeleteAliasEdge: deleteAliasEdge,
|
||||
AliasManager: s.aliasMgr,
|
||||
IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint,
|
||||
AuxFundingController: implCfg.AuxFundingController,
|
||||
AuxSigner: implCfg.AuxSigner,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -4089,6 +4092,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
|
||||
MaxFeeExposure: thresholdMSats,
|
||||
Quit: s.quit,
|
||||
AuxLeafStore: s.implCfg.AuxLeafStore,
|
||||
AuxSigner: s.implCfg.AuxSigner,
|
||||
MsgRouter: s.implCfg.MsgRouter,
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user