decred.org/dcrdex@v1.0.5/client/asset/zec/regnet_test.go (about)

     1  //go:build harness
     2  
     3  package zec
     4  
     5  // Regnet tests expect the ZEC test harness to be running.
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"os/user"
    14  	"path/filepath"
    15  	"testing"
    16  	"time"
    17  
    18  	"decred.org/dcrdex/client/asset"
    19  	"decred.org/dcrdex/client/asset/btc"
    20  	"decred.org/dcrdex/client/asset/btc/livetest"
    21  	"decred.org/dcrdex/dex"
    22  	"decred.org/dcrdex/dex/config"
    23  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    24  	dexzec "decred.org/dcrdex/dex/networks/zec"
    25  	"github.com/btcsuite/btcd/btcutil"
    26  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    27  	"github.com/decred/dcrd/rpcclient/v8"
    28  )
    29  
    30  const (
    31  	mainnetNU5ActivationHeight        = 1687104
    32  	testnetNU5ActivationHeight        = 1842420
    33  	testnetSaplingActivationHeight    = 280000
    34  	testnetOverwinterActivationHeight = 207500
    35  )
    36  
    37  var tZEC = &dex.Asset{
    38  	ID:       0,
    39  	Symbol:   "zec",
    40  	Version:  version,
    41  	SwapConf: 1,
    42  }
    43  
    44  func TestWallet(t *testing.T) {
    45  	livetest.Run(t, &livetest.Config{
    46  		NewWallet: NewWallet,
    47  		LotSize:   tLotSize,
    48  		Asset:     tZEC,
    49  		FirstWallet: &livetest.WalletName{
    50  			Node:     "alpha",
    51  			Filename: "alpha.conf",
    52  		},
    53  		SecondWallet: &livetest.WalletName{
    54  			Node:     "beta",
    55  			Filename: "beta.conf",
    56  		},
    57  	})
    58  }
    59  
    60  // TestDeserializeTestnet must be run against a full RPC node.
    61  func TestDeserializeTestnetBlocks(t *testing.T) {
    62  	testDeserializeBlocks(t, "18232", testnetNU5ActivationHeight, testnetSaplingActivationHeight, testnetOverwinterActivationHeight)
    63  }
    64  
    65  func TestDeserializeMainnetBlocks(t *testing.T) {
    66  	testDeserializeBlocks(t, "8232")
    67  }
    68  
    69  func testDeserializeBlocks(t *testing.T, port string, upgradeHeights ...int64) {
    70  	cfg := struct {
    71  		RPCUser string `ini:"rpcuser"`
    72  		RPCPass string `ini:"rpcpassword"`
    73  	}{}
    74  
    75  	usr, _ := user.Current()
    76  	if err := config.ParseInto(filepath.Join(usr.HomeDir, ".zcash", "zcash.conf"), &cfg); err != nil {
    77  		t.Fatalf("config.Parse error: %v", err)
    78  	}
    79  
    80  	cl, err := rpcclient.New(&rpcclient.ConnConfig{
    81  		HTTPPostMode: true,
    82  		DisableTLS:   true,
    83  		Host:         "localhost:" + port,
    84  		User:         cfg.RPCUser,
    85  		Pass:         cfg.RPCPass,
    86  	}, nil)
    87  	if err != nil {
    88  		t.Fatalf("error creating client: %v", err)
    89  	}
    90  
    91  	ctx, cancel := context.WithCancel(context.Background())
    92  	defer cancel()
    93  
    94  	tipHash, err := cl.GetBestBlockHash(ctx)
    95  	if err != nil {
    96  		t.Fatalf("GetBestBlockHash error: %v", err)
    97  	}
    98  
    99  	mustMarshal := func(thing any) json.RawMessage {
   100  		b, err := json.Marshal(thing)
   101  		if err != nil {
   102  			t.Fatalf("Failed to marshal %T thing: %v", thing, err)
   103  		}
   104  		return b
   105  	}
   106  
   107  	blockBytes := func(hashStr string) (blockB dex.Bytes) {
   108  		raw, err := cl.RawRequest(ctx, "getblock", []json.RawMessage{mustMarshal(hashStr), mustMarshal(0)})
   109  		if err != nil {
   110  			t.Fatalf("Failed to fetch block hash for %s: %v", hashStr, err)
   111  		}
   112  		if err := json.Unmarshal(raw, &blockB); err != nil {
   113  			t.Fatalf("Error unmarshaling block bytes for %s: %v", hashStr, err)
   114  		}
   115  		return
   116  	}
   117  
   118  	nBlocksFromHash := func(hashStr string, n int) {
   119  		for i := 0; i < n; i++ {
   120  			zecBlock, err := dexzec.DeserializeBlock(blockBytes(hashStr))
   121  			if err != nil {
   122  				t.Fatalf("Error deserializing %s: %v", hashStr, err)
   123  			}
   124  
   125  			for _, tx := range zecBlock.Transactions {
   126  				switch {
   127  				case tx.NActionsOrchard > 0:
   128  					fmt.Printf("Orchard transaction with nActionsOrchard = %d \n", tx.NActionsOrchard)
   129  					// case tx.NActionsOrchard > 0 && tx.NOutputsSapling > 0:
   130  					// 	fmt.Printf("orchard + sapling shielded tx: %s:%d \n", hashStr, i)
   131  					// 	case tx.NActionsOrchard > 0:
   132  					// 		fmt.Printf("orchard shielded tx: %s:%d \n", hashStr, i)
   133  					// 	case tx.NOutputsSapling > 0 || tx.NSpendsSapling > 0:
   134  					// 		fmt.Printf("sapling shielded tx: %s:%d \n", hashStr, i)
   135  					// 	case tx.NJoinSplit > 0:
   136  					// 		fmt.Printf("joinsplit tx: %s:%d \n", hashStr, i)
   137  					// 	default:
   138  					// 		if i > 0 {
   139  					// 			fmt.Printf("unshielded tx: %s:%d \n", hashStr, i)
   140  					// 		}
   141  				}
   142  			}
   143  
   144  			hashStr = zecBlock.Header.PrevBlock.String()
   145  		}
   146  	}
   147  
   148  	// Test version 5 blocks.
   149  	fmt.Println("Testing version 5 blocks")
   150  	nBlocksFromHash(tipHash.String(), 1000)
   151  
   152  	ver := 4
   153  	for _, upgradeHeight := range upgradeHeights {
   154  		lastVerBlock, err := cl.GetBlockHash(ctx, upgradeHeight-1)
   155  		if err != nil {
   156  			t.Fatalf("GetBlockHash(%d) error: %v", upgradeHeight-1, err)
   157  		}
   158  		fmt.Printf("Testing version %d blocks \n", ver)
   159  		nBlocksFromHash(lastVerBlock.String(), 1000)
   160  		ver--
   161  	}
   162  }
   163  
   164  func TestMultiSplit(t *testing.T) {
   165  	log := dex.StdOutLogger("T", dex.LevelTrace)
   166  	c := make(chan asset.WalletNotification, 16)
   167  	tmpDir, _ := os.MkdirTemp("", "")
   168  	defer os.RemoveAll(tmpDir)
   169  	walletCfg := &asset.WalletConfig{
   170  		Type: walletTypeRPC,
   171  		Settings: map[string]string{
   172  			"txsplit":     "true",
   173  			"rpcuser":     "user",
   174  			"rpcpassword": "pass",
   175  			"regtest":     "1",
   176  			"rpcport":     "33770",
   177  		},
   178  		Emit: asset.NewWalletEmitter(c, BipID, log),
   179  		PeersChange: func(u uint32, err error) {
   180  			log.Info("peers changed", u, err)
   181  		},
   182  		DataDir: tmpDir,
   183  	}
   184  	wi, err := NewWallet(walletCfg, log, dex.Simnet)
   185  	if err != nil {
   186  		t.Fatalf("Error making new wallet: %v", err)
   187  	}
   188  	w := wi.(*zecWallet)
   189  
   190  	ctx, cancel := context.WithCancel(context.Background())
   191  	defer cancel()
   192  
   193  	go func() {
   194  		for {
   195  			select {
   196  			case n := <-c:
   197  				log.Infof("wallet note emitted: %+v", n)
   198  			case <-ctx.Done():
   199  				return
   200  			}
   201  		}
   202  	}()
   203  
   204  	cm := dex.NewConnectionMaster(w)
   205  	if err := cm.ConnectOnce(ctx); err != nil {
   206  		t.Fatalf("Error connecting wallet: %v", err)
   207  	}
   208  
   209  	// Unlock all transparent outputs.
   210  	if ops, err := listLockUnspent(w, log); err != nil {
   211  		t.Fatalf("Error listing unspent outputs: %v", err)
   212  	} else if len(ops) > 0 {
   213  		coins := make([]*btc.Output, len(ops))
   214  		for i, op := range ops {
   215  			txHash, _ := chainhash.NewHashFromStr(op.TxID)
   216  			coins[i] = btc.NewOutput(txHash, op.Vout, 0)
   217  		}
   218  		if err := lockUnspent(w, true, coins); err != nil {
   219  			t.Fatalf("Error unlocking coins")
   220  		}
   221  		log.Info("Unlocked %d transparent outputs", len(ops))
   222  	}
   223  
   224  	bals, err := w.balances()
   225  	if err != nil {
   226  		t.Fatalf("Error getting wallet balance: %v", err)
   227  	}
   228  
   229  	var v0, v1 uint64 = 1e8, 2e8
   230  	orderReq0, orderReq1 := dexzec.RequiredOrderFunds(v0, 1, dexbtc.RedeemP2PKHInputSize, 1), dexzec.RequiredOrderFunds(v1, 1, dexbtc.RedeemP2PKHInputSize, 2)
   231  
   232  	tAddr := func() string {
   233  		addr, err := transparentAddressString(w)
   234  		if err != nil {
   235  			t.Fatalf("Error getting transparent address: %v", err)
   236  		}
   237  		return addr
   238  	}
   239  
   240  	// Send everything to a transparent address.
   241  	unspents, err := listUnspent(w)
   242  	if err != nil {
   243  		t.Fatalf("listUnspent error: %v", err)
   244  	}
   245  	fees := dexzec.TxFeesZIP317(1+(dexbtc.RedeemP2PKHInputSize*uint64(len(unspents))), 1+(3*dexbtc.P2PKHOutputSize), 0, 0, 0, uint64(bals.orchard.noteCount))
   246  	netBal := bals.available() - fees
   247  	changeVal := netBal - orderReq0 - orderReq1
   248  
   249  	recips := []*zSendManyRecipient{
   250  		{Address: tAddr(), Amount: btcutil.Amount(orderReq0).ToBTC()},
   251  		{Address: tAddr(), Amount: btcutil.Amount(orderReq1).ToBTC()},
   252  		{Address: tAddr(), Amount: btcutil.Amount(changeVal).ToBTC()},
   253  	}
   254  
   255  	txHash, err := w.sendManyShielded(recips)
   256  	if err != nil {
   257  		t.Fatalf("sendManyShielded error: %v", err)
   258  	}
   259  
   260  	log.Infof("z_sendmany successful. txid = %s", txHash)
   261  
   262  	// Could be orchard notes. Mature them.
   263  	if err := mineAlpha(ctx); err != nil {
   264  		t.Fatalf("Error mining a block: %v", err)
   265  	}
   266  
   267  	// All funds should be transparent now.
   268  	multiFund := &asset.MultiOrder{
   269  		Version: version,
   270  		Values: []*asset.MultiOrderValue{
   271  			{Value: v0, MaxSwapCount: 1},
   272  			{Value: v1, MaxSwapCount: 2},
   273  		},
   274  		Options: map[string]string{"multisplit": "true"},
   275  	}
   276  
   277  	checkFundMulti := func(expSplit bool) {
   278  		t.Helper()
   279  		coinSets, _, fundingFees, err := w.FundMultiOrder(multiFund, 0)
   280  		if err != nil {
   281  			t.Fatalf("FundMultiOrder error: %v", err)
   282  		}
   283  
   284  		if len(coinSets) != 2 || len(coinSets[0]) != 1 || len(coinSets[1]) != 1 {
   285  			t.Fatalf("Expected 2 coin sets of len 1 each, got %+v", coinSets)
   286  		}
   287  
   288  		coin0, coin1 := coinSets[0][0], coinSets[1][0]
   289  
   290  		if err := w.cm.ReturnCoins(asset.Coins{coin0, coin1}); err != nil {
   291  			t.Fatalf("ReturnCoins error: %v", err)
   292  		}
   293  
   294  		if coin0.Value() != orderReq0 {
   295  			t.Fatalf("coin 0 had insufficient value: %d < %d", coin0.Value(), orderReq0)
   296  		}
   297  
   298  		if coin1.Value() < orderReq1 {
   299  			t.Fatalf("coin 1 had insufficient value: %d < %d", coin1.Value(), orderReq1)
   300  		}
   301  
   302  		// Should be no split tx.
   303  		split := fundingFees > 0
   304  		if split != expSplit {
   305  			t.Fatalf("Expected split %t, got %t", expSplit, split)
   306  		}
   307  
   308  		log.Infof("Coin 0: %s", coin0)
   309  		log.Infof("Coin 1: %s", coin1)
   310  		log.Infof("Funding fees: %d", fundingFees)
   311  	}
   312  
   313  	checkFundMulti(false) // no split
   314  
   315  	// Could be orchard notes. Mature them.
   316  	if err := mineAlpha(ctx); err != nil {
   317  		t.Fatalf("Error mining a block: %v", err)
   318  	}
   319  
   320  	// Send everything to a single transparent address to test for a
   321  	// fully-transparent split tx.
   322  	splitFees := dexzec.TxFeesZIP317(1+(3*dexbtc.RedeemP2PKHInputSize), 1+dexbtc.P2PKHOutputSize, 0, 0, 0, 0)
   323  	netBal -= splitFees
   324  	txHash, err = w.sendOneShielded(ctx, tAddr(), netBal, NoPrivacy)
   325  	if err != nil {
   326  		t.Fatalf("sendOneShielded(transparent) error: %v", err)
   327  	}
   328  	log.Infof("Sent all to transparent with tx %s", txHash)
   329  
   330  	// Could be orchard notes. Mature them.
   331  	if err := mineAlpha(ctx); err != nil {
   332  		t.Fatalf("Error mining a block: %v", err)
   333  	}
   334  
   335  	checkFundMulti(true) // fully-transparent split
   336  
   337  	// Could be orchard notes. Mature them.
   338  	if err := mineAlpha(ctx); err != nil {
   339  		t.Fatalf("Error mining a block: %v", err)
   340  	}
   341  
   342  	// Send everything to a shielded address.
   343  	addrRes, err := zGetAddressForAccount(w, shieldedAcctNumber, []string{transparentAddressType, orchardAddressType})
   344  	if err != nil {
   345  		t.Fatalf("zGetAddressForAccount error: %v", err)
   346  	}
   347  	receivers, err := zGetUnifiedReceivers(w, addrRes.Address)
   348  	if err != nil {
   349  		t.Fatalf("zGetUnifiedReceivers error: %v", err)
   350  	}
   351  	orchardAddr := receivers.Orchard
   352  
   353  	bals, err = w.balances()
   354  	if err != nil {
   355  		t.Fatalf("Error getting wallet balance: %v", err)
   356  	}
   357  	unspents, err = listUnspent(w)
   358  	if err != nil {
   359  		t.Fatalf("listUnspent error: %v", err)
   360  	}
   361  
   362  	splitFees = dexzec.TxFeesZIP317(1+(dexbtc.RedeemP2PKHInputSize*uint64(len(unspents))), 1, 0, 0, 0, uint64(bals.orchard.noteCount))
   363  	netBal = bals.available() - splitFees
   364  
   365  	txHash, err = w.sendOneShielded(ctx, orchardAddr, netBal, NoPrivacy)
   366  	if err != nil {
   367  		t.Fatalf("sendManyShielded error: %v", err)
   368  	}
   369  	log.Infof("sendOneShielded(shielded) successful. txid = %s", txHash)
   370  
   371  	// Could be orchard notes. Mature them.
   372  	if err := mineAlpha(ctx); err != nil {
   373  		t.Fatalf("Error mining a block: %v", err)
   374  	}
   375  
   376  	checkFundMulti(true) // shielded split
   377  
   378  	cancel()
   379  	cm.Wait()
   380  }
   381  
   382  func mineAlpha(ctx context.Context) error {
   383  	// Wait for txs to propagate
   384  	select {
   385  	case <-time.After(time.Second * 5):
   386  	case <-ctx.Done():
   387  		return ctx.Err()
   388  	}
   389  	// Mine
   390  	if err := exec.Command("tmux", "send-keys", "-t", "zec-harness:4", "./mine-alpha 1", "C-m").Run(); err != nil {
   391  		return err
   392  	}
   393  	// Wait for blocks to propagate
   394  	select {
   395  	case <-time.After(time.Second * 5):
   396  	case <-ctx.Done():
   397  		return ctx.Err()
   398  	}
   399  	return nil
   400  }