decred.org/dcrdex@v1.0.5/client/asset/btc/simnet_test.go (about)

     1  //go:build harness
     2  
     3  package btc
     4  
     5  // Simnet tests expect the BTC test harness to be running.
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"errors"
    13  	"fmt"
    14  	"math/rand"
    15  	"os"
    16  	"os/exec"
    17  	"os/user"
    18  	"path/filepath"
    19  	"testing"
    20  	"time"
    21  
    22  	"decred.org/dcrdex/client/asset"
    23  	"decred.org/dcrdex/dex"
    24  	"decred.org/dcrdex/dex/config"
    25  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    26  	"github.com/btcsuite/btcd/btcutil"
    27  	"github.com/btcsuite/btcd/chaincfg"
    28  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    29  )
    30  
    31  const (
    32  	gammaSeed = "1285a47d6a59f9c548b2a72c2c34a2de97967bede3844090102bbba76707fe9d"
    33  )
    34  
    35  var (
    36  	tLogger   dex.Logger
    37  	tCtx      context.Context
    38  	tLotSize  uint64 = 1e7
    39  	tRateStep uint64 = 100
    40  	tBTC             = &dex.Asset{
    41  		ID:         0,
    42  		Symbol:     "btc",
    43  		Version:    version,
    44  		MaxFeeRate: 10,
    45  		SwapConf:   1,
    46  	}
    47  	walletPassword = []byte("abc")
    48  )
    49  
    50  func mineAlpha() error {
    51  	return exec.Command("tmux", "send-keys", "-t", "btc-harness:0", "./mine-alpha 1", "C-m").Run()
    52  }
    53  
    54  func mineBeta() error {
    55  	return exec.Command("tmux", "send-keys", "-t", "btc-harness:0", "./mine-beta 1", "C-m").Run()
    56  }
    57  
    58  func tBackend(t *testing.T, name string, isInternal bool, blkFunc func(string, error)) (*ExchangeWalletFullNode, *dex.ConnectionMaster) {
    59  	t.Helper()
    60  	user, err := user.Current()
    61  	if err != nil {
    62  		t.Fatalf("error getting current user: %v", err)
    63  	}
    64  	settings := make(map[string]string)
    65  	if !isInternal {
    66  		cfgPath := filepath.Join(user.HomeDir, "dextest", "btc", name, name+".conf")
    67  		settings, err = config.Parse(cfgPath)
    68  		if err != nil {
    69  			t.Fatalf("error reading config options: %v", err)
    70  		}
    71  	}
    72  
    73  	noteChan := make(chan asset.WalletNotification, 128)
    74  	go func() {
    75  		for {
    76  			select {
    77  			case <-noteChan:
    78  			case <-tCtx.Done():
    79  				return
    80  			}
    81  		}
    82  	}()
    83  
    84  	walletCfg := &asset.WalletConfig{
    85  		Settings: settings,
    86  		Emit:     asset.NewWalletEmitter(make(chan asset.WalletNotification, 128), 0, tLogger),
    87  		PeersChange: func(num uint32, err error) {
    88  			t.Logf("peer count = %d, err = %v", num, err)
    89  		},
    90  	}
    91  	if isInternal {
    92  		seed, err := hex.DecodeString(gammaSeed)
    93  		if err != nil {
    94  			t.Fatal(err)
    95  		}
    96  		dataDir := t.TempDir()
    97  		regtestDir := filepath.Join(dataDir, chaincfg.RegressionNetParams.Name)
    98  		err = createSPVWallet(walletPassword, seed, defaultWalletBirthday, regtestDir, tLogger, 0, 0, &chaincfg.RegressionNetParams)
    99  		if err != nil {
   100  			t.Fatal(err)
   101  		}
   102  		walletCfg.Type = walletTypeSPV
   103  		walletCfg.DataDir = dataDir
   104  	}
   105  	var backend asset.Wallet
   106  	backend, err = NewWallet(walletCfg, tLogger, dex.Simnet)
   107  	if err != nil {
   108  		t.Fatalf("error creating backend: %v", err)
   109  	}
   110  	cm := dex.NewConnectionMaster(backend)
   111  	err = cm.Connect(tCtx)
   112  	if err != nil {
   113  		t.Fatalf("error connecting backend: %v", err)
   114  	}
   115  
   116  	if isInternal {
   117  		i := 0
   118  		for {
   119  			synced, _, err := backend.SyncStatus()
   120  			if err != nil {
   121  				t.Fatal(err)
   122  			}
   123  			if synced {
   124  				break
   125  			}
   126  			if i == 5 {
   127  				t.Fatal("spv wallet not synced after 5 seconds")
   128  			}
   129  			i++
   130  			time.Sleep(time.Second)
   131  		}
   132  
   133  		spv := backend.(*ExchangeWalletSPV)
   134  		fullNode := &ExchangeWalletFullNode{
   135  			intermediaryWallet: spv.intermediaryWallet,
   136  			authAddOn:          spv.authAddOn,
   137  		}
   138  
   139  		return fullNode, cm
   140  	}
   141  
   142  	accelerator := backend.(*ExchangeWalletAccelerator)
   143  	return accelerator.ExchangeWalletFullNode, cm
   144  }
   145  
   146  type testRig struct {
   147  	backends          map[string]*ExchangeWalletFullNode
   148  	connectionMasters map[string]*dex.ConnectionMaster
   149  }
   150  
   151  func newTestRig(t *testing.T, blkFunc func(string, error)) *testRig {
   152  	t.Helper()
   153  	rig := &testRig{
   154  		backends:          make(map[string]*ExchangeWalletFullNode),
   155  		connectionMasters: make(map[string]*dex.ConnectionMaster, 3),
   156  	}
   157  	rig.backends["alpha"], rig.connectionMasters["alpha"] = tBackend(t, "alpha", false, blkFunc)
   158  	rig.backends["beta"], rig.connectionMasters["beta"] = tBackend(t, "beta", false, blkFunc)
   159  	rig.backends["gamma"], rig.connectionMasters["gamma"] = tBackend(t, "gamma", true, blkFunc)
   160  
   161  	gammaAddr, err := rig.backends["gamma"].DepositAddress()
   162  	if err != nil {
   163  		t.Fatalf("error getting gamma deposit address: %v", err)
   164  	}
   165  
   166  	_, err = rig.alpha().Send(gammaAddr, toSatoshi(100), 10)
   167  	if err != nil {
   168  		t.Fatalf("error sending to gamma: %v", err)
   169  	}
   170  
   171  	mineAlpha()
   172  
   173  	return rig
   174  }
   175  
   176  func (rig *testRig) alpha() *ExchangeWalletFullNode {
   177  	return rig.backends["alpha"]
   178  }
   179  func (rig *testRig) beta() *ExchangeWalletFullNode {
   180  	return rig.backends["beta"]
   181  }
   182  func (rig *testRig) gamma() *ExchangeWalletFullNode {
   183  	return rig.backends["gamma"]
   184  }
   185  func (rig *testRig) close(t *testing.T) {
   186  	t.Helper()
   187  	for name, cm := range rig.connectionMasters {
   188  		closed := make(chan struct{})
   189  		go func() {
   190  			cm.Disconnect()
   191  			close(closed)
   192  		}()
   193  		select {
   194  		case <-closed:
   195  		case <-time.NewTimer(60 * time.Second).C:
   196  			t.Fatalf("failed to disconnect from %s", name)
   197  		}
   198  	}
   199  }
   200  
   201  func randBytes(l int) []byte {
   202  	b := make([]byte, l)
   203  	rand.Read(b)
   204  	return b
   205  }
   206  
   207  func waitNetwork() {
   208  	time.Sleep(time.Second * 3 / 2)
   209  }
   210  
   211  func TestMain(m *testing.M) {
   212  	tLogger = dex.StdOutLogger("TEST", dex.LevelTrace)
   213  	var shutdown func()
   214  	tCtx, shutdown = context.WithCancel(context.Background())
   215  	doIt := func() int {
   216  		defer shutdown()
   217  		return m.Run()
   218  	}
   219  	os.Exit(doIt())
   220  }
   221  
   222  func TestMakeBondTx(t *testing.T) {
   223  	rig := newTestRig(t, func(name string, err error) {
   224  		tLogger.Infof("%s has reported a new block, error = %v", name, err)
   225  	})
   226  	defer rig.close(t)
   227  
   228  	// Get a private key for the bond script. This would come from the client's
   229  	// HD key chain.
   230  	priv, err := secp256k1.GeneratePrivateKey()
   231  	if err != nil {
   232  		t.Fatal(err)
   233  	}
   234  	pubkey := priv.PubKey()
   235  
   236  	acctID := randBytes(32)
   237  	fee := uint64(10_2030_4050) //  ~10.2 DCR
   238  	const bondVer = 0
   239  
   240  	wallet := rig.alpha()
   241  
   242  	// Unlock the wallet to sign the tx and get keys.
   243  	err = wallet.Unlock([]byte("abc"))
   244  	if err != nil {
   245  		t.Fatalf("error unlocking beta wallet: %v", err)
   246  	}
   247  
   248  	lockTime := time.Now().Add(10 * time.Second)
   249  	bond, _, err := wallet.MakeBondTx(bondVer, fee, 10, lockTime, priv, acctID)
   250  	if err != nil {
   251  		t.Fatal(err)
   252  	}
   253  	coinhash, _, err := decodeCoinID(bond.CoinID)
   254  	if err != nil {
   255  		t.Fatalf("decodeCoinID: %v", err)
   256  	}
   257  	t.Logf("bond txid %v\n", coinhash)
   258  	t.Logf("signed tx: %x\n", bond.SignedTx)
   259  	t.Logf("unsigned tx: %x\n", bond.UnsignedTx)
   260  	t.Logf("bond script: %x\n", bond.Data)
   261  	t.Logf("redeem tx: %x\n", bond.RedeemTx)
   262  	_, err = msgTxFromBytes(bond.SignedTx)
   263  	if err != nil {
   264  		t.Fatalf("invalid bond tx: %v", err)
   265  	}
   266  
   267  	pkh := btcutil.Hash160(pubkey.SerializeCompressed())
   268  
   269  	lockTimeUint, pkhPush, err := dexbtc.ExtractBondDetailsV0(0, bond.Data)
   270  	if err != nil {
   271  		t.Fatalf("ExtractBondDetailsV0: %v", err)
   272  	}
   273  	if !bytes.Equal(pkh, pkhPush) {
   274  		t.Fatalf("mismatching pubkeyhash in bond script and signature (%x != %x)", pkh, pkhPush)
   275  	}
   276  
   277  	if lockTime.Unix() != int64(lockTimeUint) {
   278  		t.Fatalf("mismatching locktimes (%d != %d)", lockTime.Unix(), lockTimeUint)
   279  	}
   280  	lockTimePush := time.Unix(int64(lockTimeUint), 0)
   281  	t.Logf("lock time in bond script: %v", lockTimePush)
   282  
   283  	sendBondTx, err := wallet.SendTransaction(bond.SignedTx)
   284  	if err != nil {
   285  		t.Fatalf("RefundBond: %v", err)
   286  	}
   287  	sendBondTxid, _, err := decodeCoinID(sendBondTx)
   288  	if err != nil {
   289  		t.Fatalf("decodeCoinID: %v", err)
   290  	}
   291  	t.Logf("sendBondTxid: %v\n", sendBondTxid)
   292  
   293  	waitNetwork() // wait for alpha to see the txn
   294  	mineAlpha()
   295  	waitNetwork() // wait for beta to see the new block (bond must be mined for RefundBond)
   296  
   297  	var expired bool
   298  	for !expired {
   299  		expired, err = wallet.LockTimeExpired(tCtx, lockTime)
   300  		if err != nil {
   301  			t.Fatalf("LocktimeExpired: %v", err)
   302  		}
   303  		if expired {
   304  			break
   305  		}
   306  		fmt.Println("bond still not expired")
   307  		time.Sleep(15 * time.Second)
   308  	}
   309  
   310  	refundCoin, err := wallet.RefundBond(context.Background(), bondVer, bond.CoinID,
   311  		bond.Data, bond.Amount, priv)
   312  	if err != nil {
   313  		t.Fatalf("RefundBond: %v", err)
   314  	}
   315  	t.Logf("refundCoin: %v\n", refundCoin)
   316  }
   317  
   318  func TestExternalFeeRate(t *testing.T) {
   319  	fetchRateWithTimeout(t, dex.Mainnet)
   320  	fetchRateWithTimeout(t, dex.Testnet)
   321  }
   322  
   323  func fetchRateWithTimeout(t *testing.T, net dex.Network) {
   324  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   325  	defer cancel()
   326  	feeRate, err := externalFeeRate(ctx, net)
   327  	if err != nil {
   328  		t.Fatalf("error fetching %s fees: %v", net, err)
   329  	}
   330  	fmt.Printf("##### Fee rate fetched for %s! %d Sats/vB \n", net, feeRate)
   331  }
   332  
   333  func TestWalletTxBalanceSync(t *testing.T) {
   334  	rig := newTestRig(t, func(name string, _ error) {
   335  		tLogger.Infof("%s has reported a new block", name)
   336  	})
   337  	defer rig.close(t)
   338  
   339  	beta := rig.beta()
   340  	gamma := rig.gamma()
   341  
   342  	err := beta.Unlock(walletPassword)
   343  	if err != nil {
   344  		t.Fatalf("error unlocking beta wallet: %v", err)
   345  	}
   346  	err = gamma.Unlock(walletPassword)
   347  	if err != nil {
   348  		t.Fatalf("error unlocking gamma wallet: %v", err)
   349  	}
   350  
   351  	t.Run("rpc", func(t *testing.T) {
   352  		testWalletTxBalanceSync(t, gamma, beta)
   353  	})
   354  
   355  	t.Run("spv", func(t *testing.T) {
   356  		testWalletTxBalanceSync(t, beta, gamma)
   357  	})
   358  }
   359  
   360  // This tests that redemptions becoming available in the balance and the
   361  // asset.WalletTransaction returned from WalletTransaction becomes confirmed
   362  // at the same time.
   363  func testWalletTxBalanceSync(t *testing.T, fromWallet, toWallet *ExchangeWalletFullNode) {
   364  	receivingAddr, err := toWallet.DepositAddress()
   365  	if err != nil {
   366  		t.Fatalf("error getting deposit address: %v", err)
   367  	}
   368  
   369  	order := &asset.Order{
   370  		Value:         toSatoshi(1),
   371  		FeeSuggestion: 10,
   372  		MaxSwapCount:  1,
   373  		MaxFeeRate:    20,
   374  	}
   375  	coins, _, _, err := fromWallet.FundOrder(order)
   376  	if err != nil {
   377  		t.Fatalf("error funding order: %v", err)
   378  	}
   379  
   380  	secret := randBytes(32)
   381  	secretHash := sha256.Sum256(secret)
   382  	contract := &asset.Contract{
   383  		Address:    receivingAddr,
   384  		Value:      order.Value,
   385  		SecretHash: secretHash[:],
   386  		LockTime:   uint64(time.Now().Add(-1 * time.Hour).Unix()),
   387  	}
   388  	swaps := &asset.Swaps{
   389  		Inputs:  coins,
   390  		FeeRate: 10,
   391  		Contracts: []*asset.Contract{
   392  			contract,
   393  		},
   394  	}
   395  	receipts, _, _, err := fromWallet.Swap(swaps)
   396  	if err != nil {
   397  		t.Fatalf("error swapping: %v", err)
   398  	}
   399  	receipt := receipts[0]
   400  
   401  	var auditInfo *asset.AuditInfo
   402  	for i := 0; i < 10; i++ {
   403  		auditInfo, err = toWallet.AuditContract(receipt.Coin().ID(), receipt.Contract(), []byte{}, false)
   404  		if err == nil {
   405  			break
   406  		}
   407  
   408  		time.Sleep(5 * time.Second)
   409  	}
   410  	if err != nil {
   411  		t.Fatalf("error auditing contract: %v", err)
   412  	}
   413  
   414  	balance, err := toWallet.Balance()
   415  	if err != nil {
   416  		t.Fatalf("error getting balance: %v", err)
   417  	}
   418  	_, out, _, err := toWallet.Redeem(&asset.RedeemForm{
   419  		Redemptions: []*asset.Redemption{
   420  			{
   421  				Spends: auditInfo,
   422  				Secret: secret,
   423  			},
   424  		},
   425  		FeeSuggestion: 10,
   426  	})
   427  	if err != nil {
   428  		t.Fatalf("error redeeming: %v", err)
   429  	}
   430  
   431  	confirmSync := func(originalBalance uint64, coinID []byte) {
   432  		t.Helper()
   433  
   434  		for i := 0; i < 10; i++ {
   435  			balance, err := toWallet.Balance()
   436  			if err != nil {
   437  				t.Fatalf("error getting balance: %v", err)
   438  			}
   439  			balDiff := balance.Available - originalBalance
   440  
   441  			var confirmed bool
   442  			var txDiff uint64
   443  			if wt, err := toWallet.WalletTransaction(context.Background(), hex.EncodeToString(coinID)); err == nil {
   444  				confirmed = wt.Confirmed
   445  				txDiff = wt.Amount - wt.Fees
   446  			} else if !errors.Is(err, asset.CoinNotFoundError) {
   447  				t.Fatal(err)
   448  			}
   449  
   450  			balanceChanged := balance.Available != originalBalance
   451  			if confirmed != balanceChanged {
   452  				if balanceChanged && !confirmed {
   453  					for j := 0; j < 20; j++ {
   454  						if wt, err := toWallet.WalletTransaction(context.Background(), hex.EncodeToString(coinID)); err == nil && wt.Confirmed {
   455  							t.Fatalf("took %d seconds after balance changed before tx was confirmed", j/2)
   456  						} else if !errors.Is(err, asset.CoinNotFoundError) {
   457  							t.Fatal(err)
   458  						}
   459  						time.Sleep(500 * time.Millisecond)
   460  					}
   461  				}
   462  				t.Fatalf("confirmed status does not match balance change. confirmed = %v, balance changed = %d", confirmed, balDiff)
   463  			}
   464  
   465  			if confirmed {
   466  				if balDiff != txDiff {
   467  					t.Fatalf("balance and transaction diffs do not match. balance diff = %d, tx diff = %d", balDiff, txDiff)
   468  				}
   469  				return
   470  			}
   471  
   472  			time.Sleep(5 * time.Second)
   473  		}
   474  
   475  		t.Fatal("timed out waiting for balance and transaction to sync")
   476  	}
   477  
   478  	confirmSync(balance.Available, out.ID())
   479  
   480  	balance, err = toWallet.Balance()
   481  	if err != nil {
   482  		t.Fatalf("error getting balance: %v", err)
   483  	}
   484  
   485  	receivingAddr, err = toWallet.DepositAddress()
   486  	if err != nil {
   487  		t.Fatalf("error getting deposit address: %v", err)
   488  	}
   489  
   490  	coin, err := fromWallet.Send(receivingAddr, toSatoshi(1), 10)
   491  	if err != nil {
   492  		t.Fatalf("error sending: %v", err)
   493  	}
   494  
   495  	confirmSync(balance.Available, coin.ID())
   496  }