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 }