-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathunroll_fee_input_test.go
More file actions
236 lines (206 loc) · 6.84 KB
/
Copy pathunroll_fee_input_test.go
File metadata and controls
236 lines (206 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
package waved
import (
"bytes"
"encoding/json"
"math"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/btcsuite/btclog/v2"
"github.com/btcsuite/btcwallet/waddrmgr"
btcwalletbase "github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightninglabs/wavelength/lwwallet"
"github.com/lightninglabs/wavelength/wallet"
fn "github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)
// TestLwUnrollWalletListUnspentExcludesImported is a regression test for
// wavelength#831. On the lwwallet (esplora) backend, CPFP fee selection
// must only surface wallet-derived, key-spendable outputs. Imported,
// watch-only taproot outputs (boarding outputs imported via
// ImportTaprootScript) have no BIP32 derivation and no spendable key, so the
// FinalizePsbt path cannot sign them and finalization fails with "PSBT is not
// finalizable", forcing a zero-fee parent broadcast that does not relay.
//
// The default-account ListUnspentWitness filter already excludes these
// imported outputs. The bug was an all-accounts fallback that re-admitted
// them; this test pins the corrected behaviour: the wallet's own derived
// output is offered as a fee input, while an imported boarding output is not.
func TestLwUnrollWalletListUnspentExcludesImported(t *testing.T) {
t.Parallel()
w := newFundedLwWallet(t)
ctx := t.Context()
uw := &lwUnrollWallet{Wallet: w}
// Import a watch-only boarding output and fund it as the wallet's only
// confirmed UTXO. This is the field scenario: the default account is
// empty (the boarding output lives in the imported account), so the
// regression was an all-accounts fallback that surfaced this
// unspendable output as a CPFP fee input.
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
importedAddr, err := w.BoardingBackend().ImportTaprootScript(
ctx, &waddrmgr.Tapscript{
Type: waddrmgr.TaprootFullKeyOnly,
FullOutputKey: privKey.PubKey(),
},
)
require.NoError(t, err)
importedScript, err := txscript.PayToAddrScript(importedAddr)
require.NoError(t, err)
importedOp := fundConfirmedUTXO(t, w, importedScript)
// Sanity check the harness: an all-accounts enumeration must see the
// imported output, otherwise the exclusion assertion below would be
// vacuous.
allUtxos, err := w.BtcWallet.ListUnspentWitness(0, math.MaxInt32, "")
require.NoError(t, err)
require.True(
t, containsOutpoint(allUtxos, importedOp),
"harness must fund the imported boarding output",
)
// With only the imported boarding output present, fee selection must
// return nothing rather than offering the unspendable output.
feeInputs, err := uw.ListUnspent(ctx, 0, math.MaxInt32)
require.NoError(t, err)
require.False(
t, containsFeeInput(feeInputs, importedOp),
"imported boarding output must not be a fee input",
)
require.Empty(t, feeInputs, "no spendable fee inputs available")
// A wallet-derived taproot output (the kind lwwallet hands back for
// change and sweep outputs) must, by contrast, be offered as a fee
// input.
derivedAddr, err := w.NewAddress(ctx)
require.NoError(t, err)
derivedScript, err := txscript.PayToAddrScript(derivedAddr)
require.NoError(t, err)
derivedOp := fundConfirmedUTXO(t, w, derivedScript)
feeInputs, err = uw.ListUnspent(ctx, 0, math.MaxInt32)
require.NoError(t, err)
require.True(
t, containsFeeInput(feeInputs, derivedOp),
"derived output must be a valid fee input",
)
require.False(
t, containsFeeInput(feeInputs, importedOp),
"imported boarding output must remain excluded",
)
}
// newFundedLwWallet builds and starts a real lwwallet backed by a mock
// Esplora server pinned at the regtest genesis block.
func newFundedLwWallet(t *testing.T) *lwwallet.Wallet {
t.Helper()
var seed [32]byte
for i := range seed {
seed[i] = byte(i + 1)
}
tipHash := chaincfg.RegressionNetParams.GenesisHash.String()
handler := func(rw http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/blocks/tip/height":
_, err := rw.Write([]byte("0"))
require.NoError(t, err)
case "/block-height/0":
_, err := rw.Write([]byte(tipHash))
require.NoError(t, err)
case "/block/" + tipHash:
err := json.NewEncoder(rw).Encode(map[string]any{
"id": tipHash,
"height": 0,
"timestamp": 1,
})
require.NoError(t, err)
default:
http.NotFound(rw, r)
}
}
esplora := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(esplora.Close)
w, err := lwwallet.New(lwwallet.Config{
Seed: seed[:],
WalletPassword: []byte("test-password"),
EsploraURL: esplora.URL,
ChainParams: &chaincfg.RegressionNetParams,
PollInterval: time.Hour,
RecoveryWindow: 10,
DBDir: t.TempDir(),
Log: fn.None[btclog.Logger](),
})
require.NoError(t, err)
require.NoError(t, w.Start())
t.Cleanup(w.Stop)
return w
}
// fundConfirmedUTXO records a confirmed credit for the given pkScript directly
// in btcwallet's transaction store and returns its outpoint. This mirrors
// btcwallet's own addUtxo test helper: it inserts a transaction paying the
// script and marks its sole output as a credit at the genesis block.
func fundConfirmedUTXO(t *testing.T, w *lwwallet.Wallet,
pkScript []byte) wire.OutPoint {
t.Helper()
const amount = 1_000_000
tx := &wire.MsgTx{
TxIn: []*wire.TxIn{
{},
},
TxOut: []*wire.TxOut{
wire.NewTxOut(amount, pkScript),
},
}
internal, ok := w.BtcWallet.InternalWallet().(*btcwalletbase.Wallet)
require.True(t, ok, "unexpected internal wallet type")
var buf bytes.Buffer
require.NoError(t, tx.Serialize(&buf))
rec, err := wtxmgr.NewTxRecord(buf.Bytes(), time.Now())
require.NoError(t, err)
block := &wtxmgr.BlockMeta{
Block: wtxmgr.Block{
Hash: *chaincfg.RegressionNetParams.GenesisHash,
Height: 0,
},
Time: time.Unix(1, 0),
}
require.NoError(
t,
walletdb.Update(
internal.Database(),
func(dbtx walletdb.ReadWriteTx) error {
ns := dbtx.ReadWriteBucket([]byte("wtxmgr"))
if err := internal.TxStore.InsertTx(
ns, rec, block,
); err != nil {
return err
}
return internal.TxStore.AddCredit(
ns, rec, block, 0, false,
)
},
),
)
return wire.OutPoint{Hash: tx.TxHash(), Index: 0}
}
// containsOutpoint reports whether the given lnwallet UTXOs include op.
func containsOutpoint(utxos []*lnwallet.Utxo, op wire.OutPoint) bool {
for _, u := range utxos {
if u.OutPoint == op {
return true
}
}
return false
}
// containsFeeInput reports whether the given fee inputs include op.
func containsFeeInput(utxos []*wallet.Utxo, op wire.OutPoint) bool {
for _, u := range utxos {
if u.Outpoint == op {
return true
}
}
return false
}