loop/sweepbatcher/presigned_test.go

1906 lines
39 KiB
Go
Raw Normal View History

package sweepbatcher
import (
"context"
"fmt"
"sync"
"testing"
"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/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
// TestOrderedSweeps checks that methods batch.getOrderedSweeps and
// batch.getSweepsGroups works properly.
func TestOrderedSweeps(t *testing.T) {
// Prepare the necessary data for test cases.
op1 := wire.OutPoint{Hash: chainhash.Hash{1, 1, 1}, Index: 1}
op2 := wire.OutPoint{Hash: chainhash.Hash{2, 2, 2}, Index: 2}
op3 := wire.OutPoint{Hash: chainhash.Hash{3, 3, 3}, Index: 3}
op4 := wire.OutPoint{Hash: chainhash.Hash{4, 4, 4}, Index: 4}
op5 := wire.OutPoint{Hash: chainhash.Hash{5, 5, 5}, Index: 5}
op6 := wire.OutPoint{Hash: chainhash.Hash{6, 6, 6}, Index: 6}
swapHash1 := lntypes.Hash{1, 1}
swapHash2 := lntypes.Hash{2, 2}
swapHash3 := lntypes.Hash{3, 3}
ctx := context.Background()
cases := []struct {
name string
sweeps []sweep
skippedTxns map[chainhash.Hash]struct{}
// Testing errors.
skipStore bool
reverseStore bool
replaceSweeps map[wire.OutPoint]sweep
wantGroups [][]sweep
wantErr1 string
wantErr2 string
}{
{
name: "no sweeps",
sweeps: []sweep{},
wantGroups: [][]sweep{},
},
{
name: "one sweep",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
},
wantGroups: [][]sweep{
{
{
outpoint: op1,
swapHash: swapHash1,
},
},
},
},
{
name: "one sweep, skipped",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
},
skippedTxns: map[chainhash.Hash]struct{}{
op1.Hash: {},
},
wantGroups: [][]sweep{},
},
{
name: "two sweeps, one swap",
sweeps: []sweep{
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op1,
swapHash: swapHash1,
},
},
wantGroups: [][]sweep{
{
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op1,
swapHash: swapHash1,
},
},
},
},
{
name: "two sweeps, one swap, one skipped",
sweeps: []sweep{
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op1,
swapHash: swapHash1,
},
},
skippedTxns: map[chainhash.Hash]struct{}{
op1.Hash: {},
},
wantGroups: [][]sweep{
{
{
outpoint: op2,
swapHash: swapHash1,
},
},
},
},
{
name: "two sweeps, two swap",
sweeps: []sweep{
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op1,
swapHash: swapHash2,
},
},
wantGroups: [][]sweep{
{
{
outpoint: op2,
swapHash: swapHash1,
},
},
{
{
outpoint: op1,
swapHash: swapHash2,
},
},
},
},
{
name: "many sweeps and swaps",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op3,
swapHash: swapHash1,
},
{
outpoint: op4,
swapHash: swapHash2,
},
{
outpoint: op5,
swapHash: swapHash3,
},
{
outpoint: op6,
swapHash: swapHash3,
},
},
wantGroups: [][]sweep{
{
{
outpoint: op1,
swapHash: swapHash1,
},
{
outpoint: op2,
swapHash: swapHash1,
},
{
outpoint: op3,
swapHash: swapHash1,
},
},
{
{
outpoint: op4,
swapHash: swapHash2,
},
},
{
{
outpoint: op5,
swapHash: swapHash3,
},
{
outpoint: op6,
swapHash: swapHash3,
},
},
},
},
{
name: "error: sweeps not stored in DB",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
},
skipStore: true,
wantErr1: "returned 0 sweeps, len(b.sweeps) is 1",
},
{
name: "error: wrong order in DB",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
{
outpoint: op2,
swapHash: swapHash2,
},
},
reverseStore: true,
},
{
name: "error: extra sweep in DB",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
{
outpoint: op2,
swapHash: swapHash2,
},
},
replaceSweeps: map[wire.OutPoint]sweep{
op2: {
outpoint: op3,
swapHash: swapHash3,
},
},
wantErr1: "returned unknown sweep",
},
{
name: "error: swaps interleaved",
sweeps: []sweep{
{
outpoint: op1,
swapHash: swapHash1,
},
{
outpoint: op2,
swapHash: swapHash2,
},
{
outpoint: op3,
swapHash: swapHash1,
},
},
wantErr2: "3 groups of sweeps and 2 distinct swaps",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Create a map of sweeps.
m := make(map[wire.OutPoint]sweep, len(tc.sweeps))
for _, s := range tc.sweeps {
require.NotContains(t, m, s.outpoint)
m[s.outpoint] = s
}
// Create a batch.
b := &batch{
sweeps: m,
store: NewStoreMock(),
cfg: &batchConfig{
skippedTxns: tc.skippedTxns,
},
}
// Store the sweeps in mock store.
switch {
// Don't create DB sweeps at all.
case tc.skipStore:
// Reverse the order of DB sweeps to ID incorrect IDs.
case tc.reverseStore:
for i := len(tc.sweeps) - 1; i >= 0; i-- {
s := tc.sweeps[i]
const completed = false
err := b.persistSweep(ctx, s, completed)
require.NoError(t, err)
}
// Working DB sweeps.
default:
for _, s := range tc.sweeps {
const completed = false
err := b.persistSweep(ctx, s, completed)
require.NoError(t, err)
}
}
// Replace some sweeps to test an error.
for removed, added := range tc.replaceSweeps {
require.Contains(t, m, removed)
delete(m, removed)
require.NotContains(t, m, added.outpoint)
m[added.outpoint] = added
}
// Remove skipped sweeps from the batch to make it
// match with what is read from DB after filtering.
for op := range m {
if _, has := tc.skippedTxns[op.Hash]; has {
delete(m, op)
}
}
// Now run the tested functions.
orderedSweeps, err := b.getOrderedSweeps(ctx)
if tc.wantErr1 != "" {
require.ErrorContains(t, err, tc.wantErr1)
return
}
require.NoError(t, err)
if tc.reverseStore {
require.NotEqual(t, tc.sweeps, orderedSweeps)
return
}
// The wanted list of sweeps matches the input order.
notSkipped := make([]sweep, 0, len(tc.sweeps))
for _, s := range tc.sweeps {
_, has := tc.skippedTxns[s.outpoint.Hash]
if has {
continue
}
notSkipped = append(notSkipped, s)
}
require.Equal(t, notSkipped, orderedSweeps)
groups, err := b.getSweepsGroups(ctx)
if tc.wantErr2 != "" {
require.ErrorContains(t, err, tc.wantErr2)
return
}
require.NoError(t, err)
require.Equal(t, tc.wantGroups, groups)
})
}
}
// mockPresignedTxChecker is an implementation of presignedTxChecker used in
// TestEnsurePresigned.
type mockPresignedTxChecker struct {
// primarySweepID is the value the mock matches this argument against.
primarySweepID wire.OutPoint
// destPkScript is the value returned by mock.DestPkScript.
destPkScript []byte
// signTxCalls is the number of SignTx calls.
signTxCalls int
// recordedInputAmt is the saved value of inputAmt argument of SignTx.
recordedInputAmt btcutil.Amount
// recordedMinRelayFee is the saved value of minRelayFee argument of
// SignTx.
recordedMinRelayFee chainfee.SatPerKWeight
// recordedFeeRate is the saved value of feeRate argument of SignTx.
recordedFeeRate chainfee.SatPerKWeight
// recordedLoadOnly is the saved value of loadOnly argument of SignTx.
recordedLoadOnly bool
// destPkScriptErr is the error returned by DestPkScript (if any).
destPkScriptErr error
// signedTxErr is the error returned by SignTx (if any).
signedTxErr error
}
// DestPkScript returns destination pkScript used by the sweep batch.
func (m *mockPresignedTxChecker) DestPkScript(ctx context.Context,
primarySweepID wire.OutPoint) ([]byte, error) {
if primarySweepID != m.primarySweepID {
return nil, fmt.Errorf("primarySweepID mismatch")
}
if m.destPkScriptErr != nil {
return nil, m.destPkScriptErr
}
return m.destPkScript, nil
}
// SignTx records all its argumentd and returned a "presigned" tx.
func (m *mockPresignedTxChecker) SignTx(ctx context.Context,
primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
minRelayFee, feeRate chainfee.SatPerKWeight,
loadOnly bool) (*wire.MsgTx, error) {
m.signTxCalls++
if primarySweepID != m.primarySweepID {
return nil, fmt.Errorf("primarySweepID mismatch")
}
m.recordedInputAmt = inputAmt
m.recordedMinRelayFee = minRelayFee
m.recordedFeeRate = feeRate
m.recordedLoadOnly = loadOnly
if m.signedTxErr != nil {
return nil, m.signedTxErr
}
// Pretend that we have a presigned transaction.
tx = tx.Copy()
for i := range tx.TxIn {
tx.TxIn[i].Witness = wire.TxWitness{
make([]byte, 64),
}
}
return tx, nil
}
// TestEnsurePresigned checks that function ensurePresigned works correctly.
func TestEnsurePresigned(t *testing.T) {
// Prepare the necessary data for test cases.
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2, 2},
Index: 2,
}
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
ctx := context.Background()
cases := []struct {
name string
primarySweepID wire.OutPoint
sweeps []*sweep
destPkScript []byte
minRelayFeeRate chainfee.SatPerKWeight
wantInputAmt btcutil.Amount
destPkScriptErr error
signedTxErr error
}{
{
name: "one input",
primarySweepID: op1,
sweeps: []*sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destPkScript: batchPkScript,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantInputAmt: 1_000_000,
},
{
name: "one input, higher minRelayFeeRate",
primarySweepID: op1,
sweeps: []*sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destPkScript: batchPkScript,
minRelayFeeRate: 1000,
wantInputAmt: 1_000_000,
},
{
name: "two inputs",
primarySweepID: op1,
sweeps: []*sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1000,
},
},
destPkScript: batchPkScript,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantInputAmt: 3_000_000,
},
{
name: "error: DestPkScript fails",
primarySweepID: op1,
sweeps: []*sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
minRelayFeeRate: chainfee.FeePerKwFloor,
destPkScriptErr: fmt.Errorf("test DestPkScript error"),
},
{
name: "error: SignTx fails",
primarySweepID: op1,
sweeps: []*sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destPkScript: batchPkScript,
minRelayFeeRate: chainfee.FeePerKwFloor,
signedTxErr: fmt.Errorf("test SignTx error"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := &mockPresignedTxChecker{
primarySweepID: tc.primarySweepID,
destPkScript: tc.destPkScript,
destPkScriptErr: tc.destPkScriptErr,
signedTxErr: tc.signedTxErr,
}
err := ensurePresigned(
ctx, tc.sweeps, c, tc.minRelayFeeRate,
&chaincfg.RegressionNetParams,
)
switch {
case tc.destPkScriptErr != nil:
require.ErrorIs(t, err, tc.destPkScriptErr)
case tc.signedTxErr != nil:
require.ErrorIs(t, err, tc.signedTxErr)
default:
require.NoError(t, err)
require.Equal(t, 1, c.signTxCalls)
require.Equal(
t, tc.wantInputAmt, c.recordedInputAmt,
)
require.Equal(
t, tc.minRelayFeeRate,
c.recordedMinRelayFee,
)
require.Equal(
t, tc.minRelayFeeRate,
c.recordedFeeRate,
)
require.True(t, c.recordedLoadOnly)
}
})
}
}
// hasInput returns if the transaction spends the UTXO.
func hasInput(tx *wire.MsgTx, utxo wire.OutPoint) bool {
for _, txIn := range tx.TxIn {
if txIn.PreviousOutPoint == utxo {
return true
}
}
return false
}
// mockPresigner is an implementation of Presigner used in TestPresign.
type mockPresigner struct {
// outputs collects outputs of presigned transactions.
outputs []btcutil.Amount
// lockTimes collects LockTime's of presigned transactions.
lockTimes []uint32
// failAt is optional index of a call at which it fails, 1 based.
failAt int
// mu protects the state.
mu sync.Mutex
}
// SignTx memorizes the value of the output and fails if the number of
// calls previously made is failAt.
func (p *mockPresigner) SignTx(ctx context.Context,
primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
minRelayFee, feeRate chainfee.SatPerKWeight,
loadOnly bool) (*wire.MsgTx, error) {
p.mu.Lock()
defer p.mu.Unlock()
if ctx.Err() != nil {
return nil, ctx.Err()
}
if !hasInput(tx, primarySweepID) {
return nil, fmt.Errorf("primarySweepID %v not in tx",
primarySweepID)
}
if len(p.outputs)+1 == p.failAt {
return nil, fmt.Errorf("test error in SignTx")
}
p.outputs = append(p.outputs, btcutil.Amount(tx.TxOut[0].Value))
p.lockTimes = append(p.lockTimes, tx.LockTime)
return tx, nil
}
// TestPresign checks that function presign presigns correct set of transactions
// and handles edge cases properly.
func TestPresign(t *testing.T) {
// Prepare the necessary data for test cases.
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2, 2},
Index: 2,
}
ctx := context.Background()
cases := []struct {
name string
presigner presigner
primarySweepID wire.OutPoint
sweeps []sweep
destAddr btcutil.Address
nextBlockFeeRate chainfee.SatPerKWeight
minRelayFeeRate chainfee.SatPerKWeight
wantErr string
wantOutputs []btcutil.Amount
wantLockTimes []uint32
}{
{
name: "error: no presigner",
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "presigner is not installed",
},
{
name: "error: no sweeps",
primarySweepID: op1,
presigner: &mockPresigner{},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "there are no sweeps",
},
{
name: "error: no destAddr",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "unsupported address type <nil>",
},
{
name: "error: zero nextBlockFeeRate",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "nextBlockFeeRate is not set",
},
{
name: "error: zero minRelayFeeRate",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
wantErr: "minRelayFeeRate is not set",
},
{
name: "error: timeout is not set",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "timeout is invalid: 0",
},
{
name: "error: primary not set",
presigner: &mockPresigner{},
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1100,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "not in tx",
},
{
name: "error: primary not in tx",
presigner: &mockPresigner{},
primarySweepID: op2,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "not in tx",
},
{
name: "one sweep",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
999900, 999880, 999856, 999827, 999793, 999752,
999702, 999643, 999572, 999486, 999384, 999260,
999113, 998935, 998723, 998467, 998161, 997793,
997352, 996823, 996187, 995425, 994510, 993413,
992096, 990515, 988618, 986342, 983610, 980332,
976399, 971679, 966015, 959218, 951062, 941274,
929530, 915435, 898523, 878227, 853873, 824648,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
950, 950, 950, 950, 950, 950, 950, 950, 950,
950, 950, 950, 950,
},
},
{
name: "higher minRelayFeeRate, fewer txns",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: 10 * chainfee.FeePerKwFloor,
minRelayFeeRate: 10 * chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
998998, 998797, 998557, 998269, 997923, 997507,
997009, 996411, 995694, 994833, 993800, 992560,
991072, 989286, 987144, 984573, 981488, 977786,
973343, 968012, 961614, 953937, 944725, 933670,
920405, 904486, 885383, 862460, 834952,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950,
950, 950, 950, 950, 950, 950, 950, 950, 950,
950, 950, 950, 950, 950,
},
},
{
name: "two sweeps",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1100,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
2999841, 2999810, 2999773, 2999728, 2999673,
2999608, 2999530, 2999436, 2999323, 2999188,
2999026, 2998831, 2998598, 2998317, 2997981,
2997577, 2997093, 2996512, 2995814, 2994977,
2993973, 2992768, 2991322, 2989587, 2987505,
2985006, 2982007, 2978409, 2974091, 2968910,
2962691, 2955230, 2946276, 2935532, 2922639,
2907167, 2888600, 2866320, 2839584, 2807501,
2769001, 2722802,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
950, 950, 950, 950, 950, 950, 950, 950, 950,
950, 950, 950, 950,
},
},
{
name: "two sweeps, another primary",
presigner: &mockPresigner{},
primarySweepID: op2,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1100,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
2999841, 2999810, 2999773, 2999728, 2999673,
2999608, 2999530, 2999436, 2999323, 2999188,
2999026, 2998831, 2998598, 2998317, 2997981,
2997577, 2997093, 2996512, 2995814, 2994977,
2993973, 2992768, 2991322, 2989587, 2987505,
2985006, 2982007, 2978409, 2974091, 2968910,
2962691, 2955230, 2946276, 2935532, 2922639,
2907167, 2888600, 2866320, 2839584, 2807501,
2769001, 2722802,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
950, 950, 950, 950, 950, 950, 950, 950, 950,
950, 950, 950, 950,
},
},
{
name: "timeout < 50",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 40,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 40,
},
},
destAddr: destAddr,
nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
2999841, 2999810, 2999773, 2999728, 2999673,
2999608, 2999530, 2999436, 2999323, 2999188,
2999026, 2998831, 2998598, 2998317, 2997981,
2997577, 2997093, 2996512, 2995814, 2994977,
2993973, 2992768, 2991322, 2989587, 2987505,
2985006, 2982007, 2978409, 2974091, 2968910,
2962691, 2955230, 2946276, 2935532, 2922639,
2907167, 2888600, 2866320, 2839584, 2807501,
2769001, 2722802,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
},
},
{
name: "high current feerate => locktime later",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
2999841, 2999810, 2999773, 2999728, 2999673,
2999608, 2999530, 2999436, 2999323, 2999188,
2999026, 2998831, 2998598, 2998317, 2997981,
2997577, 2997093, 2996512, 2995814, 2994977,
2993973, 2992768, 2991322, 2989587, 2987505,
2985006, 2982007, 2978409, 2974091, 2968910,
2962691, 2955230, 2946276, 2935532, 2922639,
2907167, 2888600, 2866320, 2839584, 2807501,
2769001, 2722802,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 950, 950, 950, 950, 950, 950, 950,
},
},
{
name: "small amount => fewer steps until " +
"clamped",
presigner: &mockPresigner{},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantOutputs: []btcutil.Amount{
2841, 2810, 2773, 2728, 2673, 2608, 2530, 2436,
2400,
},
wantLockTimes: []uint32{
0, 0, 0, 0, 0, 0, 0, 0, 0,
},
},
{
name: "third signing fails",
presigner: &mockPresigner{
failAt: 3,
},
primarySweepID: op1,
sweeps: []sweep{
{
outpoint: op1,
value: 1_000,
timeout: 1000,
},
{
outpoint: op2,
value: 2_000,
timeout: 1000,
},
},
destAddr: destAddr,
nextBlockFeeRate: chainfee.FeePerKwFloor,
minRelayFeeRate: chainfee.FeePerKwFloor,
wantErr: "test error in SignTx",
},
}
type pair struct {
output btcutil.Amount
lockTime uint32
}
zip := func(outputs []btcutil.Amount, lockTimes []uint32) []pair {
require.Equal(t, len(outputs), len(lockTimes))
pairs := make([]pair, len(outputs))
for i, output := range outputs {
pairs[i] = pair{
output: output,
lockTime: lockTimes[i],
}
}
return pairs
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := presign(
ctx, tc.presigner, tc.destAddr,
tc.primarySweepID, tc.sweeps,
tc.nextBlockFeeRate, tc.minRelayFeeRate,
)
if tc.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
p := tc.presigner.(*mockPresigner)
require.ElementsMatch(
t,
zip(tc.wantOutputs, tc.wantLockTimes),
zip(p.outputs, p.lockTimes),
)
}
})
}
}
// TestCheckSignedTx tests that function CheckSignedTx checks all the criteria
// of PresignedHelper.SignTx correctly.
func TestCheckSignedTx(t *testing.T) {
// Prepare the necessary data for test cases.
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2, 2},
Index: 2,
}
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
cases := []struct {
name string
unsignedTx *wire.MsgTx
signedTx *wire.MsgTx
inputAmt btcutil.Amount
minRelayFee chainfee.SatPerKWeight
wantErr string
}{
{
name: "success",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "",
},
{
name: "unsigned input in signedTx",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "is not signed",
},
{
name: "bad locktime",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_001,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "locktime",
},
{
name: "bad version",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 3,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "version",
},
{
name: "missing input",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "is missing in signed tx",
},
{
name: "extra input",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "is new in signed tx",
},
{
name: "mismatch of sequence numbers",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 3,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "sequence mismatch",
},
{
name: "extra output in unsignedTx",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "unsigned tx has 2 outputs, signed tx " +
"has 1 outputs, should be equal",
},
{
name: "extra output in signedTx",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "unsigned tx has 1 outputs, signed tx " +
"has 2 outputs, should be equal",
},
{
name: "pkscript mismatch",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: []byte{0xaf, 0xfe}, // Just to make it different.
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "mismatch of output pkScript",
},
{
name: "value mismatch, first output",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 1_337_000, // Just to make it different.
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "",
},
{
name: "value mismatch, change output",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
},
{
PreviousOutPoint: op1,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
{
Value: 1_337_000,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2_493_300,
PkScript: batchPkScript,
},
{
Value: 1_338, // Just to make it different.
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "mismatch of output value",
},
{
name: "mismatch of output pk_script",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript[1:],
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 253,
wantErr: "mismatch of output pkScript",
},
{
name: "too low feerate in signedTx",
unsignedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
Sequence: 1,
},
{
PreviousOutPoint: op2,
Sequence: 2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 800_000,
},
signedTx: &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
Sequence: 2,
Witness: wire.TxWitness{
[]byte("test"),
},
},
{
PreviousOutPoint: op1,
Sequence: 1,
Witness: wire.TxWitness{
[]byte("test"),
},
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
LockTime: 799_999,
},
inputAmt: 3_000_000,
minRelayFee: 250_000,
wantErr: "is lower than minRelayFee",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := CheckSignedTx(
tc.unsignedTx, tc.signedTx, tc.inputAmt,
tc.minRelayFee,
)
if tc.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
})
}
}