mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/withdraw: use generated change addresses
Create a fresh static address for partial-withdrawal change. Keep all withdrawal outputs in the PSBT without separate signing metadata, while preserving full-withdrawal behavior.
This commit is contained in:
parent
dc42446bae
commit
1042f3bf19
3 changed files with 347 additions and 46 deletions
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
)
|
||||
|
|
@ -18,6 +19,10 @@ type AddressManager interface {
|
|||
// GetStaticAddress returns the deposit address for the given
|
||||
// client and server public keys.
|
||||
GetStaticAddress(ctx context.Context) (*script.StaticAddress, error)
|
||||
|
||||
// NewChangeAddress derives and persists a fresh static address from the
|
||||
// change key family for this operation's change output.
|
||||
NewChangeAddress(ctx context.Context) (*address.Parameters, error)
|
||||
}
|
||||
|
||||
type DepositManager interface {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
|
|
@ -19,6 +18,7 @@ import (
|
|||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
||||
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||
|
|
@ -280,8 +280,7 @@ func (m *Manager) recoverWithdrawals(ctx context.Context) error {
|
|||
}
|
||||
|
||||
err = m.handleWithdrawal(
|
||||
ctx, deposits, tx.TxHash(),
|
||||
tx.TxOut[0].PkScript,
|
||||
ctx, deposits, tx.TxHash(), tx.TxOut[0].PkScript,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -567,10 +566,28 @@ func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
|
|||
"input proofs: %w", err)
|
||||
}
|
||||
|
||||
_, changeAmount, err := CalculateWithdrawalTxValues(
|
||||
deposits, btcutil.Amount(selectedWithdrawalAmount), feeRate,
|
||||
withdrawalAddress, commitmentType,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error calculating funding tx "+
|
||||
"values: %w", err)
|
||||
}
|
||||
|
||||
var changeParams *address.Parameters
|
||||
if changeAmount > 0 {
|
||||
changeParams, err = m.cfg.AddressManager.NewChangeAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to create static "+
|
||||
"address change output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
withdrawalTx, unsignedPsbt, err := m.createWithdrawalTx(
|
||||
ctx, outpoints, deposits, prevOuts,
|
||||
outpoints, deposits, prevOuts,
|
||||
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
|
||||
feeRate, commitmentType,
|
||||
feeRate, commitmentType, changeParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -578,10 +595,10 @@ func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
|
|||
|
||||
// Request the server to sign the withdrawal transaction.
|
||||
//
|
||||
// The withdrawal and change amount are sent to the server with the
|
||||
// expectation that the server just signs the transaction, without
|
||||
// performing fee calculations and dust considerations. The client is
|
||||
// responsible for that.
|
||||
// All withdrawal outputs, including any change output, are encoded in
|
||||
// the PSBT. The server signs the transaction as constructed without
|
||||
// performing fee calculations or dust handling. The client is
|
||||
// responsible for both.
|
||||
// nolint:lll
|
||||
sigResp, err := m.cfg.StaticAddressServerClient.ServerPsbtWithdrawDeposits(
|
||||
ctx, &staticaddressrpc.ServerPsbtWithdrawRequest{
|
||||
|
|
@ -666,22 +683,56 @@ func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func withdrawalChangePkScript(tx *wire.MsgTx) []byte {
|
||||
if tx == nil || len(tx.TxOut) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.TxOut[1].PkScript
|
||||
}
|
||||
|
||||
// validateConfirmedWithdrawalInputs verifies that a confirmed withdrawal
|
||||
// transaction spends every deposit associated with the withdrawal. Withdrawal
|
||||
// monitoring intentionally watches only the first deposit so an RBF replacement
|
||||
// can be discovered without registering a new confirmation notification for
|
||||
// every replacement transaction. However, the spend notification also fires if
|
||||
// an unrelated transaction spends only that first deposit. Requiring the full
|
||||
// deposit set prevents such a partial spend from incorrectly transitioning all
|
||||
// deposits to Withdrawn while still permitting a replacement transaction with a
|
||||
// different transaction ID or additional inputs.
|
||||
func validateConfirmedWithdrawalInputs(tx *wire.MsgTx,
|
||||
deposits []*deposit.Deposit) error {
|
||||
|
||||
inputs := make(map[wire.OutPoint]struct{}, len(tx.TxIn))
|
||||
for _, txIn := range tx.TxIn {
|
||||
inputs[txIn.PreviousOutPoint] = struct{}{}
|
||||
}
|
||||
|
||||
for _, d := range deposits {
|
||||
if _, ok := inputs[d.OutPoint]; !ok {
|
||||
return fmt.Errorf("confirmed transaction %v does not spend "+
|
||||
"withdrawal deposit %v", tx.TxHash(), d.OutPoint)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleWithdrawal starts a goroutine that listens for the spent of the first
|
||||
// input of the withdrawal transaction.
|
||||
func (m *Manager) handleWithdrawal(ctx context.Context,
|
||||
deposits []*deposit.Deposit, txHash chainhash.Hash,
|
||||
withdrawalPkscript []byte) error {
|
||||
|
||||
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("error retrieving address params: %v", err)
|
||||
|
||||
return fmt.Errorf("withdrawal failed")
|
||||
}
|
||||
deposits []*deposit.Deposit, originalTxHash chainhash.Hash,
|
||||
withdrawalPkScript []byte) error {
|
||||
|
||||
d := deposits[0]
|
||||
if d.AddressParams == nil {
|
||||
return fmt.Errorf("missing static address parameters for %v",
|
||||
d.OutPoint)
|
||||
}
|
||||
depositPkScript := d.AddressParams.PkScript
|
||||
|
||||
spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn(
|
||||
ctx, &d.OutPoint, addrParams.PkScript,
|
||||
ctx, &d.OutPoint, depositPkScript,
|
||||
int32(d.GetConfirmationHeight()),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -692,13 +743,20 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
|
|||
select {
|
||||
case spentTx := <-spentChan:
|
||||
spendingHeight := uint32(spentTx.SpendingHeight)
|
||||
spenderTxHash := originalTxHash
|
||||
if spentTx.SpenderTxHash != nil {
|
||||
spenderTxHash = *spentTx.SpenderTxHash
|
||||
} else if spentTx.SpendingTx != nil {
|
||||
spenderTxHash = spentTx.SpendingTx.TxHash()
|
||||
}
|
||||
|
||||
// If the transaction received one confirmation, we
|
||||
// ensure re-org safety by waiting for some more
|
||||
// confirmations.
|
||||
confChan, confErrChan, err :=
|
||||
m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
||||
ctx, spentTx.SpenderTxHash,
|
||||
withdrawalPkscript, MinConfs,
|
||||
ctx, &spenderTxHash, withdrawalPkScript,
|
||||
MinConfs,
|
||||
int32(m.initiationHeight.Load()),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -712,6 +770,32 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
|
|||
|
||||
select {
|
||||
case tx := <-confChan:
|
||||
confirmedTx := spentTx.SpendingTx
|
||||
if tx != nil && tx.Tx != nil {
|
||||
confirmedTx = tx.Tx
|
||||
}
|
||||
if confirmedTx == nil {
|
||||
log.Errorf("Confirmed withdrawal %v "+
|
||||
"missing transaction",
|
||||
spenderTxHash)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Since the spend notification above only watches the
|
||||
// first deposit, verify that the confirmed spender is the
|
||||
// withdrawal (or one of its RBF replacements) before
|
||||
// transitioning the complete deposit group.
|
||||
err = validateConfirmedWithdrawalInputs(
|
||||
confirmedTx, deposits,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Ignoring incomplete withdrawal: %v",
|
||||
err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = m.cfg.DepositManager.TransitionDeposits(
|
||||
ctx, deposits, deposit.OnWithdrawn,
|
||||
deposit.Withdrawn,
|
||||
|
|
@ -725,13 +809,14 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
|
|||
// withdrawals to stop republishing it on block
|
||||
// arrivals.
|
||||
m.mu.Lock()
|
||||
delete(m.finalizedWithdrawalTxns, txHash)
|
||||
delete(m.finalizedWithdrawalTxns, originalTxHash)
|
||||
delete(m.finalizedWithdrawalTxns, spenderTxHash)
|
||||
m.mu.Unlock()
|
||||
|
||||
// Persist info about the finalized withdrawal.
|
||||
err = m.cfg.Store.UpdateWithdrawal(
|
||||
ctx, deposits, tx.Tx, spendingHeight,
|
||||
addrParams.PkScript,
|
||||
ctx, deposits, confirmedTx, spendingHeight,
|
||||
withdrawalChangePkScript(confirmedTx),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Error persisting "+
|
||||
|
|
@ -888,12 +973,13 @@ func (m *Manager) signMusig2Tx(ctx context.Context,
|
|||
return tx, nil
|
||||
}
|
||||
|
||||
func (m *Manager) createWithdrawalTx(ctx context.Context,
|
||||
func (m *Manager) createWithdrawalTx(
|
||||
outpoints []wire.OutPoint, deposits []*deposit.Deposit,
|
||||
prevOuts map[wire.OutPoint]*wire.TxOut,
|
||||
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
|
||||
feeRate chainfee.SatPerKWeight,
|
||||
commitmentType lnrpc.CommitmentType) (*wire.MsgTx, []byte, error) {
|
||||
commitmentType lnrpc.CommitmentType,
|
||||
changeParams *address.Parameters) (*wire.MsgTx, []byte, error) {
|
||||
|
||||
// First Create the tx.
|
||||
msgTx := wire.NewMsgTx(2)
|
||||
|
|
@ -940,30 +1026,14 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
})
|
||||
|
||||
if changeAmount > 0 {
|
||||
// Send change back to the same static address.
|
||||
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("error retrieving taproot address %v", err)
|
||||
|
||||
return nil, nil, fmt.Errorf("withdrawal failed")
|
||||
}
|
||||
|
||||
changeAddress, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(staticAddress.TaprootKey),
|
||||
m.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(changeAddress)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
if changeParams == nil {
|
||||
return nil, nil, fmt.Errorf("missing static address " +
|
||||
"change parameters")
|
||||
}
|
||||
|
||||
msgTx.AddTxOut(&wire.TxOut{
|
||||
Value: int64(changeAmount),
|
||||
PkScript: changeScript,
|
||||
PkScript: changeParams.PkScript,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,54 @@ package withdraw
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"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"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btclog/v2"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/staticaddr/address"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type withdrawalCleanupSigner struct {
|
||||
lndclient.SignerClient
|
||||
|
||||
cleaned [][32]byte
|
||||
cleanupCtxErr []error
|
||||
}
|
||||
|
||||
func (s *withdrawalCleanupSigner) MuSig2CreateSession(context.Context,
|
||||
input.MuSig2Version, *keychain.KeyLocator, [][]byte,
|
||||
...lndclient.MuSig2SessionOpts) (*input.MuSig2SessionInfo, error) {
|
||||
|
||||
return &input.MuSig2SessionInfo{SessionID: [32]byte{1}}, nil
|
||||
}
|
||||
|
||||
func (s *withdrawalCleanupSigner) MuSig2Cleanup(ctx context.Context,
|
||||
sessionID [32]byte) error {
|
||||
|
||||
s.cleaned = append(s.cleaned, sessionID)
|
||||
s.cleanupCtxErr = append(s.cleanupCtxErr, ctx.Err())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestNewManagerHeightValidation ensures the constructor rejects zero heights.
|
||||
func TestNewManagerHeightValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
|
@ -35,6 +65,202 @@ func TestNewManagerHeightValidation(t *testing.T) {
|
|||
require.NotNil(t, manager)
|
||||
}
|
||||
|
||||
func TestWithdrawalChangePkScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Nil(t, withdrawalChangePkScript(nil))
|
||||
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 1000,
|
||||
PkScript: []byte{0x01},
|
||||
})
|
||||
require.Nil(t, withdrawalChangePkScript(tx))
|
||||
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 500,
|
||||
PkScript: []byte{0x02},
|
||||
})
|
||||
require.Equal(t, []byte{0x02}, withdrawalChangePkScript(tx))
|
||||
}
|
||||
|
||||
// TestValidateConfirmedWithdrawalInputs verifies that a replacement
|
||||
// transaction must preserve the complete withdrawal deposit set. Additional
|
||||
// inputs are allowed because they do not change which deposits are withdrawn.
|
||||
func TestValidateConfirmedWithdrawalInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{Hash: chainhash.Hash{1}, Index: 1},
|
||||
}
|
||||
second := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{Hash: chainhash.Hash{2}, Index: 2},
|
||||
}
|
||||
deposits := []*deposit.Deposit{first, second}
|
||||
|
||||
partialSpend := wire.NewMsgTx(2)
|
||||
partialSpend.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: first.OutPoint,
|
||||
})
|
||||
err := validateConfirmedWithdrawalInputs(partialSpend, deposits)
|
||||
require.ErrorContains(t, err, second.OutPoint.String())
|
||||
|
||||
replacement := wire.NewMsgTx(2)
|
||||
replacement.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: first.OutPoint,
|
||||
})
|
||||
replacement.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: second.OutPoint,
|
||||
})
|
||||
replacement.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{3}, Index: 3,
|
||||
},
|
||||
})
|
||||
require.NoError(
|
||||
t, validateConfirmedWithdrawalInputs(replacement, deposits),
|
||||
)
|
||||
}
|
||||
|
||||
func TestCreateFinalizedWithdrawalTxCleansUpSessionsOnError(t *testing.T) {
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
serverKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
signer := &withdrawalCleanupSigner{}
|
||||
manager := &Manager{cfg: &ManagerConfig{Signer: signer}}
|
||||
deposits := []*deposit.Deposit{
|
||||
{
|
||||
OutPoint: wire.OutPoint{Index: 1},
|
||||
Value: 100_000,
|
||||
AddressParams: &address.Parameters{
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
Expiry: 144,
|
||||
PkScript: []byte{0x51},
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: 1,
|
||||
Index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
_, _, err = manager.CreateFinalizedWithdrawalTx(
|
||||
ctx, deposits, nil, 1_000, 0,
|
||||
lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
)
|
||||
require.ErrorContains(
|
||||
t, err, "either address or commitment type must be specified",
|
||||
)
|
||||
require.Equal(t, [][32]byte{{1}}, signer.cleaned)
|
||||
require.Equal(t, []error{nil}, signer.cleanupCtxErr)
|
||||
}
|
||||
|
||||
type withdrawalConfRegistration struct {
|
||||
txID *chainhash.Hash
|
||||
pkScript []byte
|
||||
numConfs int32
|
||||
heightHint int32
|
||||
}
|
||||
|
||||
type withdrawalTestNotifier struct {
|
||||
lndclient.ChainNotifierClient
|
||||
|
||||
spendChan chan *chainntnfs.SpendDetail
|
||||
spendErr chan error
|
||||
confChan chan *chainntnfs.TxConfirmation
|
||||
confErr chan error
|
||||
confReq chan withdrawalConfRegistration
|
||||
}
|
||||
|
||||
func newWithdrawalTestNotifier() *withdrawalTestNotifier {
|
||||
return &withdrawalTestNotifier{
|
||||
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
spendErr: make(chan error, 1),
|
||||
confChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
confErr: make(chan error, 1),
|
||||
confReq: make(chan withdrawalConfRegistration, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *withdrawalTestNotifier) RegisterSpendNtfn(context.Context,
|
||||
*wire.OutPoint, []byte, int32, ...lndclient.NotifierOption) (
|
||||
chan *chainntnfs.SpendDetail, chan error, error) {
|
||||
|
||||
return n.spendChan, n.spendErr, nil
|
||||
}
|
||||
|
||||
func (n *withdrawalTestNotifier) RegisterConfirmationsNtfn(_ context.Context,
|
||||
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32,
|
||||
_ ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation,
|
||||
chan error, error) {
|
||||
|
||||
n.confReq <- withdrawalConfRegistration{
|
||||
txID: txid,
|
||||
pkScript: pkScript,
|
||||
numConfs: numConfs,
|
||||
heightHint: heightHint,
|
||||
}
|
||||
|
||||
return n.confChan, n.confErr, nil
|
||||
}
|
||||
|
||||
func TestHandleWithdrawalFollowsReplacementTxid(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
UseLogger(btclog.Disabled)
|
||||
|
||||
notifier := newWithdrawalTestNotifier()
|
||||
manager, err := NewManager(&ManagerConfig{
|
||||
ChainNotifier: notifier,
|
||||
}, 123)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalTxHash := chainhash.Hash{1}
|
||||
replacementTxHash := chainhash.Hash{2}
|
||||
dep := &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{3},
|
||||
Index: 0,
|
||||
},
|
||||
ConfirmationHeight: 42,
|
||||
AddressParams: &address.Parameters{
|
||||
PkScript: []byte{0x51},
|
||||
},
|
||||
}
|
||||
manager.finalizedWithdrawalTxns[originalTxHash] = wire.NewMsgTx(2)
|
||||
withdrawalPkScript := []byte{0x51}
|
||||
|
||||
err = manager.handleWithdrawal(
|
||||
ctx, []*deposit.Deposit{dep}, originalTxHash,
|
||||
withdrawalPkScript,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
notifier.spendChan <- &chainntnfs.SpendDetail{
|
||||
SpenderTxHash: &replacementTxHash,
|
||||
SpendingTx: wire.NewMsgTx(2),
|
||||
SpendingHeight: 50,
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-notifier.confReq:
|
||||
require.NotNil(t, req.txID)
|
||||
require.Equal(t, replacementTxHash, *req.txID)
|
||||
require.Equal(t, withdrawalPkScript, req.pkScript)
|
||||
require.Equal(t, MinConfs, req.numConfs)
|
||||
require.EqualValues(t, 123, req.heightHint)
|
||||
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("confirmation registration not received: %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignMusig2Tx_MissingSigningInfo tests that signMusig2Tx should error
|
||||
// when sigInfo is missing an entry for one of the deposits.
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue