Skip to content

Commit 4b0bcd5

Browse files
committed
fee testing WIP
1 parent 9939102 commit 4b0bcd5

File tree

7 files changed

+297
-30
lines changed

7 files changed

+297
-30
lines changed

itest/fee_estimation_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package itest
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/btcsuite/btcd/txscript"
8+
"github.com/btcsuite/btcd/wire"
9+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
10+
"github.com/lightningnetwork/lnd/lnrpc"
11+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// SetAnchorUTXO sets the wallet state for the sending node to one UTXO of a
16+
// specific value.
17+
func SetAnchorUTXO(t *harnessTest, amount int64) {
18+
// Burn funds that were sent to the first node.
19+
minerAddr := t.lndHarness.Miner.NewMinerAddress()
20+
primaryWallet := t.lndHarness.Alice
21+
primaryWallet.RPC.SendCoins(&lnrpc.SendCoinsRequest{
22+
Addr: minerAddr.EncodeAddress(),
23+
SendAll: true,
24+
})
25+
26+
t.lndHarness.MineBlocksAndAssertNumTxes(1, 1)
27+
28+
// Request a specific number of sats.
29+
aliceAddrResp := primaryWallet.RPC.NewAddress(&lnrpc.NewAddressRequest{
30+
Type: lnrpc.AddressType_TAPROOT_PUBKEY,
31+
})
32+
aliceAddr, err := btcutil.DecodeAddress(
33+
aliceAddrResp.Address, t.lndHarness.Miner.ActiveNet,
34+
)
35+
require.NoError(t.t, err)
36+
37+
aliceAddrScript, err := txscript.PayToAddrScript(aliceAddr)
38+
require.NoError(t.t, err)
39+
40+
output := &wire.TxOut{
41+
PkScript: aliceAddrScript,
42+
Value: amount,
43+
}
44+
t.lndHarness.Miner.SendOutput(output, 7500)
45+
46+
t.lndHarness.MineBlocksAndAssertNumTxes(6, 1)
47+
t.lndHarness.WaitForBlockchainSync(primaryWallet)
48+
}
49+
50+
// testBasicSendUnidirectional tests that we can properly send assets back and
51+
// forth between nodes.
52+
func testMintFeeEstimation(t *harnessTest) {
53+
var (
54+
mintingConfTarget = uint32(6)
55+
invalidAnchorAmt = int64(1000)
56+
invalidFeeRate = uint32(200)
57+
/*
58+
smallAnchorAmt = int64(5000)
59+
mediumAnchorAmt = int64(10000)
60+
largerAnchorAmt = int64(100000)
61+
lowFeeRate = uint32(1000)
62+
highFeeRate = uint32(10000)
63+
*/
64+
)
65+
66+
// Reset the on-chain funds of the first node.
67+
SetAnchorUTXO(t, invalidAnchorAmt)
68+
69+
/*
70+
Test cases:
71+
-Anchor amount is too small
72+
-Manual fee rate below floor (correct estimator)
73+
-Estimator fee rate below floor
74+
75+
///
76+
77+
-Estimator fee rate too large for anchor
78+
-Manual fee rate too large for anchor
79+
80+
Positive cases:
81+
-1, 5, 10, 50 sat/vB (convert to sat/kw, is it always scale 4?)
82+
-Both estimator and manual combos (test override)
83+
*/
84+
85+
// Minting with insufficient funds should fail inside the caretaker.
86+
// TODO(jhb): Should we catch this earlier?
87+
batchResp, err := MintAndFinalizeNoContext(
88+
t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets,
89+
)
90+
require.Nil(t.t, batchResp)
91+
require.ErrorContains(t.t, err, "insufficient funds available")
92+
93+
// Minting should also fail, at the RPC layer, if the manual fee rate
94+
// is too low.
95+
batchResp, err = MintAndFinalizeNoContext(
96+
t.t, t.lndHarness.Miner.Client, t.tapd, issuableAssets,
97+
WithFeeRate(invalidFeeRate),
98+
)
99+
require.Nil(t.t, batchResp)
100+
require.ErrorContains(t.t, err, "below floor of 253")
101+
102+
// An invalid fee estimate should return the floor fee rate, which is
103+
// still too low for this anchor amount.
104+
ctxb := context.Background()
105+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
106+
defer cancel()
107+
108+
t.lndHarness.SetFeeEstimateWithConf(
109+
chainfee.SatPerKWeight(invalidFeeRate), mintingConfTarget,
110+
)
111+
batchResp, err = t.tapd.FinalizeBatch(
112+
ctxt, &mintrpc.FinalizeBatchRequest{},
113+
)
114+
require.Nil(t.t, batchResp)
115+
require.ErrorContains(t.t, err, "insufficient funds available")
116+
117+
// ???
118+
// Reset node balance to not crash later itests
119+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77
)
88

99
var testCases = []*testCase{
10+
{
11+
name: "minting fee estimation",
12+
test: testMintFeeEstimation,
13+
},
1014
{
1115
name: "mint assets",
1216
test: testMintAssets,

itest/utils.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ type MintOption func(*MintOptions)
152152

153153
type MintOptions struct {
154154
mintingTimeout time.Duration
155+
feeRate uint32
155156
}
156157

157158
func DefaultMintOptions() *MintOptions {
@@ -166,12 +167,17 @@ func WithMintingTimeout(timeout time.Duration) MintOption {
166167
}
167168
}
168169

169-
// MintAssetUnconfirmed is a helper function that mints a batch of assets and
170-
// waits until the minting transaction is in the mempool but does not mine a
171-
// block.
172-
func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
170+
func WithFeeRate(feeRate uint32) MintOption {
171+
return func(options *MintOptions) {
172+
options.feeRate = feeRate
173+
}
174+
}
175+
176+
// MintAndFinalizeNoContext is a helper function that submits a batch of mint
177+
// requests and finalizes the batch. This function spawns its own context.
178+
func MintAndFinalizeNoContext(t *testing.T, minerClient *rpcclient.Client,
173179
tapClient TapdClient, assetRequests []*mintrpc.MintAssetRequest,
174-
opts ...MintOption) (chainhash.Hash, []byte) {
180+
opts ...MintOption) (*mintrpc.FinalizeBatchResponse, error) {
175181

176182
options := DefaultMintOptions()
177183
for _, opt := range opts {
@@ -182,6 +188,18 @@ func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
182188
ctxt, cancel := context.WithTimeout(ctxb, options.mintingTimeout)
183189
defer cancel()
184190

191+
return MintAndFinalize(
192+
t, ctxt, minerClient, tapClient, assetRequests, options,
193+
)
194+
}
195+
196+
// MintAndFinalize is a helper function that submits a batch of mint requests
197+
// and finalizes the batch.
198+
func MintAndFinalize(t *testing.T, ctxt context.Context,
199+
minerClient *rpcclient.Client, tapClient TapdClient,
200+
assetRequests []*mintrpc.MintAssetRequest,
201+
options *MintOptions) (*mintrpc.FinalizeBatchResponse, error) {
202+
185203
// Mint all the assets in the same batch.
186204
for idx, assetRequest := range assetRequests {
187205
assetResp, err := tapClient.MintAsset(ctxt, assetRequest)
@@ -191,8 +209,29 @@ func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
191209
}
192210

193211
// Instruct the daemon to finalize the batch.
194-
batchResp, err := tapClient.FinalizeBatch(
195-
ctxt, &mintrpc.FinalizeBatchRequest{},
212+
return tapClient.FinalizeBatch(ctxt, &mintrpc.FinalizeBatchRequest{
213+
FeeRate: options.feeRate,
214+
})
215+
}
216+
217+
// MintAssetUnconfirmed is a helper function that mints a batch of assets and
218+
// waits until the minting transaction is in the mempool but does not mine a
219+
// block.
220+
func MintAssetUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
221+
tapClient TapdClient, assetRequests []*mintrpc.MintAssetRequest,
222+
opts ...MintOption) (chainhash.Hash, []byte) {
223+
224+
options := DefaultMintOptions()
225+
for _, opt := range opts {
226+
opt(options)
227+
}
228+
229+
ctxb := context.Background()
230+
ctxt, cancel := context.WithTimeout(ctxb, options.mintingTimeout)
231+
defer cancel()
232+
233+
batchResp, err := MintAndFinalize(
234+
t, ctxt, minerClient, tapClient, assetRequests, options,
196235
)
197236
require.NoError(t, err)
198237
require.NotEmpty(t, batchResp.Batch)

tapfreighter/wallet.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,7 @@ func addAnchorPsbtInputs(btcPkt *psbt.Packet, vPkt *tappsbt.VPacket,
16561656
// Now that we've added an extra input, we'll want to re-calculate the
16571657
// total weight of the transaction, so we can ensure we're paying
16581658
// enough in fees.
1659+
// TODO(jhb): Replace with util
16591660
var (
16601661
weightEstimator input.TxWeightEstimator
16611662
inputAmt, outputAmt int64

tapgarden/caretaker.go

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/btcsuite/btcd/btcec/v2"
1313
"github.com/btcsuite/btcd/btcutil"
1414
"github.com/btcsuite/btcd/btcutil/psbt"
15+
"github.com/btcsuite/btcd/chaincfg"
1516
"github.com/btcsuite/btcd/chaincfg/chainhash"
1617
"github.com/btcsuite/btcd/wire"
1718
"github.com/davecgh/go-spew/spew"
@@ -389,24 +390,21 @@ func (b *BatchCaretaker) fundGenesisPsbt(ctx context.Context) (*FundedPsbt, erro
389390
log.Tracef("PSBT: %v", spew.Sdump(genesisPkt))
390391

391392
// If a fee rate was manually assigned for this batch, use that instead
392-
// of a fee rate estimate.
393-
var feeRate chainfee.SatPerKWeight
394-
switch {
395-
case b.cfg.BatchFeeRate != nil:
396-
feeRate = *b.cfg.BatchFeeRate
397-
log.Infof("BatchCaretaker(%x): using manual fee rate")
398-
399-
default:
400-
feeRate, err = b.cfg.ChainBridge.EstimateFee(
401-
ctx, GenesisConfTarget,
402-
)
403-
if err != nil {
404-
return nil, fmt.Errorf("unable to estimate fee: %w", err)
405-
}
393+
// of a fee rate estimate. Always log the estimated fee rate.
394+
feeRate, err := b.cfg.ChainBridge.EstimateFee(ctx, GenesisConfTarget)
395+
if err != nil {
396+
return nil, fmt.Errorf("unable to estimate fee: %w", err)
406397
}
407398

408-
log.Infof("BatchCaretaker(%x): using fee rate: %v",
409-
feeRate.FeePerKVByte().String())
399+
log.Infof("BatchCaretaker(%x): estimated fee rate: %s, %d sat/vB",
400+
b.batchKey[:], feeRate.String(),
401+
feeRate.FeePerKVByte().FeeForVSize(1))
402+
403+
if b.cfg.BatchFeeRate != nil {
404+
feeRate = *b.cfg.BatchFeeRate
405+
log.Infof("BatchCaretaker(%x): using manual fee rate: %s",
406+
b.batchKey[:], feeRate.String())
407+
}
410408

411409
fundedGenesisPkt, err := b.cfg.Wallet.FundPsbt(
412410
ctx, genesisPkt, 1, feeRate,
@@ -415,7 +413,22 @@ func (b *BatchCaretaker) fundGenesisPsbt(ctx context.Context) (*FundedPsbt, erro
415413
return nil, fmt.Errorf("unable to fund psbt: %w", err)
416414
}
417415

416+
// TODO(jhb): Thread actual chain params into caretaker?
417+
genesisPktFeeCustom, genesisPktWeight, err := tapscript.EstimateWeight(
418+
fundedGenesisPkt.Pkt, &chaincfg.MainNetParams,
419+
)
420+
if err != nil {
421+
return nil, err
422+
}
423+
424+
genesisPktFee, err := GetTxFee(fundedGenesisPkt.Pkt)
425+
if err != nil {
426+
return nil, err
427+
}
428+
418429
log.Infof("BatchCaretaker(%x): funded GenesisPacket", b.batchKey[:])
430+
log.Infof("GenesisPacket weight %d wu, absolute fee %d or %d sats",
431+
genesisPktWeight, genesisPktFeeCustom, genesisPktFee)
419432
log.Tracef("GenesisPacket: %v", spew.Sdump(fundedGenesisPkt))
420433

421434
return &fundedGenesisPkt, nil
@@ -738,8 +751,8 @@ func (b *BatchCaretaker) stateStep(currentState BatchState) (BatchState, error)
738751
}
739752
b.cfg.Batch.GenesisPacket.ChainFees = chainFees
740753

741-
log.Infof("BatchCaretaker(%x): GenesisPacket absolute fee: "+
742-
"%d sats", chainFees)
754+
log.Infof("BatchCaretaker(%x): GenesisPacket absolute fee "+
755+
"after finalization %d sats", b.batchKey[:], chainFees)
743756
log.Infof("BatchCaretaker(%x): GenesisPacket finalized",
744757
b.batchKey[:])
745758
log.Tracef("GenesisPacket: %v", spew.Sdump(signedPkt))

tapgarden/planter.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,20 @@ func (c *ChainPlanter) canCancelBatch() (*btcec.PublicKey, error) {
402402
return c.pendingBatch.BatchKey.PubKey, nil
403403
case 1:
404404
// TODO(jhb): Update once we support multiple batches.
405-
// If there is exactly one caretaker, our pending batch should
406-
// be empty. Otherwise, the batch to cancel is ambiguous.
405+
// If there is exactly one caretaker, we may still be able to
406+
// cancel, depending on the careatker state.
407407
if c.pendingBatch != nil {
408-
return nil, fmt.Errorf("multiple batches not supported")
408+
batchState := c.pendingBatch.State()
409+
switch batchState {
410+
case BatchStatePending, BatchStateFrozen,
411+
BatchStateCommitted:
412+
// We could still cancel, continue.
413+
414+
default:
415+
return nil, fmt.Errorf(
416+
"cannot cancel caretaker",
417+
)
418+
}
409419
}
410420

411421
batchKeys := maps.Keys(c.caretakers)
@@ -452,6 +462,8 @@ func (c *ChainPlanter) cancelMintingBatch(ctx context.Context,
452462

453463
return cancelResp.err
454464

465+
// What if caretaker was yeeted?
466+
455467
case <-c.Quit:
456468
return nil
457469
}
@@ -596,8 +608,8 @@ func (c *ChainPlanter) gardener() {
596608
}
597609

598610
batchKey := c.pendingBatch.BatchKey.PubKey
599-
log.Infof("Finalizing batch %x",
600-
batchKey.SerializeCompressed())
611+
batchKeySerial := asset.ToSerialized(batchKey)
612+
log.Infof("Finalizing batch %x", batchKeySerial)
601613

602614
feeRate, err :=
603615
typedParam[*chainfee.SatPerKWeight](req)
@@ -623,6 +635,13 @@ func (c *ChainPlanter) gardener() {
623635

624636
case err := <-caretaker.cfg.BroadcastErrChan:
625637
req.Error(err)
638+
// Unrecoverable error, stop caretaker
639+
// directly.
640+
// TODO(jhb): What state do we need to
641+
// clean up here?
642+
caretaker.Stop()
643+
delete(c.caretakers, batchKeySerial)
644+
c.pendingBatch = nil
626645
continue
627646

628647
case <-c.Quit:

0 commit comments

Comments
 (0)