mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loop: integrate sweepbatcher to loopout flow
This commit is contained in:
parent
849d26bba6
commit
0914074b10
7 changed files with 216 additions and 473 deletions
39
client.go
39
client.go
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
|
|
@ -61,7 +62,7 @@ var (
|
|||
// probeTimeout is the maximum time until a probe is allowed to take.
|
||||
probeTimeout = 3 * time.Minute
|
||||
|
||||
republishDelay = 10 * time.Second
|
||||
repushDelay = 1 * time.Second
|
||||
|
||||
// MinerFeeEstimationFailed is a magic number that is returned in a
|
||||
// quote call as the miner fee if the fee estimation in lnd's wallet
|
||||
|
|
@ -134,7 +135,8 @@ type ClientConfig struct {
|
|||
|
||||
// NewClient returns a new instance to initiate swaps with.
|
||||
func NewClient(dbDir string, loopDB loopdb.SwapStore,
|
||||
cfg *ClientConfig) (*Client, func(), error) {
|
||||
sweeperDb sweepbatcher.BatcherStore, cfg *ClientConfig) (
|
||||
*Client, func(), error) {
|
||||
|
||||
lsatStore, err := lsat.NewFileStore(dbDir)
|
||||
if err != nil {
|
||||
|
|
@ -162,27 +164,36 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
|
|||
Lnd: cfg.Lnd,
|
||||
}
|
||||
|
||||
verifySchnorrSig := func(pubKey *btcec.PublicKey, hash, sig []byte) error {
|
||||
schnorrSig, err := schnorr.ParseSignature(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !schnorrSig.Verify(hash, pubKey) {
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
batcher := sweepbatcher.NewBatcher(
|
||||
cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer,
|
||||
swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig,
|
||||
cfg.Lnd.ChainParams, sweeperDb, loopDB,
|
||||
)
|
||||
|
||||
executor := newExecutor(&executorConfig{
|
||||
lnd: cfg.Lnd,
|
||||
store: loopDB,
|
||||
sweeper: sweeper,
|
||||
batcher: batcher,
|
||||
createExpiryTimer: config.CreateExpiryTimer,
|
||||
loopOutMaxParts: cfg.LoopOutMaxParts,
|
||||
totalPaymentTimeout: cfg.TotalPaymentTimeout,
|
||||
maxPaymentRetries: cfg.MaxPaymentRetries,
|
||||
cancelSwap: swapServerClient.CancelLoopOutSwap,
|
||||
verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error {
|
||||
schnorrSig, err := schnorr.ParseSignature(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !schnorrSig.Verify(hash, pubKey) {
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
verifySchnorrSig: verifySchnorrSig,
|
||||
})
|
||||
|
||||
client := &Client{
|
||||
|
|
|
|||
23
executor.go
23
executor.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/queue"
|
||||
)
|
||||
|
|
@ -23,6 +24,8 @@ type executorConfig struct {
|
|||
|
||||
sweeper *sweep.Sweeper
|
||||
|
||||
batcher *sweepbatcher.Batcher
|
||||
|
||||
store loopdb.SwapStore
|
||||
|
||||
createExpiryTimer func(expiry time.Duration) <-chan time.Time
|
||||
|
|
@ -71,6 +74,7 @@ func (s *executor) run(mainCtx context.Context,
|
|||
err error
|
||||
blockEpochChan <-chan int32
|
||||
blockErrorChan <-chan error
|
||||
batcherErrChan chan error
|
||||
)
|
||||
|
||||
for {
|
||||
|
|
@ -121,6 +125,21 @@ func (s *executor) run(mainCtx context.Context,
|
|||
return mainCtx.Err()
|
||||
}
|
||||
|
||||
batcherErrChan = make(chan error, 1)
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
err := s.batcher.Run(mainCtx)
|
||||
if err != nil {
|
||||
select {
|
||||
case batcherErrChan <- err:
|
||||
case <-mainCtx.Done():
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start main event loop.
|
||||
log.Infof("Starting event loop at height %v", height)
|
||||
|
||||
|
|
@ -156,6 +175,7 @@ func (s *executor) run(mainCtx context.Context,
|
|||
err := newSwap.execute(mainCtx, &executeConfig{
|
||||
statusChan: statusChan,
|
||||
sweeper: s.sweeper,
|
||||
batcher: s.batcher,
|
||||
blockEpochChan: queue.ChanOut(),
|
||||
timerFactory: s.executorConfig.createExpiryTimer,
|
||||
loopOutMaxParts: s.executorConfig.loopOutMaxParts,
|
||||
|
|
@ -211,6 +231,9 @@ func (s *executor) run(mainCtx context.Context,
|
|||
case err := <-blockErrorChan:
|
||||
return fmt.Errorf("block error: %v", err)
|
||||
|
||||
case err := <-batcherErrChan:
|
||||
return fmt.Errorf("batcher error: %v", err)
|
||||
|
||||
case <-mainCtx.Done():
|
||||
return mainCtx.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/loopd/perms"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
loop_looprpc "github.com/lightninglabs/loop/looprpc"
|
||||
|
|
@ -412,9 +413,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams)
|
||||
|
||||
// Create an instance of the loop client library.
|
||||
swapClient, clientCleanup, err := getClient(
|
||||
d.cfg, swapDb, &d.lnd.LndServices,
|
||||
d.cfg, swapDb, sweeperDb, &d.lnd.LndServices,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import (
|
|||
"github.com/lightninglabs/loop/liquidity"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/ticker"
|
||||
)
|
||||
|
||||
// getClient returns an instance of the swap client.
|
||||
func getClient(cfg *Config, swapDb loopdb.SwapStore,
|
||||
lnd *lndclient.LndServices) (*loop.Client, func(), error) {
|
||||
sweeperDb sweepbatcher.BatcherStore, lnd *lndclient.LndServices) (
|
||||
*loop.Client, func(), error) {
|
||||
|
||||
clientConfig := &loop.ClientConfig{
|
||||
ServerAddress: cfg.Server.Host,
|
||||
|
|
@ -33,7 +35,7 @@ func getClient(cfg *Config, swapDb loopdb.SwapStore,
|
|||
}
|
||||
|
||||
swapClient, cleanUp, err := loop.NewClient(
|
||||
cfg.DataDir, swapDb, clientConfig,
|
||||
cfg.DataDir, swapDb, sweeperDb, clientConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
)
|
||||
|
||||
|
|
@ -26,12 +27,16 @@ func view(config *Config, lisCfg *ListenerCfg) error {
|
|||
return err
|
||||
}
|
||||
|
||||
swapDb, _, err := openDatabase(config, chainParams)
|
||||
swapDb, baseDb, err := openDatabase(config, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swapClient, cleanup, err := getClient(config, swapDb, &lnd.LndServices)
|
||||
sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams)
|
||||
|
||||
swapClient, cleanup, err := getClient(
|
||||
config, swapDb, sweeperDb, &lnd.LndServices,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
561
loopout.go
561
loopout.go
|
|
@ -1,7 +1,6 @@
|
|||
package loop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
|
|
@ -12,20 +11,17 @@ import (
|
|||
"time"
|
||||
|
||||
"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/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/labels"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/sweep"
|
||||
"github.com/lightninglabs/loop/sweepbatcher"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
|
@ -90,6 +86,7 @@ type loopOutSwap struct {
|
|||
// executeConfig contains extra configuration to execute the swap.
|
||||
type executeConfig struct {
|
||||
sweeper *sweep.Sweeper
|
||||
batcher *sweepbatcher.Batcher
|
||||
statusChan chan<- SwapInfo
|
||||
blockEpochChan <-chan interface{}
|
||||
timerFactory func(time.Duration) <-chan time.Time
|
||||
|
|
@ -517,7 +514,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
|
|||
}
|
||||
|
||||
// Try to spend htlc and continue (rbf) until a spend has confirmed.
|
||||
spendDetails, err := s.waitForHtlcSpendConfirmed(
|
||||
spendTx, err := s.waitForHtlcSpendConfirmedV2(
|
||||
globalCtx, *htlcOutpoint, htlcValue,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -526,7 +523,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
|
|||
|
||||
// If spend details are nil, we resolved the swap without waiting for
|
||||
// its spend, so we can exit.
|
||||
if spendDetails == nil {
|
||||
if spendTx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +531,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
|
|||
// don't just try to match with the hash of our sweep tx, because it
|
||||
// may be swept by a different (fee) sweep tx from a previous run.
|
||||
htlcInput, err := swap.GetTxInputByOutpoint(
|
||||
spendDetails.SpendingTx, htlcOutpoint,
|
||||
spendTx, htlcOutpoint,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -545,7 +542,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
|
|||
s.cost.Server -= htlcValue
|
||||
|
||||
s.cost.Onchain = htlcValue -
|
||||
btcutil.Amount(spendDetails.SpendingTx.TxOut[0].Value)
|
||||
btcutil.Amount(spendTx.TxOut[0].Value)
|
||||
|
||||
s.state = loopdb.StateSuccess
|
||||
} else {
|
||||
|
|
@ -1004,26 +1001,36 @@ func (s *loopOutSwap) waitForConfirmedHtlc(globalCtx context.Context) (
|
|||
return txConf, nil
|
||||
}
|
||||
|
||||
// waitForHtlcSpendConfirmed waits for the htlc to be spent either by our own
|
||||
// sweep or a server revocation tx. During this process, this function will try
|
||||
// to spend the htlc every block by calling spendFunc.
|
||||
//
|
||||
// TODO: Improve retry/fee increase mechanism. Once in the mempool, server can
|
||||
// sweep offchain. So we must make sure we sweep successfully before on-chain
|
||||
// timeout.
|
||||
func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
|
||||
// waitForHtlcSpendConfirmedV2 waits for the htlc to be spent either by our own
|
||||
// sweep or a server revocation tx.
|
||||
func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
|
||||
htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) (
|
||||
*chainntnfs.SpendDetail, error) {
|
||||
*wire.MsgTx, error) {
|
||||
|
||||
spendChan := make(chan *wire.MsgTx)
|
||||
spendErrChan := make(chan error, 1)
|
||||
quitChan := make(chan bool, 1)
|
||||
|
||||
defer func() {
|
||||
quitChan <- true
|
||||
}()
|
||||
|
||||
notifier := sweepbatcher.SpendNotifier{
|
||||
SpendChan: spendChan,
|
||||
SpendErrChan: spendErrChan,
|
||||
QuitChan: quitChan,
|
||||
}
|
||||
|
||||
sweepReq := sweepbatcher.SweepRequest{
|
||||
SwapHash: s.hash,
|
||||
Outpoint: htlcOutpoint,
|
||||
Value: htlcValue,
|
||||
Notifier: ¬ifier,
|
||||
}
|
||||
|
||||
// Register the htlc spend notification.
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
|
||||
ctx, &htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("register spend ntfn: %v", err)
|
||||
}
|
||||
|
||||
// Track our payment status so that we can detect whether our off chain
|
||||
// htlc is settled. We track this information to determine whether it is
|
||||
|
|
@ -1040,26 +1047,20 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
|
|||
// is used to decide whether we need to push our preimage to
|
||||
// the server.
|
||||
paymentComplete bool
|
||||
// musigSweepTryCount tracts the number of cooperative, MuSig2
|
||||
// sweep attempts.
|
||||
musigSweepTryCount int
|
||||
// musigSweepSuccess tracks whether at least one MuSig2 sweep
|
||||
// txn was successfully published to the mempool.
|
||||
musigSweepSuccess bool
|
||||
)
|
||||
|
||||
timerChan := s.timerFactory(republishDelay)
|
||||
timerChan := s.timerFactory(repushDelay)
|
||||
|
||||
for {
|
||||
select {
|
||||
// Htlc spend, break loop.
|
||||
case spendDetails := <-spendChan:
|
||||
s.log.Infof("Htlc spend by tx: %v",
|
||||
spendDetails.SpenderTxHash)
|
||||
case spendTx := <-spendChan:
|
||||
s.log.Infof("Htlc spend by tx: %v", spendTx.TxHash())
|
||||
|
||||
return spendDetails, nil
|
||||
return spendTx, nil
|
||||
|
||||
// Spend notification error.
|
||||
case err := <-spendErr:
|
||||
case err := <-spendErrChan:
|
||||
return nil, err
|
||||
|
||||
// Receive status updates for our payment so that we can detect
|
||||
|
|
@ -1101,121 +1102,51 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// New block arrived, update height and restart the republish
|
||||
// timer.
|
||||
// New block arrived, update height and try pushing preimage.
|
||||
case notification := <-s.blockEpochChan:
|
||||
s.height = notification.(int32)
|
||||
timerChan = s.timerFactory(republishDelay)
|
||||
timerChan = s.timerFactory(repushDelay)
|
||||
|
||||
// Some time after start or after arrival of a new block, try
|
||||
// to spend again.
|
||||
case <-timerChan:
|
||||
if IsTaprootSwap(&s.SwapContract) {
|
||||
// sweepConfTarget will return false if the
|
||||
// preimage is not revealed yet but the conf
|
||||
// target is closer than 20 blocks. In this case
|
||||
// to be sure we won't attempt to sweep at all
|
||||
// and we won't reveal the preimage either.
|
||||
_, canSweep := s.sweepConfTarget()
|
||||
if !canSweep {
|
||||
s.log.Infof("Aborting swap, timed " +
|
||||
"out on-chain")
|
||||
// sweepConfTarget will return false if the preimage is
|
||||
// not revealed yet but the conf target is closer than
|
||||
// 20 blocks. In this case to be sure we won't attempt
|
||||
// to sweep at all and we won't reveal the preimage
|
||||
// either.
|
||||
_, canSweep := s.sweepConfTarget()
|
||||
if !canSweep {
|
||||
s.log.Infof("Aborting swap, timed " +
|
||||
"out on-chain")
|
||||
|
||||
s.state = loopdb.StateFailTimeout
|
||||
err := s.persistState(ctx)
|
||||
if err != nil {
|
||||
log.Warnf("unable to persist " +
|
||||
"state")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// When using taproot HTLCs we're pushing the
|
||||
// preimage before attempting to sweep. This
|
||||
// way the server will know that the swap will
|
||||
// go through and we'll be able to MuSig2
|
||||
// cosign our sweep transaction. In the worst
|
||||
// case if the server is uncooperative for any
|
||||
// reason we can still sweep using scriptpath
|
||||
// spend.
|
||||
err = s.setStatePreimageRevealed(ctx)
|
||||
s.state = loopdb.StateFailTimeout
|
||||
err := s.persistState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Warnf("unable to persist " +
|
||||
"state")
|
||||
}
|
||||
|
||||
if !paymentComplete {
|
||||
// Push the preimage for as long as the
|
||||
// server is able to settle the swap
|
||||
// invoice. So that we can continue
|
||||
// with the MuSig2 sweep afterwards.
|
||||
s.pushPreimage(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Now attempt to publish a MuSig2 sweep txn.
|
||||
// Only attempt at most maxMusigSweepRetires
|
||||
// times to still leave time for an emergency
|
||||
// script path sweep.
|
||||
if musigSweepTryCount < maxMusigSweepRetries {
|
||||
success := s.sweepMuSig2(
|
||||
ctx, htlcOutpoint, htlcValue,
|
||||
)
|
||||
if !success {
|
||||
musigSweepTryCount++
|
||||
} else {
|
||||
// Mark that we had a sweep
|
||||
// that was successful. There's
|
||||
// no need for the script spend
|
||||
// now we can just keep pushing
|
||||
// new sweeps to bump the fee.
|
||||
musigSweepSuccess = true
|
||||
}
|
||||
} else if !musigSweepSuccess {
|
||||
// Attempt to script path sweep. If the
|
||||
// sweep fails, we can't do any better
|
||||
// than go on and try again later as
|
||||
// the preimage is already revealed and
|
||||
// the server settled the swap payment.
|
||||
// From the server's point of view the
|
||||
// swap is succeeded at this point so
|
||||
// we are free to retry as long as we
|
||||
// want.
|
||||
err := s.sweep(
|
||||
ctx, htlcOutpoint, htlcValue,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to publish "+
|
||||
"non-cooperative "+
|
||||
"sweep: %v", err)
|
||||
}
|
||||
}
|
||||
// Send the sweep to the sweeper.
|
||||
err := s.batcher.AddSweep(&sweepReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the result of our spend func was that the
|
||||
// swap has reached a final state, then we
|
||||
// return nil spend details, because there is
|
||||
// no further action required for this swap.
|
||||
if s.state.Type() != loopdb.StateTypePending {
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
err := s.sweep(ctx, htlcOutpoint, htlcValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Now that the sweep is taken care of, we can update
|
||||
// our state.
|
||||
err = s.setStatePreimageRevealed(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the result of our spend func was that the
|
||||
// swap has reached a final state, then we
|
||||
// return nil spend details, because there is no
|
||||
// further action required for this swap.
|
||||
if s.state.Type() != loopdb.StateTypePending {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If our off chain payment is not yet complete,
|
||||
// we try to push our preimage to the server.
|
||||
if !paymentComplete {
|
||||
s.pushPreimage(ctx)
|
||||
}
|
||||
if !paymentComplete {
|
||||
// Push the preimage for as long as the
|
||||
// server is able to settle the swap
|
||||
// invoice. So that we can continue
|
||||
// with the MuSig2 sweep afterwards.
|
||||
s.pushPreimage(ctx)
|
||||
}
|
||||
|
||||
// Context canceled.
|
||||
|
|
@ -1324,248 +1255,6 @@ func (s *loopOutSwap) failOffChain(ctx context.Context, paymentType paymentType,
|
|||
}
|
||||
}
|
||||
|
||||
// createMuSig2SweepTxn creates a taproot keyspend sweep transaction and
|
||||
// attempts to cooperate with the server to create a MuSig2 signature witness.
|
||||
func (s *loopOutSwap) createMuSig2SweepTxn(
|
||||
ctx context.Context, htlcOutpoint wire.OutPoint,
|
||||
htlcValue btcutil.Amount, fee btcutil.Amount) (*wire.MsgTx, error) {
|
||||
|
||||
// First assemble our taproot keyspend sweep transaction and get the
|
||||
// sig hash.
|
||||
sweepTx, sweepTxPsbt, sigHash, err := s.sweeper.CreateUnsignedTaprootKeySpendSweepTx(
|
||||
ctx, uint32(s.height), s.htlc, htlcOutpoint, htlcValue, fee,
|
||||
s.DestAddr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
signers [][]byte
|
||||
muSig2Version input.MuSig2Version
|
||||
)
|
||||
|
||||
// Depending on the MuSig2 version we either pass 32 byte Schnorr
|
||||
// public keys or normal 33 byte public keys.
|
||||
if s.ProtocolVersion >= loopdb.ProtocolVersionMuSig2 {
|
||||
muSig2Version = input.MuSig2Version100RC2
|
||||
signers = [][]byte{
|
||||
s.HtlcKeys.SenderInternalPubKey[:],
|
||||
s.HtlcKeys.ReceiverInternalPubKey[:],
|
||||
}
|
||||
} else {
|
||||
muSig2Version = input.MuSig2Version040
|
||||
signers = [][]byte{
|
||||
s.HtlcKeys.SenderInternalPubKey[1:],
|
||||
s.HtlcKeys.ReceiverInternalPubKey[1:],
|
||||
}
|
||||
}
|
||||
|
||||
htlcScript, ok := s.htlc.HtlcScript.(*swap.HtlcScriptV3)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("non taproot htlc")
|
||||
}
|
||||
|
||||
// Now we're creating a local MuSig2 session using the receiver key's
|
||||
// key locator and the htlc's root hash.
|
||||
musig2SessionInfo, err := s.lnd.Signer.MuSig2CreateSession(
|
||||
ctx, muSig2Version, &s.HtlcKeys.ClientScriptKeyLocator, signers,
|
||||
lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// With the session active, we can now send the server our public nonce
|
||||
// and the sig hash, so that it can create it's own MuSig2 session and
|
||||
// return the server side nonce and partial signature.
|
||||
serverNonce, serverSig, err := s.swapKit.server.MuSig2SignSweep(
|
||||
ctx, s.SwapContract.ProtocolVersion, s.hash,
|
||||
s.swapInvoicePaymentAddr, musig2SessionInfo.PublicNonce[:],
|
||||
sweepTxPsbt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var serverPublicNonce [musig2.PubNonceSize]byte
|
||||
copy(serverPublicNonce[:], serverNonce)
|
||||
|
||||
// Register the server's nonce before attempting to create our partial
|
||||
// signature.
|
||||
haveAllNonces, err := s.lnd.Signer.MuSig2RegisterNonces(
|
||||
ctx, musig2SessionInfo.SessionID,
|
||||
[][musig2.PubNonceSize]byte{serverPublicNonce},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sanity check that we have all the nonces.
|
||||
if !haveAllNonces {
|
||||
return nil, fmt.Errorf("invalid MuSig2 session: nonces missing")
|
||||
}
|
||||
|
||||
var digest [32]byte
|
||||
copy(digest[:], sigHash)
|
||||
|
||||
// Since our MuSig2 session has all nonces, we can now create the local
|
||||
// partial signature by signing the sig hash.
|
||||
_, err = s.lnd.Signer.MuSig2Sign(
|
||||
ctx, musig2SessionInfo.SessionID, digest, false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now combine the partial signatures to use the final combined
|
||||
// signature in the sweep transaction's witness.
|
||||
haveAllSigs, finalSig, err := s.lnd.Signer.MuSig2CombineSig(
|
||||
ctx, musig2SessionInfo.SessionID, [][]byte{serverSig},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !haveAllSigs {
|
||||
return nil, fmt.Errorf("failed to combine signatures")
|
||||
}
|
||||
|
||||
// To be sure that we're good, parse and validate that the combined
|
||||
// signature is indeed valid for the sig hash and the internal pubkey.
|
||||
err = s.executeConfig.verifySchnorrSig(
|
||||
htlcScript.TaprootKey, sigHash, finalSig,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now that we know the signature is correct, we can fill it in to our
|
||||
// witness.
|
||||
sweepTx.TxIn[0].Witness = wire.TxWitness{
|
||||
finalSig,
|
||||
}
|
||||
|
||||
return sweepTx, nil
|
||||
}
|
||||
|
||||
// sweepConfTarget returns the confirmation target for the htlc sweep or false
|
||||
// if we're too late.
|
||||
func (s *loopOutSwap) sweepConfTarget() (int32, bool) {
|
||||
remainingBlocks := s.CltvExpiry - s.height
|
||||
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
|
||||
preimageRevealed := s.state == loopdb.StatePreimageRevealed
|
||||
|
||||
// If we have not revealed our preimage, and we don't have time left
|
||||
// to sweep the swap, we abandon the swap because we can no longer
|
||||
// sweep the success path (without potentially having to compete with
|
||||
// the server's timeout sweep), and we have not had any coins pulled
|
||||
// off-chain.
|
||||
if blocksToLastReveal <= 0 && !preimageRevealed {
|
||||
s.log.Infof("Preimage can no longer be safely revealed: "+
|
||||
"expires at: %v, current height: %v", s.CltvExpiry,
|
||||
s.height)
|
||||
|
||||
s.state = loopdb.StateFailTimeout
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Calculate the transaction fee based on the confirmation target
|
||||
// required to sweep the HTLC before the timeout. We'll use the
|
||||
// confirmation target provided by the client unless we've come too
|
||||
// close to the expiration height, in which case we'll use the default
|
||||
// if it is better than what the client provided.
|
||||
confTarget := s.SweepConfTarget
|
||||
if remainingBlocks <= DefaultSweepConfTargetDelta &&
|
||||
confTarget > DefaultSweepConfTarget {
|
||||
|
||||
confTarget = DefaultSweepConfTarget
|
||||
}
|
||||
|
||||
return confTarget, true
|
||||
}
|
||||
|
||||
// clampSweepFee will clamp the passed in sweep fee to the maximum configured
|
||||
// miner fee. Returns false if sweeping should not continue. Note that in the
|
||||
// MuSig2 case we always continue as the preimage is revealed to the server
|
||||
// before cooperatively signing the sweep transaction.
|
||||
func (s *loopOutSwap) clampSweepFee(fee btcutil.Amount) (btcutil.Amount, bool) {
|
||||
// Ensure it doesn't exceed our maximum fee allowed.
|
||||
if fee > s.MaxMinerFee {
|
||||
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
|
||||
fee, s.MaxMinerFee)
|
||||
|
||||
if s.state == loopdb.StatePreimageRevealed {
|
||||
// The currently required fee exceeds the max, but we
|
||||
// already revealed the preimage. The best we can do now
|
||||
// is to republish with the max fee.
|
||||
fee = s.MaxMinerFee
|
||||
} else {
|
||||
s.log.Warnf("Not revealing preimage")
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
return fee, true
|
||||
}
|
||||
|
||||
// sweepMuSig2 attempts to sweep the on-chain HTLC using MuSig2. If anything
|
||||
// fails, we'll log it but will simply return to allow further retries. Since
|
||||
// the preimage is revealed by the time we attempt to MuSig2 sweep, we'll need
|
||||
// to fall back to a script spend sweep if all MuSig2 sweep attempts fail (for
|
||||
// example the server could be down due to maintenance or any other issue
|
||||
// making the cooperative sweep fail).
|
||||
func (s *loopOutSwap) sweepMuSig2(ctx context.Context,
|
||||
htlcOutpoint wire.OutPoint, htlcValue btcutil.Amount) bool {
|
||||
|
||||
addInputToEstimator := func(e *input.TxWeightEstimator) error {
|
||||
e.AddTaprootKeySpendInput(txscript.SigHashDefault)
|
||||
return nil
|
||||
}
|
||||
|
||||
confTarget, _ := s.sweepConfTarget()
|
||||
fee, err := s.sweeper.GetSweepFee(
|
||||
ctx, addInputToEstimator, s.DestAddr, confTarget,
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Warnf("Failed to estimate fee MuSig2 sweep txn: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
fee, _ = s.clampSweepFee(fee)
|
||||
|
||||
// Now attempt the co-signing of the txn.
|
||||
sweepTx, err := s.createMuSig2SweepTxn(
|
||||
ctx, htlcOutpoint, htlcValue, fee,
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Warnf("Failed to create MuSig2 sweep txn: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Finally, try publish the txn.
|
||||
s.log.Infof("Sweep on chain HTLC using MuSig2 to address %v "+
|
||||
"fee %v (tx %v)", s.DestAddr, fee, sweepTx.TxHash())
|
||||
|
||||
err = s.lnd.WalletKit.PublishTransaction(
|
||||
ctx, sweepTx,
|
||||
labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)),
|
||||
)
|
||||
if err != nil {
|
||||
var sweepTxBuf bytes.Buffer
|
||||
if err := sweepTx.Serialize(&sweepTxBuf); err != nil {
|
||||
s.log.Warnf("Unable to serialize sweep txn: %v", err)
|
||||
}
|
||||
|
||||
s.log.Warnf("Publish of MuSig2 sweep failed: %v. Raw tx: %x",
|
||||
err, sweepTxBuf.Bytes())
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error {
|
||||
if s.state != loopdb.StatePreimageRevealed {
|
||||
s.state = loopdb.StatePreimageRevealed
|
||||
|
|
@ -1579,78 +1268,6 @@ func (s *loopOutSwap) setStatePreimageRevealed(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// sweep tries to sweep the given htlc to a destination address. It takes into
|
||||
// account the max miner fee and unless the preimage is already revealed
|
||||
// (MuSig2 case), marks the preimage as revealed when it published the tx. If
|
||||
// the preimage has not yet been revealed, and the time during which we can
|
||||
// safely reveal it has passed, the swap will be marked as failed, and the
|
||||
// function will return.
|
||||
func (s *loopOutSwap) sweep(ctx context.Context, htlcOutpoint wire.OutPoint,
|
||||
htlcValue btcutil.Amount) error {
|
||||
|
||||
confTarget, canSweep := s.sweepConfTarget()
|
||||
if !canSweep {
|
||||
return nil
|
||||
}
|
||||
|
||||
fee, err := s.sweeper.GetSweepFee(
|
||||
ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fee, canSweep = s.clampSweepFee(fee)
|
||||
if !canSweep {
|
||||
return nil
|
||||
}
|
||||
|
||||
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
|
||||
return s.htlc.GenSuccessWitness(sig, s.Preimage)
|
||||
}
|
||||
|
||||
// Retrieve the full script required to unlock the output.
|
||||
redeemScript := s.htlc.SuccessScript()
|
||||
|
||||
// Create sweep tx.
|
||||
sweepTx, err := s.sweeper.CreateSweepTx(
|
||||
ctx, s.height, s.htlc.SuccessSequence(), s.htlc,
|
||||
htlcOutpoint, s.contract.HtlcKeys.ReceiverScriptKey,
|
||||
redeemScript, witnessFunc, htlcValue, fee, s.DestAddr,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Before publishing the tx, already mark the preimage as revealed. This
|
||||
// is a precaution in case the publish call never returns and would
|
||||
// leave us thinking we didn't reveal yet.
|
||||
err = s.setStatePreimageRevealed(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Publish tx.
|
||||
s.log.Infof("Sweep on chain HTLC to address %v with fee %v (tx %v)",
|
||||
s.DestAddr, fee, sweepTx.TxHash())
|
||||
|
||||
err = s.lnd.WalletKit.PublishTransaction(
|
||||
ctx, sweepTx,
|
||||
labels.LoopOutSweepSuccess(swap.ShortHash(&s.hash)),
|
||||
)
|
||||
if err != nil {
|
||||
var sweepTxBuf bytes.Buffer
|
||||
if err := sweepTx.Serialize(&sweepTxBuf); err != nil {
|
||||
s.log.Warnf("Unable to serialize sweep txn: %v", err)
|
||||
}
|
||||
|
||||
s.log.Warnf("Publish sweep failed: %v. Raw tx: %x",
|
||||
err, sweepTxBuf.Bytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLoopOutContract validates the contract parameters against our
|
||||
// request.
|
||||
func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest,
|
||||
|
|
@ -1696,3 +1313,39 @@ func validateLoopOutContract(lnd *lndclient.LndServices, request *OutRequest,
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sweepConfTarget returns the confirmation target for the htlc sweep or false
|
||||
// if we're too late.
|
||||
func (s *loopOutSwap) sweepConfTarget() (int32, bool) {
|
||||
remainingBlocks := s.CltvExpiry - s.height
|
||||
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
|
||||
preimageRevealed := s.state == loopdb.StatePreimageRevealed
|
||||
|
||||
// If we have not revealed our preimage, and we don't have time left
|
||||
// to sweep the swap, we abandon the swap because we can no longer
|
||||
// sweep the success path (without potentially having to compete with
|
||||
// the server's timeout sweep), and we have not had any coins pulled
|
||||
// off-chain.
|
||||
if blocksToLastReveal <= 0 && !preimageRevealed {
|
||||
s.log.Infof("Preimage can no longer be safely revealed: "+
|
||||
"expires at: %v, current height: %v", s.CltvExpiry,
|
||||
s.height)
|
||||
|
||||
s.state = loopdb.StateFailTimeout
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Calculate the transaction fee based on the confirmation target
|
||||
// required to sweep the HTLC before the timeout. We'll use the
|
||||
// confirmation target provided by the client unless we've come too
|
||||
// close to the expiration height, in which case we'll use the default
|
||||
// if it is better than what the client provided.
|
||||
confTarget := s.SweepConfTarget
|
||||
if remainingBlocks <= DefaultSweepConfTargetDelta &&
|
||||
confTarget > DefaultSweepConfTarget {
|
||||
|
||||
confTarget = DefaultSweepConfTarget
|
||||
}
|
||||
|
||||
return confTarget, true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/aperture/lsat"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
looprpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||
|
|
@ -775,6 +776,51 @@ func (s *grpcSwapServerClient) MuSig2SignSweep(ctx context.Context,
|
|||
return res.Nonce, res.PartialSignature, nil
|
||||
}
|
||||
|
||||
// MultiMuSig2SignSweep calls the server to cooperatively sign an input in
|
||||
// a batch transaction that attempts to sweep multiple htlcs at once. This
|
||||
// method is called once per input signed. The prevoutMap is a map of all the
|
||||
// prevout information for each spend outpoint. Returns the server's nonce and
|
||||
// partial signature.
|
||||
func (s *grpcSwapServerClient) MultiMuSig2SignSweep(ctx context.Context,
|
||||
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
|
||||
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
|
||||
prevoutMap map[wire.OutPoint]*wire.TxOut) (
|
||||
[]byte, []byte, error) {
|
||||
|
||||
prevOutInfo := make([]*looprpc.PrevoutInfo, 0, len(prevoutMap))
|
||||
for prevOut, txOut := range prevoutMap {
|
||||
txOut := *txOut
|
||||
prevOut := prevOut
|
||||
|
||||
prevOutInfo = append(prevOutInfo,
|
||||
&looprpc.PrevoutInfo{
|
||||
TxidBytes: prevOut.Hash[:],
|
||||
OutputIndex: prevOut.Index,
|
||||
Value: uint64(txOut.Value),
|
||||
PkScript: txOut.PkScript,
|
||||
})
|
||||
}
|
||||
|
||||
req := &looprpc.MuSig2SignSweepReq{
|
||||
ProtocolVersion: looprpc.ProtocolVersion(protocolVersion),
|
||||
SwapHash: swapHash[:],
|
||||
PaymentAddress: paymentAddr[:],
|
||||
Nonce: nonce,
|
||||
SweepTxPsbt: sweepTxPsbt,
|
||||
PrevoutInfo: prevOutInfo,
|
||||
}
|
||||
|
||||
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
|
||||
defer rpcCancel()
|
||||
|
||||
res, err := s.server.MuSig2SignSweep(rpcCtx, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return res.Nonce, res.PartialSignature, nil
|
||||
}
|
||||
|
||||
// PushKey sends the client's HTLC internal key associated with the swap to
|
||||
// the server.
|
||||
func (s *grpcSwapServerClient) PushKey(ctx context.Context,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue