mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
1484 lines
39 KiB
Go
1484 lines
39 KiB
Go
package account
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
timeout = 500 * time.Millisecond
|
|
|
|
maxAccountValue = 2 * btcutil.SatoshiPerBitcoin
|
|
|
|
bestHeight = 100
|
|
)
|
|
|
|
var (
|
|
p2wsh, _ = hex.DecodeString("00208c2865c87ffd33fc5d698c7df9cf2d0fb39d93103c637a06dea32c848ebc3e1d")
|
|
p2tr, _ = hex.DecodeString("5120bb91443dd777945ef4422cffddec00d5feed2aca2562a902fbe8d2a258b337da")
|
|
p2wpkh, _ = hex.DecodeString("0014ccdeffed4f9c91d5bf45c34e4b8f03a5025ec062")
|
|
np2wpkh, _ = hex.DecodeString("a91458c11505b54582ab04e96d36908f85a8b689459787")
|
|
|
|
// defaultFeeExpr is a fee expression noting that the minimum fee rate
|
|
// should be used for a wallet output.
|
|
defaultFeeExpr = OutputWithFee{
|
|
FeeRate: chainfee.FeePerKwFloor,
|
|
}
|
|
)
|
|
|
|
type testCase struct {
|
|
name string
|
|
feeExpr FeeExpr
|
|
fee btcutil.Amount
|
|
version Version
|
|
newVersion Version
|
|
expectedErr string
|
|
action Action
|
|
isExpirySpend bool
|
|
|
|
// The following fields are only used by deposit tests.
|
|
fundedOutputAmount btcutil.Amount
|
|
fundingTxFee btcutil.Amount
|
|
utxo *lnwallet.Utxo
|
|
}
|
|
|
|
func runSubTests(t *testing.T, testCases []*testCase,
|
|
runTest func(t *testing.T, h *testHarness, tc *testCase)) {
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
success := t.Run(tc.name, func(tt *testing.T) {
|
|
tt.Parallel()
|
|
|
|
h := newTestHarness(tt)
|
|
h.start()
|
|
defer h.stop()
|
|
|
|
runTest(tt, h, tc)
|
|
})
|
|
if !success {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type testHarness struct {
|
|
t *testing.T
|
|
store *mockStore
|
|
notifier *mockChainNotifier
|
|
wallet *mockWallet
|
|
auctioneer *mockAuctioneer
|
|
manager Manager
|
|
}
|
|
|
|
func newTestHarness(t *testing.T) *testHarness {
|
|
store := newMockStore()
|
|
wallet := newMockWallet()
|
|
notifier := newMockChainNotifier()
|
|
auctioneer := newMockAuctioneer()
|
|
|
|
return &testHarness{
|
|
t: t,
|
|
store: store,
|
|
wallet: wallet,
|
|
notifier: notifier,
|
|
auctioneer: auctioneer,
|
|
manager: NewManager(&ManagerConfig{
|
|
Store: store,
|
|
Auctioneer: auctioneer,
|
|
Wallet: wallet,
|
|
Signer: wallet,
|
|
ChainNotifier: notifier,
|
|
TxSource: wallet,
|
|
TxFeeEstimator: wallet,
|
|
ChainParams: &chaincfg.TestNet3Params,
|
|
LndVersion: &verrpc.Version{
|
|
AppMajor: 0,
|
|
AppMinor: 15,
|
|
AppPatch: 1,
|
|
},
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (h *testHarness) start() {
|
|
require.NoError(h.t, h.manager.Start())
|
|
}
|
|
|
|
func (h *testHarness) stop() {
|
|
h.manager.Stop()
|
|
}
|
|
|
|
func (h *testHarness) assertAccountSubscribed(traderKey *btcec.PublicKey) {
|
|
h.t.Helper()
|
|
|
|
var rawTraderKey [33]byte
|
|
copy(rawTraderKey[:], traderKey.SerializeCompressed())
|
|
|
|
h.auctioneer.mu.Lock()
|
|
defer h.auctioneer.mu.Unlock()
|
|
|
|
_, ok := h.auctioneer.subscribed[rawTraderKey]
|
|
require.Truef(h.t, ok, "account %x not subscribed", rawTraderKey)
|
|
}
|
|
|
|
func (h *testHarness) assertAccountNotSubscribed(traderKey *btcec.PublicKey) {
|
|
h.t.Helper()
|
|
|
|
var rawTraderKey [33]byte
|
|
copy(rawTraderKey[:], traderKey.SerializeCompressed())
|
|
|
|
h.auctioneer.mu.Lock()
|
|
defer h.auctioneer.mu.Unlock()
|
|
|
|
_, ok := h.auctioneer.subscribed[rawTraderKey]
|
|
require.Falsef(h.t, ok, "account %x not subscribed", rawTraderKey)
|
|
}
|
|
|
|
func (h *testHarness) assertAccountExists(expected *Account) {
|
|
h.t.Helper()
|
|
|
|
err := wait.NoError(func() error {
|
|
found, err := h.store.Account(expected.TraderKey.PubKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !reflect.DeepEqual(found, expected) {
|
|
return fmt.Errorf("expected account: %v\ngot: %v",
|
|
spew.Sdump(expected), spew.Sdump(found))
|
|
}
|
|
|
|
return nil
|
|
}, 10*timeout)
|
|
require.NoError(h.t, err)
|
|
}
|
|
|
|
func (h *testHarness) openAccount(value btcutil.Amount, expiry uint32, // nolint:unparam
|
|
bestHeight uint32, version Version) *Account { // nolint:unparam
|
|
|
|
h.t.Helper()
|
|
|
|
// Create a new account. Its initial state should be StatePendingOpen.
|
|
ctx := context.Background()
|
|
account, err := h.manager.InitAccount(
|
|
ctx, value, version, chainfee.FeePerKwFloor, expiry, bestHeight,
|
|
)
|
|
require.NoError(h.t, err)
|
|
|
|
// The same account should be found in the store.
|
|
h.assertAccountExists(account)
|
|
|
|
// Since the account is still pending confirmation, it should not be
|
|
// subscribed to updates from the auctioneer yet.
|
|
h.assertAccountNotSubscribed(account.TraderKey.PubKey)
|
|
|
|
// Notify the confirmation of the account.
|
|
confHeight := bestHeight + 6
|
|
h.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
BlockHeight: confHeight,
|
|
}
|
|
|
|
// This should prompt the account to now be in a StateOpen state and
|
|
// the subscription for updates should now be realized.
|
|
account.State = StateOpen
|
|
account.HeightHint = confHeight
|
|
h.assertAccountExists(account)
|
|
h.assertAccountSubscribed(account.TraderKey.PubKey)
|
|
|
|
return account
|
|
}
|
|
|
|
func (h *testHarness) expireAccount(account *Account) {
|
|
h.t.Helper()
|
|
|
|
// Notify the height at which the account expires.
|
|
h.notifier.blockChan <- int32(account.Expiry)
|
|
|
|
// This should prompt the account to now be in a StateExpired state.
|
|
account.State = StateExpired
|
|
h.assertAccountExists(account)
|
|
}
|
|
|
|
func (h *testHarness) closeAccount(account *Account, feeExpr FeeExpr,
|
|
bestHeight uint32) {
|
|
|
|
h.t.Helper()
|
|
|
|
// Close the account with the auctioneer.
|
|
go func() {
|
|
_, err := h.manager.CloseAccount(
|
|
context.Background(), account.TraderKey.PubKey, feeExpr,
|
|
bestHeight,
|
|
)
|
|
require.NoError(h.t, err)
|
|
}()
|
|
|
|
// This should prompt the account's closing transaction to be broadcast
|
|
// and its state transitioned to StatePendingClosed.
|
|
closeTx := h.assertSpendTxBroadcast(
|
|
account, nil, nil, nil, account.Version,
|
|
)
|
|
|
|
account.Value = 0
|
|
account.State = StatePendingClosed
|
|
account.HeightHint = bestHeight
|
|
account.LatestTx = closeTx
|
|
h.assertAccountExists(account)
|
|
|
|
// Notify the transaction as a spend of the account.
|
|
spendHeight := bestHeight + 6
|
|
h.notifier.spendChan <- &chainntnfs.SpendDetail{
|
|
SpendingTx: closeTx,
|
|
SpendingHeight: int32(spendHeight),
|
|
}
|
|
|
|
// This should prompt the account to now be in a StateClosed state.
|
|
account.State = StateClosed
|
|
account.HeightHint = spendHeight
|
|
h.assertAccountExists(account)
|
|
}
|
|
|
|
func (h *testHarness) assertSpendTxBroadcast(accountBeforeSpend *Account,
|
|
externalInputs []*lnwallet.Utxo, externalOutputs []*wire.TxOut,
|
|
newValue *btcutil.Amount, newVersion Version) *wire.MsgTx {
|
|
|
|
h.t.Helper()
|
|
|
|
var spendTx *wire.MsgTx
|
|
select {
|
|
case spendTx = <-h.wallet.publishChan:
|
|
case <-time.After(timeout):
|
|
h.t.Fatal("expected spend transaction to be broadcast")
|
|
}
|
|
|
|
// The spending transaction should spend the account.
|
|
foundAccountInput := false
|
|
for _, txIn := range spendTx.TxIn {
|
|
if txIn.PreviousOutPoint == accountBeforeSpend.OutPoint {
|
|
foundAccountInput = true
|
|
}
|
|
}
|
|
require.True(h.t, foundAccountInput, "account input in transaction")
|
|
|
|
// If no outputs were provided and the account wasn't re-created, we
|
|
// should expect to see a single wallet output.
|
|
if len(externalOutputs) == 0 && newValue == nil {
|
|
require.Len(h.t, spendTx.TxOut, 1)
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
spendTx.TxOut[0].PkScript, &chaincfg.MainNetParams,
|
|
)
|
|
require.NoError(h.t, err)
|
|
require.Len(h.t, addrs, 1)
|
|
addr, ok := addrs[0].(*btcutil.AddressWitnessPubKeyHash)
|
|
if !ok {
|
|
h.t.Fatalf("expected P2WPKH address, found %T", addr)
|
|
}
|
|
// Witness program of address returned by the mock
|
|
// implementation of NextAddr.
|
|
witnessProgram := btcutil.Hash160(testRawTraderKey)
|
|
if !bytes.Equal(addr.WitnessProgram(), witnessProgram) {
|
|
h.t.Fatalf("expected witness program %x, got %x",
|
|
witnessProgram, addr.WitnessProgram())
|
|
}
|
|
|
|
return spendTx
|
|
}
|
|
|
|
// Otherwise, the spending transaction should include the expected
|
|
// inputs and outputs. If it recreates the account output, we should
|
|
// also attempt to locate it.
|
|
require.Len(h.t, spendTx.TxIn, len(externalInputs)+1)
|
|
|
|
// Perhaps this is an in-flight account upgrade?
|
|
upgradedAccount := accountBeforeSpend.Copy(VersionModifier(newVersion))
|
|
nextPkScript, err := upgradedAccount.NextOutputScript()
|
|
require.NoError(h.t, err)
|
|
outputs := externalOutputs
|
|
if newValue != nil {
|
|
outputs = append(outputs, &wire.TxOut{
|
|
Value: int64(*newValue),
|
|
PkScript: nextPkScript,
|
|
})
|
|
}
|
|
require.Len(h.t, spendTx.TxOut, len(outputs))
|
|
|
|
// The input and output indices may not match due to BIP-69 sorting.
|
|
nextInput:
|
|
for _, input := range externalInputs {
|
|
for _, txIn := range spendTx.TxIn {
|
|
if txIn.PreviousOutPoint != input.OutPoint {
|
|
continue
|
|
}
|
|
continue nextInput
|
|
}
|
|
h.t.Fatalf("expected input %v in spend transaction",
|
|
input.OutPoint)
|
|
}
|
|
nextOutput:
|
|
for _, output := range outputs {
|
|
for _, txOut := range spendTx.TxOut {
|
|
if !bytes.Equal(txOut.PkScript, output.PkScript) {
|
|
continue
|
|
}
|
|
if txOut.Value != output.Value {
|
|
h.t.Fatalf("expected value %v for output %x, "+
|
|
"got %v", output.Value, output.PkScript,
|
|
txOut.Value)
|
|
}
|
|
continue nextOutput
|
|
}
|
|
h.t.Fatalf("expected output script %x in spend transaction",
|
|
output.PkScript)
|
|
}
|
|
|
|
return spendTx
|
|
}
|
|
|
|
// assertAuctioneerReceived asserts that auctioneer has received the correct
|
|
// information regarding an account modification.
|
|
func (h *testHarness) assertAuctioneerReceived(inputs []*lnwallet.Utxo,
|
|
outputs []*wire.TxOut) {
|
|
|
|
h.t.Helper()
|
|
|
|
h.auctioneer.mu.Lock()
|
|
defer h.auctioneer.mu.Unlock()
|
|
|
|
require.Len(h.t, inputs, len(h.auctioneer.inputsReceived))
|
|
for _, inp := range inputs {
|
|
found := false
|
|
for _, inputReceived := range h.auctioneer.inputsReceived {
|
|
if inp.OutPoint == inputReceived.PreviousOutPoint {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.Truef(h.t, found, "input %v missing", inp.OutPoint)
|
|
}
|
|
for _, inp := range inputs {
|
|
found := false
|
|
for _, outReceived := range h.auctioneer.prevOutputsReceived {
|
|
if bytes.Equal(inp.PkScript, outReceived.PkScript) &&
|
|
int64(inp.Value) == outReceived.Value {
|
|
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.Truef(
|
|
h.t, found, "input %v not in previous outputs",
|
|
inp.OutPoint,
|
|
)
|
|
}
|
|
|
|
require.Len(h.t, outputs, len(h.auctioneer.outputsReceived))
|
|
for _, output := range outputs {
|
|
found := false
|
|
for _, outputReceived := range h.auctioneer.outputsReceived {
|
|
if reflect.DeepEqual(*output, outputReceived) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
require.Truef(h.t, found, "output %x missing", output.PkScript)
|
|
}
|
|
|
|
h.auctioneer.inputsReceived = nil
|
|
h.auctioneer.outputsReceived = nil
|
|
h.auctioneer.prevOutputsReceived = nil
|
|
}
|
|
|
|
// assertAuctioneerMuSig2NoncesReceived makes sure that the nonces we created for
|
|
// our local MuSig2 session were sent to the auctioneer.
|
|
func (h *testHarness) assertAuctioneerMuSig2NoncesReceived() {
|
|
h.t.Helper()
|
|
|
|
h.auctioneer.mu.Lock()
|
|
defer h.auctioneer.mu.Unlock()
|
|
|
|
h.wallet.Lock()
|
|
defer h.wallet.Unlock()
|
|
|
|
require.Len(h.t, h.wallet.muSig2Sessions, 0)
|
|
require.Len(h.t, h.wallet.muSig2RemovedSessions, 1)
|
|
|
|
var sessionInfo *input.MuSig2SessionInfo
|
|
for _, info := range h.wallet.muSig2RemovedSessions {
|
|
sessionInfo = info
|
|
break
|
|
}
|
|
require.NotNil(h.t, sessionInfo)
|
|
|
|
require.Equal(
|
|
h.t, sessionInfo.PublicNonce[:], h.auctioneer.noncesReceived,
|
|
)
|
|
}
|
|
|
|
// assertAccountModification provides several assertions for an account
|
|
// modification to determine whether it was successful. The assertions ensure
|
|
// that the account is transitioned from its StatePendingUpdate state to
|
|
// StateOpen.
|
|
func (h *testHarness) assertAccountModification(account *Account,
|
|
inputs []*lnwallet.Utxo, outputs []*wire.TxOut,
|
|
newAccountValue btcutil.Amount, newAccountVersion Version,
|
|
accountInputIdx, accountOutputIdx uint32, broadcastHeight uint32) {
|
|
|
|
h.t.Helper()
|
|
|
|
// We'll start by ensuring that the auctioneer received the intended
|
|
// modification parameters.
|
|
h.assertAuctioneerReceived(inputs, outputs)
|
|
|
|
// Make sure a MuSig2 signing session was created for a Taproot/MuSig2
|
|
// spend.
|
|
if account.Version >= VersionTaprootEnabled {
|
|
h.assertAuctioneerMuSig2NoncesReceived()
|
|
}
|
|
|
|
// A proper spend transaction should have been broadcast that contains
|
|
// the expected inputs and outputs from above, and the recreated account
|
|
// output.
|
|
spendTx := h.assertSpendTxBroadcast(
|
|
account, inputs, outputs, &newAccountValue, newAccountVersion,
|
|
)
|
|
|
|
// The account should be found within the store with the following
|
|
// modifiers.
|
|
mods := []Modifier{
|
|
ValueModifier(newAccountValue),
|
|
StateModifier(StatePendingUpdate),
|
|
OutPointModifier(wire.OutPoint{
|
|
Hash: spendTx.TxHash(),
|
|
Index: accountOutputIdx,
|
|
}),
|
|
HeightHintModifier(broadcastHeight),
|
|
IncrementBatchKey(),
|
|
LatestTxModifier(spendTx),
|
|
VersionModifier(newAccountVersion),
|
|
}
|
|
for _, mod := range mods {
|
|
mod(account)
|
|
}
|
|
h.assertAccountExists(account)
|
|
|
|
// Notify the transaction as a spend of the account. The account should
|
|
// remain in StatePendingUpdate until it reaches the appropriate number
|
|
// of confirmations.
|
|
select {
|
|
case h.notifier.spendChan <- &chainntnfs.SpendDetail{
|
|
SpendingTx: spendTx,
|
|
SpenderInputIndex: accountInputIdx,
|
|
}:
|
|
|
|
case <-time.After(timeout):
|
|
h.t.Fatalf("couldn't send tx spend notification")
|
|
}
|
|
|
|
h.assertAccountExists(account)
|
|
|
|
// Notify the confirmation, causing the account to transition back to
|
|
// StateOpen.
|
|
confHeight := broadcastHeight + 6
|
|
|
|
select {
|
|
case h.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
Tx: spendTx,
|
|
BlockHeight: confHeight,
|
|
}:
|
|
|
|
case <-time.After(timeout):
|
|
h.t.Fatalf("couldn't send tx confirmation")
|
|
}
|
|
|
|
StateModifier(StateOpen)(account)
|
|
HeightHintModifier(confHeight)(account)
|
|
h.assertAccountExists(account)
|
|
}
|
|
|
|
func (h *testHarness) restartManager() {
|
|
h.t.Helper()
|
|
|
|
h.manager.Stop()
|
|
|
|
mgr, ok := h.manager.(*manager)
|
|
require.True(h.t, ok, "invalid manager")
|
|
|
|
auctioneer := newMockAuctioneer()
|
|
h.auctioneer = auctioneer
|
|
h.manager = NewManager(&ManagerConfig{
|
|
Store: mgr.cfg.Store,
|
|
Auctioneer: auctioneer,
|
|
Wallet: mgr.cfg.Wallet,
|
|
Signer: mgr.cfg.Signer,
|
|
ChainNotifier: mgr.cfg.ChainNotifier,
|
|
TxSource: mgr.cfg.TxSource,
|
|
ChainParams: mgr.cfg.ChainParams,
|
|
LndVersion: mgr.cfg.LndVersion,
|
|
})
|
|
|
|
require.NoError(h.t, h.manager.Start())
|
|
}
|
|
|
|
// TestNewAccountHappyFlow ensures that we are able to create a new account
|
|
// and close it throughout the happy flow.
|
|
func TestNewAccountHappyFlow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
expr := defaultFeeExpr
|
|
h.closeAccount(account, &expr, bestHeight+1)
|
|
})
|
|
}
|
|
|
|
// TestResumeAccountAfterRestart ensures we're able to properly create a new
|
|
// account even if we've shut down during the process.
|
|
func TestResumeAccountAfterRestart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
value = maxAccountValue
|
|
expiry = maxAccountExpiry
|
|
)
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
// We'll create an interceptor for SendOutputs to simulate a
|
|
// transaction being crafted and persisted for the account, but
|
|
// it not being returned to the account manager because of a
|
|
// crash, etc.
|
|
sendOutputsChan := make(chan *wire.MsgTx)
|
|
sendOutputsInterceptor := func(_ context.Context,
|
|
outputs []*wire.TxOut,
|
|
_ chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
|
|
|
tx := &wire.MsgTx{
|
|
Version: 2,
|
|
TxOut: outputs,
|
|
}
|
|
|
|
h.wallet.addTx(tx)
|
|
sendOutputsChan <- tx
|
|
|
|
return nil, errors.New("error")
|
|
}
|
|
h.wallet.interceptSendOutputs(sendOutputsInterceptor)
|
|
|
|
// We'll then proceed to create a new account. We expect this to
|
|
// fail given the interceptor above, but that's fine. We should
|
|
// still have a persisted intent to create the account.
|
|
go func() {
|
|
_, _ = h.manager.InitAccount(
|
|
context.Background(), value, tc.version,
|
|
chainfee.FeePerKwFloor, expiry, bestHeight,
|
|
)
|
|
}()
|
|
|
|
var tx *wire.MsgTx
|
|
select {
|
|
case tx = <-sendOutputsChan:
|
|
case <-time.After(timeout):
|
|
t.Fatal("expected call to SendOutputs")
|
|
}
|
|
|
|
account := &Account{
|
|
Value: value,
|
|
Expiry: expiry,
|
|
TraderKey: testTraderKeyDesc,
|
|
AuctioneerKey: testAuctioneerKey,
|
|
BatchKey: testBatchKey,
|
|
Secret: sharedSecret,
|
|
HeightHint: bestHeight,
|
|
State: StateInitiated,
|
|
Version: tc.version,
|
|
}
|
|
h.assertAccountExists(account)
|
|
|
|
// Then, we'll create a new interceptor to send us a signal if
|
|
// SendOutputs is invoked again. This shouldn't happen as the
|
|
// transaction originally crafted the first time should be in our
|
|
// TxSource, so we should pick it from there.
|
|
sendOutputsSignal := make(chan struct{}, 1)
|
|
sendOutputsInterceptor = func(_ context.Context,
|
|
outputs []*wire.TxOut,
|
|
_ chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
|
|
|
close(sendOutputsSignal)
|
|
return nil, errors.New("error")
|
|
}
|
|
h.wallet.interceptSendOutputs(sendOutputsInterceptor)
|
|
|
|
// Restart the manager. This should cause any pending accounts
|
|
// to be resumed and their transaction rebroadcast/recreated. In
|
|
// our case, we should have an account transaction in our
|
|
// TxSource, so a new one shouldn't be created.
|
|
h.restartManager()
|
|
|
|
select {
|
|
case accountTx := <-h.wallet.publishChan:
|
|
require.Equal(t, accountTx.TxHash(), tx.TxHash())
|
|
|
|
case <-time.After(timeout):
|
|
h.t.Fatal("expected account transaction to be " +
|
|
"rebroadcast")
|
|
}
|
|
|
|
select {
|
|
case <-sendOutputsSignal:
|
|
t.Fatal("unexpected call to SendOutputs")
|
|
case <-time.After(2 * timeout):
|
|
}
|
|
|
|
// With the account resumed, it should now be in a
|
|
// StatePendingOpen state.
|
|
account.State = StatePendingOpen
|
|
account.LatestTx = tx
|
|
account.OutPoint = wire.OutPoint{
|
|
Hash: tx.TxHash(),
|
|
Index: 0,
|
|
}
|
|
h.assertAccountExists(account)
|
|
|
|
// Notify the confirmation of the account.
|
|
confHeight := uint32(bestHeight + 6)
|
|
h.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
BlockHeight: confHeight,
|
|
}
|
|
|
|
// This should prompt the account to now be in a Confirmed
|
|
// state.
|
|
account.State = StateOpen
|
|
account.HeightHint = confHeight
|
|
h.assertAccountExists(account)
|
|
})
|
|
}
|
|
|
|
// TestAccountCloseFundsDestination ensures the different possible destinations
|
|
// for the funds of an account being closed work as intended.
|
|
func TestAccountClose(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const highFeeRate = 10_000 * chainfee.FeePerKwFloor
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "below P2WKH dust limit",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: p2wpkh,
|
|
FeeRate: highFeeRate,
|
|
},
|
|
expectedErr: "with 2530000 sat/kw results in dust",
|
|
},
|
|
{
|
|
name: "below P2SH dust limit",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: np2wpkh,
|
|
FeeRate: highFeeRate,
|
|
},
|
|
expectedErr: "with 2530000 sat/kw results in dust",
|
|
},
|
|
{
|
|
name: "below P2WSH dust limit",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: p2wsh,
|
|
FeeRate: highFeeRate,
|
|
},
|
|
expectedErr: "with 2530000 sat/kw results in dust",
|
|
},
|
|
{
|
|
name: "single wallet P2WKH output",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: nil,
|
|
FeeRate: chainfee.FeePerKwFloor,
|
|
},
|
|
fee: btcutil.Amount(141),
|
|
},
|
|
{
|
|
name: "single external P2WKH output",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: p2wpkh,
|
|
FeeRate: 2 * chainfee.FeePerKwFloor,
|
|
},
|
|
fee: btcutil.Amount(282),
|
|
},
|
|
{
|
|
name: "multiple external outputs",
|
|
feeExpr: &OutputsWithImplicitFee{
|
|
{
|
|
PkScript: p2wpkh,
|
|
Value: 10_000,
|
|
},
|
|
{
|
|
PkScript: np2wpkh,
|
|
Value: 10_000,
|
|
},
|
|
{
|
|
PkScript: p2wsh,
|
|
Value: 10_000,
|
|
},
|
|
},
|
|
fee: btcutil.Amount(70_000),
|
|
},
|
|
{
|
|
name: "taproot single wallet P2WKH output",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: nil,
|
|
FeeRate: chainfee.FeePerKwFloor,
|
|
},
|
|
fee: btcutil.Amount(100),
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
{
|
|
name: "taproot single wallet P2TR output",
|
|
feeExpr: &OutputWithFee{
|
|
PkScript: p2tr,
|
|
FeeRate: chainfee.FeePerKwFloor,
|
|
},
|
|
fee: btcutil.Amount(112),
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
// We'll start by creating a new account of the minimum value
|
|
// for each test.
|
|
account := h.openAccount(
|
|
MinAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
// We'll immediately attempt to close the account with the
|
|
// test's fee expression.
|
|
_, err := h.manager.CloseAccount(
|
|
context.Background(), account.TraderKey.PubKey,
|
|
tc.feeExpr, bestHeight,
|
|
)
|
|
|
|
// If the test's fee expression is not valid, we should expect
|
|
// to see an error.
|
|
if tc.expectedErr != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
|
|
return
|
|
}
|
|
|
|
// Otherwise, we'll proceed to check whether our fee expression
|
|
// was honored.
|
|
require.NoError(t, err)
|
|
|
|
// Any external outputs (not sourced from the backing lnd node)
|
|
// should be found in the closing transaction.
|
|
var externalOutputs []*wire.TxOut
|
|
switch feeExpr := tc.feeExpr.(type) {
|
|
case *OutputWithFee:
|
|
if feeExpr.PkScript != nil {
|
|
output := &wire.TxOut{
|
|
PkScript: feeExpr.PkScript,
|
|
Value: int64(
|
|
account.Value - tc.fee,
|
|
),
|
|
}
|
|
externalOutputs = append(
|
|
externalOutputs, output,
|
|
)
|
|
}
|
|
|
|
case *OutputsWithImplicitFee:
|
|
externalOutputs = feeExpr.Outputs()
|
|
|
|
default:
|
|
t.Fatal("unhandled fee expr")
|
|
}
|
|
|
|
// The account's closing transaction should be broadcast.
|
|
closeTx := h.assertSpendTxBroadcast(
|
|
account, nil, externalOutputs, nil, tc.version,
|
|
)
|
|
|
|
// Finally, compute the resulting fee of the transaction and
|
|
// ensure it matches what we expect.
|
|
var outputTotal btcutil.Amount
|
|
for _, output := range closeTx.TxOut {
|
|
outputTotal += btcutil.Amount(output.Value)
|
|
}
|
|
fee := account.Value - outputTotal
|
|
require.Equal(t, fee, tc.fee)
|
|
})
|
|
}
|
|
|
|
// TestAccountExpiration ensures that we properly detect when an account expires
|
|
// on-chain. As a result, the account should be marked as StateExpired in the
|
|
// database.
|
|
func TestAccountExpiration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
h.expireAccount(account)
|
|
})
|
|
}
|
|
|
|
// TestAccountSpendBatchNotFinalized ensures that if a pending batch exists at
|
|
// the time of an account spend, then its updates are applied to the account in
|
|
// order to properly locate the latest account output.
|
|
func TestAccountSpendBatchNotFinalized(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
newVersion: VersionTaprootEnabled,
|
|
},
|
|
{
|
|
name: "happy path version upgrade during batch",
|
|
version: VersionInitialNoVersion,
|
|
newVersion: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
// Create an account spend which we'll notify later. This spend
|
|
// should take the multi-sig path to trigger the pending batch
|
|
// logic.
|
|
const newValue = maxAccountValue / 2
|
|
upgradedAccount := account.Copy(VersionModifier(tc.newVersion))
|
|
newPkScript, err := upgradedAccount.NextOutputScript()
|
|
require.NoError(h.t, err)
|
|
spendTx := &wire.MsgTx{
|
|
Version: 2,
|
|
TxIn: []*wire.TxIn{{
|
|
PreviousOutPoint: account.OutPoint,
|
|
Witness: wire.TxWitness{
|
|
{0x01}, // Use multi-sig path.
|
|
{},
|
|
{},
|
|
},
|
|
}},
|
|
TxOut: []*wire.TxOut{{
|
|
Value: int64(newValue),
|
|
PkScript: newPkScript,
|
|
}},
|
|
}
|
|
|
|
// Then, we'll simulate a pending batch by staging some account
|
|
// updates that should be applied once the spend arrives.
|
|
mods := []Modifier{
|
|
ValueModifier(newValue),
|
|
StateModifier(StatePendingUpdate),
|
|
OutPointModifier(wire.OutPoint{
|
|
Hash: spendTx.TxHash(),
|
|
Index: 0,
|
|
}),
|
|
IncrementBatchKey(),
|
|
VersionModifier(tc.newVersion),
|
|
}
|
|
h.store.setPendingBatch(func() error {
|
|
return h.store.updateAccount(account, mods...)
|
|
})
|
|
|
|
// Notify the spend.
|
|
h.notifier.spendChan <- &chainntnfs.SpendDetail{
|
|
SpendingTx: spendTx,
|
|
}
|
|
|
|
// Assert that the account updates have been applied. Note that
|
|
// it may seem like our account pointer hasn't had the updates
|
|
// applied, but the updateAccount call above does so implicitly.
|
|
h.assertAccountExists(account)
|
|
})
|
|
}
|
|
|
|
// TestAccountWithdrawal ensures that we can process an account withdrawal
|
|
// through the happy flow.
|
|
func TestAccountWithdrawal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const feeRate = chainfee.FeePerKwFloor
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
|
|
// We'll use the lowest fee rate possible, which should
|
|
// yield a transaction fee of 260 satoshis when taking
|
|
// into account the outputs we'll be withdrawing to.
|
|
fee: 260,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
newVersion: VersionTaprootEnabled,
|
|
fee: 219,
|
|
},
|
|
{
|
|
name: "happy path version upgrade during " +
|
|
"withdrawal",
|
|
version: VersionInitialNoVersion,
|
|
newVersion: VersionTaprootEnabled,
|
|
fee: 260,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
// With our account created, we'll start with an invalid
|
|
// withdrawal to a dust output, which should fail.
|
|
dustOutput := &wire.TxOut{Value: 0, PkScript: p2wsh}
|
|
_, _, err := h.manager.WithdrawAccount(
|
|
context.Background(), account.TraderKey.PubKey,
|
|
[]*wire.TxOut{dustOutput}, feeRate, bestHeight, 0,
|
|
tc.newVersion,
|
|
)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "dust output")
|
|
|
|
// We'll now attempt a withdrawal that should succeed. We'll
|
|
// start by creating the outputs we'll withdraw our funds to.
|
|
// We'll create three outputs, one of each supported output
|
|
// type. Each output will have 1/4 of the account's value.
|
|
valuePerOutput := account.Value / 4
|
|
outputs := []*wire.TxOut{
|
|
{
|
|
Value: int64(valuePerOutput),
|
|
PkScript: p2wsh,
|
|
},
|
|
{
|
|
Value: int64(valuePerOutput),
|
|
PkScript: p2wpkh,
|
|
},
|
|
{
|
|
Value: int64(valuePerOutput),
|
|
PkScript: np2wpkh,
|
|
},
|
|
}
|
|
|
|
// Attempt the withdrawal.
|
|
//
|
|
// If successful, we'll follow with a series of assertions to
|
|
// ensure it was performed correctly.
|
|
_, _, err = h.manager.WithdrawAccount(
|
|
context.Background(), account.TraderKey.PubKey, outputs,
|
|
feeRate, bestHeight, 0, tc.newVersion,
|
|
)
|
|
require.NoError(h.t, err)
|
|
|
|
// The value of the account after the withdrawal depends on the
|
|
// transaction fee and the amount of each output withdrawn to.
|
|
withdrawOutputSum := valuePerOutput * btcutil.Amount(
|
|
len(outputs),
|
|
)
|
|
valueAfterWithdrawal := account.Value - withdrawOutputSum -
|
|
tc.fee
|
|
h.assertAccountModification(
|
|
account, nil, outputs, valueAfterWithdrawal,
|
|
tc.newVersion, 0, 0, bestHeight,
|
|
)
|
|
|
|
// Finally, close the account to ensure we can process another
|
|
// spend after the withdrawal.
|
|
expr := defaultFeeExpr
|
|
h.closeAccount(account, &expr, bestHeight)
|
|
})
|
|
}
|
|
|
|
// TestAccountDeposit ensures that we can process an account deposit
|
|
// through the happy flow.
|
|
func TestAccountDeposit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll start by defining our initial account value and its value after
|
|
// a successful deposit.
|
|
const initialAccountValue = MinAccountValue
|
|
const valueAfterDeposit = initialAccountValue * 2
|
|
const utxoAmount = initialAccountValue * 3
|
|
const depositAmount = valueAfterDeposit - initialAccountValue
|
|
|
|
const feeRate = chainfee.FeePerKwFloor
|
|
const accountInputFees = 110
|
|
const accountInputFeesTaproot = 68
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
fundedOutputAmount: depositAmount + accountInputFees,
|
|
|
|
// We'll use the lowest fee rate possible, which should
|
|
// yield a transaction fee of 346 satoshis when taking
|
|
// into account the additional inputs needed to satisfy
|
|
// the deposit.
|
|
fundingTxFee: accountInputFees + 236,
|
|
utxo: &lnwallet.Utxo{
|
|
AddressType: lnwallet.WitnessPubKey,
|
|
Value: utxoAmount,
|
|
PkScript: p2wpkh,
|
|
OutPoint: wire.OutPoint{Index: 1},
|
|
},
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "insufficient fees",
|
|
fundedOutputAmount: depositAmount + accountInputFees,
|
|
|
|
// We'll use a fee that is lower than the lowest fee
|
|
// we can get with the lowest rate possible (which would
|
|
// be 346 satoshis), so we should get an error.
|
|
fundingTxFee: accountInputFees + 10,
|
|
utxo: &lnwallet.Utxo{
|
|
AddressType: lnwallet.WitnessPubKey,
|
|
Value: utxoAmount,
|
|
PkScript: p2wpkh,
|
|
OutPoint: wire.OutPoint{Index: 1},
|
|
},
|
|
version: VersionInitialNoVersion,
|
|
expectedErr: "signed transaction only pays 120 sats " +
|
|
"in fees while 255 are required for relay",
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
fundedOutputAmount: depositAmount +
|
|
accountInputFeesTaproot,
|
|
|
|
// We'll use the lowest fee rate possible, which should
|
|
// yield a transaction fee of 304 satoshis when taking
|
|
// into account the additional inputs needed to satisfy
|
|
// the deposit.
|
|
fundingTxFee: accountInputFeesTaproot + 236,
|
|
utxo: &lnwallet.Utxo{
|
|
AddressType: lnwallet.WitnessPubKey,
|
|
Value: utxoAmount,
|
|
PkScript: p2wpkh,
|
|
OutPoint: wire.OutPoint{Index: 1},
|
|
},
|
|
version: VersionTaprootEnabled,
|
|
newVersion: VersionTaprootEnabled,
|
|
},
|
|
{
|
|
name: "happy path version upgrade during deposit",
|
|
fundedOutputAmount: depositAmount +
|
|
accountInputFees,
|
|
|
|
// We'll use the lowest fee rate possible, which should
|
|
// yield a transaction fee of 346 satoshis when taking
|
|
// into account the additional inputs needed to satisfy
|
|
// the deposit.
|
|
fundingTxFee: accountInputFees + 236,
|
|
utxo: &lnwallet.Utxo{
|
|
AddressType: lnwallet.WitnessPubKey,
|
|
Value: utxoAmount,
|
|
PkScript: p2wpkh,
|
|
OutPoint: wire.OutPoint{Index: 1},
|
|
},
|
|
version: VersionInitialNoVersion,
|
|
newVersion: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
initialAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
upgradedAccount := account.Copy(VersionModifier(tc.newVersion))
|
|
accountOutputScript, _ := upgradedAccount.NextOutputScript()
|
|
|
|
// We'll provide a funded PSBT to the manager that has a change
|
|
// output.
|
|
h.wallet.utxos = []*lnwallet.Utxo{tc.utxo}
|
|
h.wallet.fundPsbt = &psbt.Packet{
|
|
UnsignedTx: &wire.MsgTx{
|
|
Version: 2,
|
|
TxIn: []*wire.TxIn{{
|
|
PreviousOutPoint: tc.utxo.OutPoint,
|
|
}},
|
|
TxOut: []*wire.TxOut{{
|
|
Value: int64(tc.fundedOutputAmount),
|
|
PkScript: accountOutputScript,
|
|
}, {
|
|
Value: int64(
|
|
tc.utxo.Value - depositAmount -
|
|
tc.fundingTxFee,
|
|
),
|
|
PkScript: np2wpkh,
|
|
}},
|
|
},
|
|
Inputs: []psbt.PInput{{
|
|
WitnessUtxo: &wire.TxOut{
|
|
Value: int64(tc.utxo.Value),
|
|
PkScript: tc.utxo.PkScript,
|
|
},
|
|
// Normally the FinalizePsbt call would add the
|
|
// signatures for the wallet UTXOs. Since we're
|
|
// only using a mock, we add some signatures
|
|
// manually.
|
|
PartialSigs: []*psbt.PartialSig{{
|
|
Signature: []byte{1, 2, 3},
|
|
}},
|
|
}},
|
|
Outputs: []psbt.POutput{{}, {}},
|
|
}
|
|
h.wallet.fundPsbtChangeIdx = 1
|
|
|
|
// Attempt the deposit.
|
|
//
|
|
// If successful, we'll follow with a series of assertions to
|
|
// ensure it was performed correctly.
|
|
_, _, err := h.manager.DepositAccount(
|
|
context.Background(), account.TraderKey.PubKey,
|
|
depositAmount, feeRate, bestHeight, 0, tc.newVersion,
|
|
)
|
|
|
|
if tc.expectedErr != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
// The transaction should have found the expected account input
|
|
// and output at the following indices.
|
|
const accountInputIdx = 1
|
|
const accountOutputIdx = 1
|
|
|
|
h.assertAccountModification(
|
|
account, h.wallet.utxos,
|
|
[]*wire.TxOut{h.wallet.fundPsbt.UnsignedTx.TxOut[0]},
|
|
valueAfterDeposit, tc.newVersion, accountInputIdx,
|
|
accountOutputIdx, bestHeight,
|
|
)
|
|
|
|
// Finally, close the account to ensure we can process another
|
|
// spend after the withdrawal.
|
|
expr := defaultFeeExpr
|
|
h.closeAccount(account, &expr, bestHeight)
|
|
})
|
|
}
|
|
|
|
// TestAccountConsecutiveBatches ensures that we can process an account update
|
|
// through multiple consecutive batches that only confirm after we've already
|
|
// updated our database state.
|
|
func TestAccountConsecutiveBatches(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
newVersion: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
// Then, we'll simulate the maximum number of unconfirmed
|
|
// batches to happen that'll all confirm in the same block.
|
|
const newValue = maxAccountValue / 2
|
|
const numBatches = 10
|
|
batchTxs := make([]*wire.MsgTx, numBatches)
|
|
for i := 0; i < numBatches; i++ {
|
|
newPkScript, err := account.NextOutputScript()
|
|
require.NoError(t, err)
|
|
|
|
// Create an account spend which we'll notify later.
|
|
// This spend should take the multi-sig path to trigger
|
|
// the logic to lookup previous outpoints.
|
|
batchTx := &wire.MsgTx{
|
|
Version: 2,
|
|
TxIn: []*wire.TxIn{{
|
|
PreviousOutPoint: account.OutPoint,
|
|
Witness: wire.TxWitness{
|
|
{0x01}, // Use multi-sig path.
|
|
{},
|
|
{},
|
|
},
|
|
}},
|
|
TxOut: []*wire.TxOut{{
|
|
Value: int64(newValue),
|
|
PkScript: newPkScript,
|
|
}},
|
|
}
|
|
batchTxs[i] = batchTx
|
|
|
|
mods := []Modifier{
|
|
ValueModifier(newValue - btcutil.Amount(i)),
|
|
StateModifier(StatePendingBatch),
|
|
OutPointModifier(wire.OutPoint{
|
|
Hash: batchTx.TxHash(),
|
|
Index: 0,
|
|
}),
|
|
IncrementBatchKey(),
|
|
VersionModifier(tc.newVersion),
|
|
}
|
|
err = h.store.updateAccount(account, mods...)
|
|
require.NoError(t, err)
|
|
|
|
// The RPC server will notify the manager each time a
|
|
// batch is finalized, we do the same here.
|
|
err = h.manager.WatchMatchedAccounts(
|
|
context.Background(),
|
|
[]*btcec.PublicKey{account.TraderKey.PubKey},
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Notify the confirmation, causing the account to transition
|
|
// back to StateOpen.
|
|
confHeight := bestHeight + 6
|
|
h.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
Tx: batchTxs[len(batchTxs)-1],
|
|
BlockHeight: uint32(confHeight),
|
|
}
|
|
StateModifier(StateOpen)(account)
|
|
HeightHintModifier(uint32(confHeight))(account)
|
|
h.assertAccountExists(account)
|
|
})
|
|
}
|
|
|
|
// TestAccountUpdateSubscriptionOnRestart ensures that the account manager
|
|
// subscribes accounts in certain states for auction updates after a restart.
|
|
func TestAccountUpdateSubscriptionOnRestart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
name: "happy path version 0",
|
|
version: VersionInitialNoVersion,
|
|
},
|
|
{
|
|
name: "happy path version 1",
|
|
version: VersionTaprootEnabled,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
account := h.openAccount(
|
|
maxAccountValue, bestHeight+maxAccountExpiry,
|
|
bestHeight, tc.version,
|
|
)
|
|
|
|
// StateOpen case.
|
|
h.restartManager()
|
|
h.assertAccountSubscribed(account.TraderKey.PubKey)
|
|
|
|
err := h.store.UpdateAccount(
|
|
account, StateModifier(StatePendingBatch),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// StatePendingBatch case.
|
|
h.restartManager()
|
|
h.assertAccountSubscribed(account.TraderKey.PubKey)
|
|
|
|
err = h.store.UpdateAccount(
|
|
account, StateModifier(StatePendingUpdate),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// StatePendingUpdate case.
|
|
h.restartManager()
|
|
h.assertAccountNotSubscribed(account.TraderKey.PubKey)
|
|
|
|
// Confirm the account.
|
|
confHeight := uint32(bestHeight + 6)
|
|
h.notifier.confChan <- &chainntnfs.TxConfirmation{
|
|
BlockHeight: confHeight,
|
|
}
|
|
account.State = StateOpen
|
|
account.HeightHint = confHeight
|
|
h.assertAccountExists(account)
|
|
|
|
// It should now be subscribed since it's eligible for batch
|
|
// execution.
|
|
h.assertAccountSubscribed(account.TraderKey.PubKey)
|
|
})
|
|
}
|
|
|
|
// TestMakeTxnLabel tests that the label will be formatted properly, and also
|
|
// truncated if needed.
|
|
func TestMakeTxnLabel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
prefix string
|
|
labelContext string
|
|
|
|
label string
|
|
}{
|
|
// Normal case, prefix present, should add a space.
|
|
{
|
|
prefix: "ok",
|
|
labelContext: "important stuff",
|
|
label: "ok important stuff",
|
|
},
|
|
|
|
// No prefix, should be no leading space.
|
|
{
|
|
prefix: "",
|
|
labelContext: "important stuff",
|
|
label: "important stuff",
|
|
},
|
|
|
|
// No prefix, prefix itself is so long the context can't fit,
|
|
// should be truncated.
|
|
{
|
|
prefix: strings.Repeat("o", 500),
|
|
labelContext: "will be left off",
|
|
label: strings.Repeat("o", 500),
|
|
},
|
|
|
|
// Prefix itself is too long, it'll be truncated as well.
|
|
{
|
|
prefix: strings.Repeat("o", 501),
|
|
labelContext: "will be left off",
|
|
label: strings.Repeat("o", 500),
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
genLabel := makeTxnLabel(
|
|
testCase.prefix, testCase.labelContext,
|
|
)
|
|
|
|
require.Equal(t, genLabel, testCase.label)
|
|
}
|
|
}
|
|
|
|
// TestParseTxLabel tests whether an account transaction labels can be
|
|
// parsed correctly.
|
|
func TestParseTxLabel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []*testCase{
|
|
{
|
|
fee: btcutil.Amount(1027),
|
|
action: WITHDRAW,
|
|
isExpirySpend: false,
|
|
},
|
|
{
|
|
fee: btcutil.Amount(42),
|
|
action: DEPOSIT,
|
|
isExpirySpend: false,
|
|
},
|
|
{
|
|
fee: btcutil.Amount(42),
|
|
action: RENEW,
|
|
isExpirySpend: true,
|
|
},
|
|
}
|
|
|
|
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
|
|
expiryHeight := uint32(bestHeight + maxAccountExpiry)
|
|
account := h.openAccount(
|
|
maxAccountValue, expiryHeight, bestHeight, tc.version,
|
|
)
|
|
acctKey := account.TraderKey.PubKey.SerializeCompressed()
|
|
key := fmt.Sprintf("%x", acctKey)
|
|
label := actionTxLabel(
|
|
account, tc.action, tc.isExpirySpend, &tc.fee,
|
|
btcutil.Amount(10000),
|
|
)
|
|
actual, err := ParseTxLabel(label)
|
|
|
|
expected := &TxLabel{
|
|
AccountTxLabel{
|
|
Key: key,
|
|
Action: tc.action,
|
|
ExpiryHeight: expiryHeight,
|
|
OutputIndex: 0,
|
|
IsExpirySpend: tc.isExpirySpend,
|
|
TxFee: &tc.fee,
|
|
BalanceDiff: btcutil.Amount(10000),
|
|
},
|
|
}
|
|
require.Nil(t, err)
|
|
require.Equal(t, expected, actual)
|
|
})
|
|
}
|