func TestAccountExpiration(t *testing.T) {
	t.Parallel()
	const bestHeight = 100
	h := newTestHarness(t)
	h.start()
	defer h.stop()
	account := h.openAccount(
		maxAccountValue, bestHeight+maxAccountExpiry, bestHeight,
	)
	h.expireAccount(account)
}
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.
package account
import (
	"bytes"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"
	"github.com/btcsuite/btcd/btcec"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcutil"
	"github.com/davecgh/go-spew/spew"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"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
)
var (
	p2wsh, _   = hex.DecodeString("00208c2865c87ffd33fc5d698c7df9cf2d0fb39d93103c637a06dea32c848ebc3e1d")
	p2wpkh, _  = hex.DecodeString("0014ccdeffed4f9c91d5bf45c34e4b8f03a5025ec062")
	np2wpkh, _ = hex.DecodeString("a91458c11505b54582ab04e96d36908f85a8b689459787")
	defaultFeeExpr = OutputWithFee{
		FeeRate: chainfee.FeePerKwFloor,
	}
)
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,
		}),
	}
}
func (h *testHarness) start() {
	if err := h.manager.Start(); err != nil {
		h.t.Fatalf("unable to start account manager: %v", err)
	}
}
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()
	if _, ok := h.auctioneer.subscribed[rawTraderKey]; !ok {
		h.t.Fatalf("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()
	if _, ok := h.auctioneer.subscribed[rawTraderKey]; ok {
		h.t.Fatalf("account %x is 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) {
			found.TraderKey.PubKey.Curve = nil
			expected.TraderKey.PubKey.Curve = nil
			found.AuctioneerKey.Curve = nil
			expected.AuctioneerKey.Curve = nil
			found.BatchKey.Curve = nil
			expected.BatchKey.Curve = nil
			return fmt.Errorf("expected account: %v\ngot: %v",
				spew.Sdump(expected), spew.Sdump(found))
		}
		return nil
	}, 10*timeout)
	if err != nil {
		h.t.Fatal(err)
	}
}
func (h *testHarness) openAccount(value btcutil.Amount, expiry uint32,  
	bestHeight uint32) *Account {  
	h.t.Helper()
	ctx := context.Background()
	account, err := h.manager.InitAccount(
		ctx, value, chainfee.FeePerKwFloor, expiry, bestHeight,
	)
	if err != nil {
		h.t.Fatalf("unable to create new account: %v", err)
	}
	h.assertAccountExists(account)
	h.assertAccountNotSubscribed(account.TraderKey.PubKey)
	confHeight := bestHeight + 6
	h.notifier.confChan <- &chainntnfs.TxConfirmation{
		BlockHeight: confHeight,
	}
	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()
	h.notifier.blockChan <- int32(account.Expiry)
	account.State = StateExpired
	h.assertAccountExists(account)
}
func (h *testHarness) closeAccount(account *Account, feeExpr FeeExpr,
	bestHeight uint32) *wire.MsgTx {
	h.t.Helper()
	go func() {
		_, err := h.manager.CloseAccount(
			context.Background(), account.TraderKey.PubKey, feeExpr,
			bestHeight,
		)
		if err != nil {
			h.t.Logf("unable to close account: %v", err)
		}
	}()
	closeTx := h.assertSpendTxBroadcast(account, nil, nil, nil)
	account.Value = 0
	account.State = StatePendingClosed
	account.HeightHint = bestHeight
	account.LatestTx = closeTx
	h.assertAccountExists(account)
	spendHeight := bestHeight + 6
	h.notifier.spendChan <- &chainntnfs.SpendDetail{
		SpendingTx:     closeTx,
		SpendingHeight: int32(spendHeight),
	}
	account.State = StateClosed
	account.HeightHint = spendHeight
	h.assertAccountExists(account)
	return closeTx
}
func (h *testHarness) assertSpendTxBroadcast(accountBeforeSpend *Account,
	externalInputs []*lnwallet.Utxo, externalOutputs []*wire.TxOut,
	newValue *btcutil.Amount) *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")
	}
	foundAccountInput := false
	for _, txIn := range spendTx.TxIn {
		if txIn.PreviousOutPoint == accountBeforeSpend.OutPoint {
			foundAccountInput = true
		}
	}
	if !foundAccountInput {
		h.t.Fatalf("did not find account input %v in spend transaction",
			accountBeforeSpend.OutPoint)
	}
	if len(externalOutputs) == 0 && newValue == nil {
		if len(spendTx.TxOut) != 1 {
			h.t.Fatalf("expected 1 output in spend transaction, "+
				"found %d", len(spendTx.TxOut))
		}
		_, addrs, _, err := txscript.ExtractPkScriptAddrs(
			spendTx.TxOut[0].PkScript, &chaincfg.MainNetParams,
		)
		if err != nil {
			h.t.Fatalf("unable to extract address: %v", err)
		}
		if len(addrs) != 1 {
			h.t.Fatalf("expected 1 address, found %d", len(addrs))
		}
		addr, ok := addrs[0].(*btcutil.AddressWitnessPubKeyHash)
		if !ok {
			h.t.Fatalf("expected P2WPKH address, found %T", addr)
		}
		witnessProgram := btcutil.Hash160(testRawTraderKey)
		if !bytes.Equal(addr.WitnessProgram(), witnessProgram) {
			h.t.Fatalf("expected witness program %x, got %x",
				witnessProgram, addr.WitnessProgram())
		}
		return spendTx
	}
	if len(spendTx.TxIn) != len(externalInputs)+1 {
		h.t.Fatalf("expected %d input(s) in spend transaction, found %d",
			len(externalInputs)+1, len(spendTx.TxIn))
	}
	nextPkScript, err := accountBeforeSpend.NextOutputScript()
	if err != nil {
		h.t.Fatalf("unable to generate next output script: %v",
			err)
	}
	outputs := externalOutputs
	if newValue != nil {
		outputs = append(outputs, &wire.TxOut{
			Value:    int64(*newValue),
			PkScript: nextPkScript,
		})
	}
	if len(spendTx.TxOut) != len(outputs) {
		h.t.Fatalf("expected %d output(s) in spend transaction, found %d",
			len(outputs), len(spendTx.TxOut))
	}
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
}
func (h *testHarness) assertAuctioneerReceived(inputs []*lnwallet.Utxo,
	outputs []*wire.TxOut) {
	h.t.Helper()
	h.auctioneer.mu.Lock()
	defer h.auctioneer.mu.Unlock()
	if len(inputs) != len(h.auctioneer.inputsReceived) {
		h.t.Fatal("auctioneer input count mismatch")
	}
	for _, input := range inputs {
		found := false
		for _, inputReceived := range h.auctioneer.inputsReceived {
			if input.OutPoint == inputReceived.PreviousOutPoint {
				found = true
				break
			}
		}
		if !found {
			h.t.Fatalf("expected auctioneer to receive input %v",
				input.OutPoint)
		}
	}
	if len(outputs) != len(h.auctioneer.outputsReceived) {
		h.t.Fatal("auctioneer output count mismatch")
	}
	for _, output := range outputs {
		found := false
		for _, outputReceived := range h.auctioneer.outputsReceived {
			if reflect.DeepEqual(*output, outputReceived) {
				found = true
				break
			}
		}
		if !found {
			h.t.Fatalf("expected auctioneer to receive output %x",
				output.PkScript)
		}
	}
	h.auctioneer.inputsReceived = nil
	h.auctioneer.outputsReceived = nil
}
func (h *testHarness) assertAccountModification(account *Account,
	inputs []*lnwallet.Utxo, outputs []*wire.TxOut,
	newAccountValue btcutil.Amount, accountInputIdx,
	accountOutputIdx uint32, broadcastHeight uint32) {
	h.t.Helper()
	h.assertAuctioneerReceived(inputs, outputs)
	spendTx := h.assertSpendTxBroadcast(
		account, inputs, outputs, &newAccountValue,
	)
	mods := []Modifier{
		ValueModifier(newAccountValue),
		StateModifier(StatePendingUpdate),
		OutPointModifier(wire.OutPoint{
			Hash:  spendTx.TxHash(),
			Index: accountOutputIdx,
		}),
		HeightHintModifier(broadcastHeight),
		IncrementBatchKey(),
		LatestTxModifier(spendTx),
	}
	for _, mod := range mods {
		mod(account)
	}
	h.assertAccountExists(account)
	h.notifier.spendChan <- &chainntnfs.SpendDetail{
		SpendingTx:        spendTx,
		SpenderInputIndex: accountInputIdx,
	}
	h.assertAccountExists(account)
	confHeight := broadcastHeight + 6
	h.notifier.confChan <- &chainntnfs.TxConfirmation{
		Tx:          spendTx,
		BlockHeight: confHeight,
	}
	StateModifier(StateOpen)(account)
	HeightHintModifier(confHeight)(account)
	h.assertAccountExists(account)
}
func (h *testHarness) restartManager() {
	h.t.Helper()
	h.manager.Stop()
	auctioneer := newMockAuctioneer()
	h.auctioneer = auctioneer
	h.manager = NewManager(&ManagerConfig{
		Store:         h.manager.cfg.Store,
		Auctioneer:    auctioneer,
		Wallet:        h.manager.cfg.Wallet,
		Signer:        h.manager.cfg.Signer,
		ChainNotifier: h.manager.cfg.ChainNotifier,
		TxSource:      h.manager.cfg.TxSource,
	})
	if err := h.manager.Start(); err != nil {
		h.t.Fatalf("unable to restart account manager: %v", err)
	}
}
func TestNewAccountHappyFlow(t *testing.T) {
	t.Parallel()
	const bestHeight = 100
	h := newTestHarness(t)
	h.start()
	defer h.stop()
	account := h.openAccount(
		maxAccountValue, bestHeight+maxAccountExpiry, bestHeight,
	)
	expr := defaultFeeExpr
	h.closeAccount(account, &expr, bestHeight+1)
}
func TestResumeAccountAfterRestart(t *testing.T) {
	t.Parallel()
	const (
		value      = maxAccountValue
		expiry     = maxAccountExpiry
		bestHeight = 100
	)
	h := newTestHarness(t)
	h.start()
	defer h.stop()
	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)
	go func() {
		_, _ = h.manager.InitAccount(
			context.Background(), value, 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,
	}
	h.assertAccountExists(account)
	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)
	h.restartManager()
	select {
	case accountTx := <-h.wallet.publishChan:
		if accountTx.TxHash() != tx.TxHash() {
			t.Fatalf("transaction mismatch after restart: "+
				"%v vs %v", 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):
	}
	account.State = StatePendingOpen
	account.LatestTx = tx
	account.OutPoint = wire.OutPoint{
		Hash:  tx.TxHash(),
		Index: 0,
	}
	h.assertAccountExists(account)
	confHeight := uint32(bestHeight + 6)
	h.notifier.confChan <- &chainntnfs.TxConfirmation{
		BlockHeight: confHeight,
	}
	account.State = StateOpen
	account.HeightHint = confHeight
	h.assertAccountExists(account)
}
func TestAccountClose(t *testing.T) {
	t.Parallel()
	const bestHeight = 100
	const highFeeRate = 10_000 * chainfee.FeePerKwFloor
	testCases := []struct {
		name    string
		feeExpr FeeExpr
		fee     btcutil.Amount
		valid   bool
	}{
		{
			name: "below P2WKH dust limit",
			feeExpr: &OutputWithFee{
				PkScript: p2wpkh,
				FeeRate:  highFeeRate,
			},
		},
		{
			name: "below P2SH dust limit",
			feeExpr: &OutputWithFee{
				PkScript: np2wpkh,
				FeeRate:  highFeeRate,
			},
		},
		{
			name: "below P2WSH dust limit",
			feeExpr: &OutputWithFee{
				PkScript: p2wsh,
				FeeRate:  highFeeRate,
			},
		},
		{
			name: "single wallet P2WKH output",
			feeExpr: &OutputWithFee{
				PkScript: nil,
				FeeRate:  chainfee.FeePerKwFloor,
			},
			fee:   btcutil.Amount(141),
			valid: true,
		},
		{
			name: "single external P2WKH output",
			feeExpr: &OutputWithFee{
				PkScript: p2wpkh,
				FeeRate:  2 * chainfee.FeePerKwFloor,
			},
			fee:   btcutil.Amount(282),
			valid: true,
		},
		{
			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),
			valid: true,
		},
	}
	for _, testCase := range testCases {
		testCase := testCase
		success := t.Run(testCase.name, func(t *testing.T) {
			h := newTestHarness(t)
			h.start()
			defer h.stop()
			account := h.openAccount(
				MinAccountValue, bestHeight+maxAccountExpiry,
				bestHeight,
			)
			_, err := h.manager.CloseAccount(
				context.Background(), account.TraderKey.PubKey,
				testCase.feeExpr, bestHeight,
			)
			if !testCase.valid {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			var externalOutputs []*wire.TxOut
			switch feeExpr := testCase.feeExpr.(type) {
			case *OutputWithFee:
				if feeExpr.PkScript != nil {
					output := &wire.TxOut{
						PkScript: feeExpr.PkScript,
						Value: int64(
							account.Value - testCase.fee,
						),
					}
					externalOutputs = append(
						externalOutputs, output,
					)
				}
			case *OutputsWithImplicitFee:
				externalOutputs = feeExpr.Outputs()
			default:
				t.Fatal("unhandled fee expr")
			}
			closeTx := h.assertSpendTxBroadcast(
				account, nil, externalOutputs, nil,
			)
			var outputTotal btcutil.Amount
			for _, output := range closeTx.TxOut {
				outputTotal += btcutil.Amount(output.Value)
			}
			fee := account.Value - outputTotal
			require.Equal(t, fee, testCase.fee)
		})
		if !success {
			return
		}
	}
}