decred.org/dcrdex@v1.0.3/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 }