diff --git a/assets/assets.go b/assets/assets.go index 10f1892..f3fed91 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -2,13 +2,15 @@ package assets import ( "fmt" + "math" "math/big" "github.com/bisoncraft/utxowallet/netparams" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" ) -func NetParams(chain, net string) (p *netparams.ChainParams, _ error) { +func NetParams(chain, net string) (p *netparams.ChainParams, err error) { switch chain { case "btc": p = BTCParams[net] @@ -18,6 +20,9 @@ func NetParams(chain, net string) (p *netparams.ChainParams, _ error) { if p == nil { return nil, fmt.Errorf("no net params for chain %s, network %s", chain, net) } + registerParams := *p.BTCDParams() // populate the internal btcParams field + registerParams.Net = math.MaxUint32 + err = chaincfg.Register(®isterParams) return } diff --git a/assets/assets_test.go b/assets/assets_test.go new file mode 100644 index 0000000..ff10cee --- /dev/null +++ b/assets/assets_test.go @@ -0,0 +1,23 @@ +package assets + +import "testing" + +func TestParams(t *testing.T) { + for chain, nets := range map[string][]string{ + "btc": {"mainnet", "testnet", "simnet"}, + "ltc": {"mainnet", "testnet", "simnet"}, + } { + for _, net := range nets { + t.Run(chain+"."+net, func(t *testing.T) { + chainParams, _ := NetParams(chain, net) + h := chainParams.GenesisBlock.BlockHash() + if h != *chainParams.GenesisHash { + t.Fatalf("Genesis hash mismatch") + } + if chainParams.Chain != chain { + t.Fatalf("Wrong chain. %s != %s", chainParams.Chain, chain) + } + }) + } + } +} diff --git a/assets/btc.go b/assets/btc.go index dbccc2f..5aaa082 100644 --- a/assets/btc.go +++ b/assets/btc.go @@ -22,6 +22,7 @@ var genesisMerkleRoot = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet var BTCParams = map[string]*netparams.ChainParams{ "mainnet": { + Chain: "btc", Name: "mainnet", Net: wire.MainNet, DefaultPort: "8333", @@ -95,9 +96,12 @@ var BTCParams = map[string]*netparams.ChainParams{ 0x68, 0xd6, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, }), PowLimit: new(big.Int).Sub(new(big.Int).Lsh(bigOne, 224), bigOne), + PowLimitBits: 0x1d00ffff, TargetTimespan: time.Hour * 24 * 14, // 14 days TargetTimePerBlock: time.Minute * 10, // 10 minutes RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: false, + MinDiffReductionTime: 0, // Checkpoints ordered from oldest to newest. Checkpoints: []chaincfg.Checkpoint{ @@ -144,14 +148,17 @@ var BTCParams = map[string]*netparams.ChainParams{ WitnessPubKeyHashAddrID: 0x06, // starts with p2 WitnessScriptHashAddrID: 0x0A, // starts with 7Xh // BIP32 hierarchical deterministic extended key magics - HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, // starts with xprv - HDPublicKeyID: [4]byte{0x04, 0x88, 0xb2, 0x1e}, // starts with xpub - HDCoinType: 0, - BIP0034Height: 227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8 - BIP0065Height: 388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0 - BIP0066Height: 363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931 + HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x88, 0xb2, 0x1e}, // starts with xpub + HDCoinType: 0, + BIP0034Height: 227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8 + BIP0065Height: 388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0 + BIP0066Height: 363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931 + CoinbaseMaturity: 100, + MaxSatoshi: btcutil.MaxSatoshi, }, "testnet": { + Chain: "btc", Name: "testnet3", Net: wire.TestNet3, DefaultPort: "18333", @@ -219,9 +226,12 @@ var BTCParams = map[string]*netparams.ChainParams{ 0x01, 0xea, 0x33, 0x09, 0x00, 0x00, 0x00, 0x00, }), PowLimit: new(big.Int).Sub(new(big.Int).Lsh(bigOne, 224), bigOne), + PowLimitBits: 0x1d00ffff, TargetTimespan: time.Hour * 24 * 14, // 14 days TargetTimePerBlock: time.Minute * 10, // 10 minutes RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 20, // TargetTimePerBlock * 2 // Checkpoints ordered from oldest to newest. Checkpoints: []chaincfg.Checkpoint{ @@ -261,11 +271,14 @@ var BTCParams = map[string]*netparams.ChainParams{ BIP0034Height: 21111, // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8 BIP0065Height: 581885, // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6 BIP0066Height: 330776, // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182 + CoinbaseMaturity: 100, + MaxSatoshi: btcutil.MaxSatoshi, }, "simnet": { + Chain: "btc", Name: "regtest", Net: wire.TestNet, - DefaultPort: "18444", + DefaultPort: "20575", // 18444 // Changed to match dcrdex testing harness DNSSeeds: []chaincfg.DNSSeed{}, GenesisBlock: &wire.MsgBlock{ Header: wire.BlockHeader{ @@ -325,9 +338,13 @@ var BTCParams = map[string]*netparams.ChainParams{ 0xc7, 0xb2, 0xb7, 0x3c, 0xf1, 0x88, 0x91, 0x0f, }), PowLimit: new(big.Int).Sub(new(big.Int).Lsh(bigOne, 255), bigOne), + PowLimitBits: 0x207fffff, + PoWNoRetargeting: true, TargetTimespan: time.Hour * 24 * 14, // 14 days TargetTimePerBlock: time.Minute * 10, // 10 minutes RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 20, // TargetTimePerBlock * 2 Checkpoints: nil, Bech32HRPSegwit: "bcrt", // always bcrt for reg test net PubKeyHashAddrID: 0x6f, // starts with m or n @@ -339,6 +356,7 @@ var BTCParams = map[string]*netparams.ChainParams{ BIP0034Height: 100000000, // Not active - Permit ver 1 blocks BIP0065Height: 1351, // Used by regression tests BIP0066Height: 1251, // Used by regression tests + CoinbaseMaturity: 100, MaxSatoshi: btcutil.MaxSatoshi, }, } diff --git a/assets/ltc.go b/assets/ltc.go new file mode 100644 index 0000000..4305aea --- /dev/null +++ b/assets/ltc.go @@ -0,0 +1,262 @@ +package assets + +import ( + "bytes" + "fmt" + "math/big" + "time" + + "github.com/bisoncraft/utxowallet/netparams" + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "golang.org/x/crypto/scrypt" +) + +var ( + ltcMainPowLimit, _ = new(big.Int).SetString("0x0fffff000000000000000000000000000000000000000000000000000000", 0) + ltcGenesisCoinbaseTx = wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{ + 0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04, 0x40, 0x4e, 0x59, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x73, // |.......@NY Times| + 0x20, 0x30, 0x35, 0x2f, 0x4f, 0x63, 0x74, 0x2f, 0x32, 0x30, 0x31, 0x31, 0x20, 0x53, 0x74, 0x65, // | 05/Oct/2011 Ste| + 0x76, 0x65, 0x20, 0x4a, 0x6f, 0x62, 0x73, 0x2c, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x65, 0xe2, 0x80, // |ve Jobs, Apple..| + 0x99, 0x73, 0x20, 0x56, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x61, 0x72, 0x79, 0x2c, 0x20, 0x44, 0x69, // |.s Visionary, Di| + 0x65, 0x73, 0x20, 0x61, 0x74, 0x20, 0x35, 0x36, // |es at 56| + + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0x12a05f200, + PkScript: []byte{ + 0x41, 0x4, 0x1, 0x84, 0x71, 0xf, 0xa6, 0x89, + 0xad, 0x50, 0x23, 0x69, 0xc, 0x80, 0xf3, 0xa4, + 0x9c, 0x8f, 0x13, 0xf8, 0xd4, 0x5b, 0x8c, 0x85, + 0x7f, 0xbc, 0xbc, 0x8b, 0xc4, 0xa8, 0xe4, 0xd3, + 0xeb, 0x4b, 0x10, 0xf4, 0xd4, 0x60, 0x4f, 0xa0, + 0x8d, 0xce, 0x60, 0x1a, 0xaf, 0xf, 0x47, 0x2, + 0x16, 0xfe, 0x1b, 0x51, 0x85, 0xb, 0x4a, 0xcf, + 0x21, 0xb1, 0x79, 0xc4, 0x50, 0x70, 0xac, 0x7b, + 0x3, 0xa9, 0xac, + }, + }, + }, + LockTime: 0, + } + ltcGenesisMerkleRoot = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy. + 0xd9, 0xce, 0xd4, 0xed, 0x11, 0x30, 0xf7, 0xb7, + 0xfa, 0xad, 0x9b, 0xe2, 0x53, 0x23, 0xff, 0xaf, + 0xa3, 0x32, 0x32, 0xa1, 0x7c, 0x3e, 0xdf, 0x6c, + 0xfd, 0x97, 0xbe, 0xe6, 0xba, 0xfb, 0xdd, 0x97, + }) +) + +var LTCParams = map[string]*netparams.ChainParams{ + "mainnet": { + Chain: "ltc", + Name: "mainnet", + Net: 0xdbb6c0fb, + DefaultPort: "9333", + DNSSeeds: []chaincfg.DNSSeed{ + {"seed-a.litecoin.loshan.co.uk", true}, + {"dnsseed.thrasher.io", true}, + {"dnsseed.litecointools.com", false}, + {"dnsseed.litecoinpool.org", false}, + }, + GenesisBlock: &wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000 + MerkleRoot: ltcGenesisMerkleRoot, + Timestamp: time.Unix(1317972665, 0), + Bits: 0x1e0ffff0, + Nonce: 2084524493, + }, + Transactions: []*wire.MsgTx{<cGenesisCoinbaseTx}, + }, + GenesisHash: hashPointer([chainhash.HashSize]byte{ // Make go vet happy. + 0xe2, 0xbf, 0x04, 0x7e, 0x7e, 0x5a, 0x19, 0x1a, + 0xa4, 0xef, 0x34, 0xd3, 0x14, 0x97, 0x9d, 0xc9, + 0x98, 0x6e, 0x0f, 0x19, 0x25, 0x1e, 0xda, 0xba, + 0x59, 0x40, 0xfd, 0x1f, 0xe3, 0x65, 0xa7, 0x12, + }), + PowLimit: ltcMainPowLimit, + PowLimitBits: 504365055, + TargetTimespan: (time.Hour * 24 * 3) + (time.Hour * 12), // 3.5 days + TargetTimePerBlock: (time.Minute * 2) + (time.Second * 30), // 2.5 minutes + RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: false, + MinDiffReductionTime: 0, + Checkpoints: []chaincfg.Checkpoint{ + {1500, newHashFromStr("841a2965955dd288cfa707a755d05a54e45f8bd476835ec9af4402a2b59a2967")}, + {4032, newHashFromStr("9ce90e427198fc0ef05e5905ce3503725b80e26afd35a987965fd7e3d9cf0846")}, + {8064, newHashFromStr("eb984353fc5190f210651f150c40b8a4bab9eeeff0b729fcb3987da694430d70")}, + {16128, newHashFromStr("602edf1859b7f9a6af809f1d9b0e6cb66fdc1d4d9dcd7a4bec03e12a1ccd153d")}, + {23420, newHashFromStr("d80fdf9ca81afd0bd2b2a90ac3a9fe547da58f2530ec874e978fce0b5101b507")}, + {50000, newHashFromStr("69dc37eb029b68f075a5012dcc0419c127672adb4f3a32882b2b3e71d07a20a6")}, + {80000, newHashFromStr("4fcb7c02f676a300503f49c764a89955a8f920b46a8cbecb4867182ecdb2e90a")}, + {120000, newHashFromStr("bd9d26924f05f6daa7f0155f32828ec89e8e29cee9e7121b026a7a3552ac6131")}, + {161500, newHashFromStr("dbe89880474f4bb4f75c227c77ba1cdc024991123b28b8418dbbf7798471ff43")}, + {179620, newHashFromStr("2ad9c65c990ac00426d18e446e0fd7be2ffa69e9a7dcb28358a50b2b78b9f709")}, + {240000, newHashFromStr("7140d1c4b4c2157ca217ee7636f24c9c73db39c4590c4e6eab2e3ea1555088aa")}, + {383640, newHashFromStr("2b6809f094a9215bafc65eb3f110a35127a34be94b7d0590a096c3f126c6f364")}, + {409004, newHashFromStr("487518d663d9f1fa08611d9395ad74d982b667fbdc0e77e9cf39b4f1355908a3")}, + {456000, newHashFromStr("bf34f71cc6366cd487930d06be22f897e34ca6a40501ac7d401be32456372004")}, + {638902, newHashFromStr("15238656e8ec63d28de29a8c75fcf3a5819afc953dcd9cc45cecc53baec74f38")}, + {721000, newHashFromStr("198a7b4de1df9478e2463bd99d75b714eab235a2e63e741641dc8a759a9840e5")}, + }, + Bech32HRPSegwit: "ltc", // always ltc for main net + PubKeyHashAddrID: 0x30, // starts with L + ScriptHashAddrID: 0x32, // starts with M + PrivateKeyID: 0xB0, // starts with 6 (uncompressed) or T (compressed) + WitnessPubKeyHashAddrID: 0x06, // starts with p2 + WitnessScriptHashAddrID: 0x0A, // starts with 7Xh + HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, // starts with xprv + HDPublicKeyID: [4]byte{0x04, 0x88, 0xb2, 0x1e}, // starts with xpub + HDCoinType: 2, + BIP0034Height: 710000, + BIP0065Height: 918684, + BIP0066Height: 811879, + CheckPoW: checkPoWScrypt, + CoinbaseMaturity: 100, + MaxSatoshi: 84e6 * btcutil.SatoshiPerBitcoin, + }, + "testnet": { + Chain: "ltc", + Name: "testnet4", + Net: 0xf1c8d2fd, + DefaultPort: "19335", + DNSSeeds: []chaincfg.DNSSeed{ + {"testnet-seed.litecointools.com", false}, + {"seed-b.litecoin.loshan.co.uk", false /*true*/}, + {"dnsseed-testnet.thrasher.io", false /*true*/}, + }, + GenesisBlock: &wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000 + MerkleRoot: ltcGenesisMerkleRoot, + Timestamp: time.Unix(1486949366, 0), + Bits: 0x1e0ffff0, + Nonce: 293345, + }, + Transactions: []*wire.MsgTx{<cGenesisCoinbaseTx}, + }, + GenesisHash: hashPointer([chainhash.HashSize]byte{ // Make go vet happy. + 0xa0, 0x29, 0x3e, 0x4e, 0xeb, 0x3d, 0xa6, 0xe6, + 0xf5, 0x6f, 0x81, 0xed, 0x59, 0x5f, 0x57, 0x88, + 0xd, 0x1a, 0x21, 0x56, 0x9e, 0x13, 0xee, 0xfd, + 0xd9, 0x51, 0x28, 0x4b, 0x5a, 0x62, 0x66, 0x49, + }), + PowLimit: ltcMainPowLimit, + PowLimitBits: 504365055, + BIP0034Height: 76, + BIP0065Height: 76, + BIP0066Height: 76, + TargetTimespan: (time.Hour * 24 * 3) + (time.Hour * 12), // 3.5 days + TargetTimePerBlock: (time.Minute * 2) + (time.Second * 30), // 2.5 minutes + RetargetAdjustmentFactor: 4, + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 5, // TargetTimePerBlock * 2 + Checkpoints: []chaincfg.Checkpoint{ + {26115, newHashFromStr("817d5b509e91ab5e439652eee2f59271bbc7ba85021d720cdb6da6565b43c14f")}, + {43928, newHashFromStr("7d86614c153f5ef6ad878483118ae523e248cd0dd0345330cb148e812493cbb4")}, + {69296, newHashFromStr("66c2f58da3cfd282093b55eb09c1f5287d7a18801a8ff441830e67e8771010df")}, + {99949, newHashFromStr("8dd471cb5aecf5ead91e7e4b1e932c79a0763060f8d93671b6801d115bfc6cde")}, + {159256, newHashFromStr("ab5b0b9968842f5414804591119d6db829af606864b1959a25d6f5c114afb2b7")}, + {2394367, newHashFromStr("bc5829f4973d0797755efee11313687b3c63ee2f70b60b62eebcd10283534327")}, + }, + Bech32HRPSegwit: "tltc", // always tltc for test net + PubKeyHashAddrID: 0x6f, // starts with m or n + ScriptHashAddrID: 0x3a, // starts with Q + WitnessPubKeyHashAddrID: 0x52, // starts with QW + WitnessScriptHashAddrID: 0x31, // starts with T7n + PrivateKeyID: 0xef, // starts with 9 (uncompressed) or c (compressed) + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + HDCoinType: 1, + CheckPoW: checkPoWScrypt, + CoinbaseMaturity: 100, + MaxSatoshi: 84e6 * btcutil.SatoshiPerBitcoin, + }, + "simnet": { + Chain: "ltc", + Name: "regtest", + Net: 0xdab5bffa, + DefaultPort: "19444", + DNSSeeds: []chaincfg.DNSSeed{}, + + // Chain parameters + GenesisBlock: &wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash{}, + MerkleRoot: ltcGenesisMerkleRoot, + Timestamp: time.Unix(1296688602, 0), // 2011-02-02 23:16:42 +0000 UTC + Bits: 0x207fffff, // 545259519 [7fffff0000000000000000000000000000000000000000000000000000000000] + Nonce: 0, + }, + Transactions: []*wire.MsgTx{<cGenesisCoinbaseTx}, + }, + GenesisHash: hashPointer([chainhash.HashSize]byte{ // Make go vet happy. + 0xf9, 0x16, 0xc4, 0x56, 0xfc, 0x51, 0xdf, 0x62, + 0x78, 0x85, 0xd7, 0xd6, 0x74, 0xed, 0x02, 0xdc, + 0x88, 0xa2, 0x25, 0xad, 0xb3, 0xf0, 0x2a, 0xd1, + 0x3e, 0xb4, 0x93, 0x8f, 0xf3, 0x27, 0x08, 0x53, + }), + PowLimit: new(big.Int).Sub(new(big.Int).Lsh(bigOne, 255), bigOne), + PowLimitBits: 0x207fffff, + PoWNoRetargeting: true, + BIP0034Height: 100000000, // Not active - Permit ver 1 blocks + BIP0065Height: 1351, // Used by regression tests + BIP0066Height: 1251, // Used by regression tests + TargetTimespan: (time.Hour * 24 * 3) + (time.Hour * 12), // 3.5 days + TargetTimePerBlock: (time.Minute * 2) + (time.Second * 30), // 2.5 minutes + RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 20, // TargetTimePerBlock * 2 + + // Checkpoints ordered from oldest to newest. + Checkpoints: nil, + Bech32HRPSegwit: "rltc", // always rltc for reg test net + PubKeyHashAddrID: 0x6f, // starts with m or n + ScriptHashAddrID: 0x3a, // starts with Q + PrivateKeyID: 0xef, // starts with 9 (uncompressed) or c (compressed) + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + HDCoinType: 1, + CheckPoW: checkPoWScrypt, + CoinbaseMaturity: 100, + MaxSatoshi: 84e6 * btcutil.SatoshiPerBitcoin, + }, +} + +func checkPoWScrypt(hdr *wire.BlockHeader) error { + var powHash chainhash.Hash + + buf := bytes.NewBuffer(make([]byte, 0, wire.MaxBlockHeaderPayload)) + hdr.Serialize(buf) + + scryptHash, _ := scrypt.Key(buf.Bytes(), buf.Bytes(), 1024, 1, 1, 32) + copy(powHash[:], scryptHash) + + target := blockchain.CompactToBig(hdr.Bits) + + hashNum := blockchain.HashToBig(&powHash) + if hashNum.Cmp(target) > 0 { + return fmt.Errorf("block hash of %064x is higher than "+ + "expected max of %064x", hashNum, target) + } + return nil +} diff --git a/bisonwire/bisonwire.go b/bisonwire/bisonwire.go index 1025f02..beb5236 100644 --- a/bisonwire/bisonwire.go +++ b/bisonwire/bisonwire.go @@ -38,13 +38,13 @@ func makeEmptyMessage(chain Chain, command string) (wire.Message, error) { msg = &wire.MsgGetBlocks{} case wire.CmdBlock: - msg = &wire.MsgBlock{} + msg = &Block{Chain: string(chain)} case wire.CmdHeaders: msg = &wire.MsgHeaders{} case wire.CmdTx: - msg = &wire.MsgTx{} + msg = &Tx{Chain: string(chain)} // Standard BTC wire types case wire.CmdVersion: @@ -129,7 +129,7 @@ func makeEmptyMessage(chain Chain, command string) (wire.Message, error) { msg = &wire.MsgCFCheckpt{} default: - return nil, wire.ErrUnknownMessage + return nil, fmt.Errorf("%w: %s", wire.ErrUnknownMessage, command) } return msg, nil } diff --git a/bisonwire/block.go b/bisonwire/block.go new file mode 100644 index 0000000..7af20dd --- /dev/null +++ b/bisonwire/block.go @@ -0,0 +1,98 @@ +package bisonwire + +import ( + "fmt" + "io" + + "github.com/btcsuite/btcd/wire" +) + +// Block is a wire.MsgBlock, but with *bisonwire.Tx instead of wire.MsgTx. +type Block struct { + Chain string + Header wire.BlockHeader + Transactions []*Tx +} + +// MsgBlock generates a *wire.MsgBlock from the *Block. +func (b *Block) MsgBlock() *wire.MsgBlock { + msgBlock := &wire.MsgBlock{ + Header: b.Header, + Transactions: make([]*wire.MsgTx, len(b.Transactions)), + } + for i, tx := range b.Transactions { + msgBlock.Transactions[i] = &tx.MsgTx + } + return msgBlock +} + +// BlockFromMsgBlock generates a *Block, with a *wire.MsgBlock as it's base. The +// wire.MsgTx's are converted to *Tx. This is only used in tests for now, So we +// can ignore the additional Tx fields that aren't contained in *wire.MsgBlock. +func BlockFromMsgBlock(chain string, msgBlock *wire.MsgBlock) *Block { + blk := &Block{ + Chain: chain, + Header: msgBlock.Header, + Transactions: make([]*Tx, len(msgBlock.Transactions)), + } + for i, msgTx := range msgBlock.Transactions { + blk.Transactions[i] = &Tx{ + MsgTx: *msgTx, + Chain: chain, + } + } + return blk +} + +// BtcDecode decodes the serialized block based on the Block's Chain. +func (b *Block) BtcDecode(r io.Reader, pver uint32, enc wire.MessageEncoding) error { + switch b.Chain { + case "btc": + var msgBlock wire.MsgBlock + if err := msgBlock.BtcDecode(r, pver, enc); err != nil { + return fmt.Errorf("error decoding Bitcoin block: %w", err) + } + b.Header = msgBlock.Header + b.Transactions = make([]*Tx, len(msgBlock.Transactions)) + for i, msgTx := range msgBlock.Transactions { + b.Transactions[i] = &Tx{ + MsgTx: *msgTx, + Chain: b.Chain, + } + } + case "ltc": + blk, err := deserializeLitecoinBlock(r) + if err != nil { + return fmt.Errorf("error decoding Litecoin Bitcoin block: %w", err) + } + b.Header = blk.Header + b.Transactions = blk.Transactions + default: + return fmt.Errorf("unknown chain %q specified for block decoding", b.Chain) + } + return nil +} + +func (b *Block) BtcEncode(r io.Writer, pver uint32, enc wire.MessageEncoding) error { + switch b.Chain { + case "btc": + return b.MsgBlock().BtcEncode(r, pver, enc) + case "ltc": + panic("bisonwire block encoding not implemented for Litecoin") // I don't think we ever use this + default: + return fmt.Errorf("unknown chain %q specified for block decoding", b.Chain) + } +} + +func (b *Block) Command() string { + return wire.CmdBlock +} + +func (b *Block) MaxPayloadLength(uint32) uint32 { + return wire.MaxBlockPayload +} + +type BlockWithHeight struct { + Block *Block + Height uint32 +} diff --git a/bisonwire/decoder.go b/bisonwire/decoder.go new file mode 100644 index 0000000..042a8ac --- /dev/null +++ b/bisonwire/decoder.go @@ -0,0 +1,404 @@ +package bisonwire + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/btcsuite/btcd/wire" +) + +var byteOrder = binary.LittleEndian + +const ( + pver uint32 = 0 // only protocol version 0 supported + maxTxInPerMessage = wire.MaxMessagePayload/41 + 1 // wire.maxTxInPerMessage + maxTxOutPerMessage = wire.MaxMessagePayload/ // wire.maxTxOutPerMessage + wire.MinTxOutPayload + 1 + maxWitnessItemsPerInput = 4_000_000 // from wire + maxWitnessItemSize = 4_000_000 // from wire +) + +type decoder struct { + buf [8]byte + rd io.Reader + tee *bytes.Buffer // anything read from rd is Written to tee +} + +func newDecoder(r io.Reader) *decoder { + return &decoder{rd: r} +} + +func (d *decoder) Read(b []byte) (n int, err error) { + n, err = d.rd.Read(b) + if err != nil { + return 0, err + } + if d.tee != nil { + d.tee.Write(b) + } + return n, nil +} + +func (d *decoder) readByte() (byte, error) { + b := d.buf[:1] + if _, err := io.ReadFull(d, b); err != nil { + return 0, err + } + return b[0], nil +} + +func (d *decoder) readUint16() (uint16, error) { + b := d.buf[:2] + if _, err := io.ReadFull(d, b); err != nil { + return 0, err + } + return byteOrder.Uint16(b), nil +} + +func (d *decoder) readUint32() (uint32, error) { + b := d.buf[:4] + if _, err := io.ReadFull(d, b); err != nil { + return 0, err + } + return byteOrder.Uint32(b), nil +} + +func (d *decoder) readUint64() (uint64, error) { + b := d.buf[:] + if _, err := io.ReadFull(d, b); err != nil { + return 0, err + } + return byteOrder.Uint64(b), nil +} + +// readOutPoint reads the next sequence of bytes from r as an OutPoint. +func (d *decoder) readOutPoint(op *wire.OutPoint) error { + _, err := io.ReadFull(d, op.Hash[:]) + if err != nil { + return err + } + + op.Index, err = d.readUint32() + return err +} + +// wire.ReadVarInt a.k.a. CompactSize, not VARINT +// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer +func (d *decoder) readCompactSize() (uint64, error) { + // Compact Size + // size < 253 -- 1 byte + // size <= USHRT_MAX -- 3 bytes (253 + 2 bytes) + // size <= UINT_MAX -- 5 bytes (254 + 4 bytes) + // size > UINT_MAX -- 9 bytes (255 + 8 bytes) + chSize, err := d.readByte() + if err != nil { + return 0, err + } + switch chSize { + case 253: + sz, err := d.readUint16() + if err != nil { + return 0, err + } + return uint64(sz), nil + case 254: + sz, err := d.readUint32() + if err != nil { + return 0, err + } + return uint64(sz), nil + case 255: + sz, err := d.readUint64() + if err != nil { + return 0, err + } + return sz, nil + default: // < 253 + return uint64(chSize), nil + } +} + +// variable length quantity, not supposed to be part of the wire protocol. +// Not the same as CompactSize type in the C++ code, but rather VARINT. +// https://en.wikipedia.org/wiki/Variable-length_quantity +// This function is borrowed from dcrd. +func (d *decoder) readVLQ() (uint64, error) { + var n uint64 + for { + val, err := d.readByte() + if err != nil { + return 0, err + } + n = (n << 7) | uint64(val&0x7f) + if val&0x80 != 0x80 { + break + } + n++ + } + + return n, nil +} + +// readTxIn reads the next sequence of bytes from r as a transaction input. +func (d *decoder) readTxIn(ti *wire.TxIn) error { + err := d.readOutPoint(&ti.PreviousOutPoint) + if err != nil { + return err + } + + ti.SignatureScript, err = wire.ReadVarBytes(d, pver, wire.MaxMessagePayload, "sigScript") + if err != nil { + return err + } + + ti.Sequence, err = d.readUint32() + return err +} + +// readTxOut reads the next sequence of bytes from r as a transaction output. +func (d *decoder) readTxOut(to *wire.TxOut) error { + v, err := d.readUint64() + if err != nil { + return err + } + + pkScript, err := wire.ReadVarBytes(d, pver, wire.MaxMessagePayload, "pkScript") + if err != nil { + return err + } + + to.Value = int64(v) + to.PkScript = pkScript + + return nil +} + +func (d *decoder) discardBytes(n int64) error { + m, err := io.CopyN(io.Discard, d, n) + if err != nil { + return err + } + if m != n { + return fmt.Errorf("only discarded %d of %d bytes", m, n) + } + return nil +} + +func (d *decoder) discardVect() error { + sz, err := d.readCompactSize() + if err != nil { + return err + } + return d.discardBytes(int64(sz)) +} + +func (d *decoder) readMWTX() ([]byte, bool, error) { + // src/mweb/mweb_models.h - struct Tx + // "A convenience wrapper around a possibly-null MWEB transcation." + // Read the uint8_t is_set of OptionalPtr. + haveMWTX, err := d.readByte() + if err != nil { + return nil, false, err + } + if haveMWTX == 0 { + return nil, true, nil // HogEx - that's all folks + } + + // src/libmw/include/mw/models/tx/Transaction.h - class Transaction + // READWRITE(obj.m_kernelOffset); // class BlindingFactor + // READWRITE(obj.m_stealthOffset); // class BlindingFactor + // READWRITE(obj.m_body); // class TxBody + + // src/libmw/include/mw/models/crypto/BlindingFactor.h - class BlindingFactor + // just 32 bytes: + // BigInt<32> m_value; + // READWRITE(obj.m_value); + // x2 for both kernel_offset and stealth_offset BlindingFactor + if err = d.discardBytes(64); err != nil { + return nil, false, err + } + + // TxBody + kern0, err := d.readMWTXBody() + if err != nil { + return nil, false, err + } + return kern0, false, nil +} + +func (d *decoder) readMWTXBody() ([]byte, error) { + // src/libmw/include/mw/models/tx/TxBody.h - class TxBody + // READWRITE(obj.m_inputs, obj.m_outputs, obj.m_kernels); + // std::vector m_inputs; + // std::vector m_outputs; + // std::vector m_kernels; + + // inputs + numIn, err := d.readCompactSize() + if err != nil { + return nil, err + } + for i := 0; i < int(numIn); i++ { + // src/libmw/include/mw/models/tx/Input.h - class Input + // - features uint8_t + // - outputID mw::Hash - 32 bytes BigInt<32> + // - commit Commitment - 33 bytes BigInt for SIZE 33 + // - outputPubKey PublicKey - 33 bytes BigInt<33> + // (if features includes STEALTH_KEY_FEATURE_BIT) input_pubkey PublicKey + // (if features includes EXTRA_DATA_FEATURE_BIT) extraData vector + // - signature Signature - 64 bytes BigInt for SIZE 64 + // + // enum FeatureBit { + // STEALTH_KEY_FEATURE_BIT = 0x01, + // EXTRA_DATA_FEATURE_BIT = 0x02 + // }; + feats, err := d.readByte() + if err != nil { + return nil, err + } + if err = d.discardBytes(32 + 33 + 33); err != nil { // outputID, commitment, outputPubKey + return nil, err + } + if feats&0x1 != 0 { // input pubkey + if err = d.discardBytes(33); err != nil { + return nil, err + } + } + if feats&0x2 != 0 { // extraData + if err = d.discardVect(); err != nil { + return nil, err + } + } + if err = d.discardBytes(64); err != nil { // sig + return nil, err + } + } // inputs + + // outputs + numOut, err := d.readCompactSize() + if err != nil { + return nil, err + } + for i := 0; i < int(numOut); i++ { + // src/libmw/include/mw/models/tx/Output.h - class Output + // commit + // sender pubkey + // receiver pubkey + // message OutputMessage --- fuuuuuuuu + // proof RangeProof + // signature + if err = d.discardBytes(33 + 33 + 33); err != nil { // commitment, sender pk, receiver pk + return nil, err + } + + // OutputMessage + feats, err := d.readByte() + if err != nil { + return nil, err + } + // enum FeatureBit { + // STANDARD_FIELDS_FEATURE_BIT = 0x01, + // EXTRA_DATA_FEATURE_BIT = 0x02 + // }; + if feats&0x1 != 0 { // pubkey | view_tag uint8_t | masked_value uint64_t | nonce 16-bytes + if err = d.discardBytes(33 + 1 + 8 + 16); err != nil { + return nil, err + } + } + if feats&0x2 != 0 { // extraData + if err = d.discardVect(); err != nil { + return nil, err + } + } + + // RangeProof "The proof itself, at most 675 bytes long." + // std::vector m_bytes; -- except it's actually used like a [675]byte... + if err = d.discardBytes(675 + 64); err != nil { // proof + sig + return nil, err + } + } // outputs + + // kernels + numKerns, err := d.readCompactSize() + if err != nil { + return nil, err + } + // Capture the first kernel since pure MW txns (no canonical inputs or + // outputs) that live in the MWEB but are also seen in mempool have their + // hash computed as blake3_256(kernel0). + var kern0 []byte + d.tee = new(bytes.Buffer) + for i := 0; i < int(numKerns); i++ { + // src/libmw/include/mw/models/tx/Kernel.h - class Kernel + + // enum FeatureBit { + // FEE_FEATURE_BIT = 0x01, + // PEGIN_FEATURE_BIT = 0x02, + // PEGOUT_FEATURE_BIT = 0x04, + // HEIGHT_LOCK_FEATURE_BIT = 0x08, + // STEALTH_EXCESS_FEATURE_BIT = 0x10, + // EXTRA_DATA_FEATURE_BIT = 0x20, + // ALL_FEATURE_BITS = FEE_FEATURE_BIT | PEGIN... + // }; + feats, err := d.readByte() + if err != nil { + return nil, err + } + if feats&0x1 != 0 { // fee + _, err = d.readVLQ() // vlq for amount? in the wire protocol?!? + if err != nil { + return nil, err + } + } + if feats&0x2 != 0 { // pegin amt + _, err = d.readVLQ() + if err != nil { + return nil, err + } + } + if feats&0x4 != 0 { // pegouts vector + sz, err := d.readCompactSize() + if err != nil { + return nil, err + } + for i := uint64(0); i < sz; i++ { + _, err = d.readVLQ() // pegout amt + if err != nil { + return nil, err + } + if err = d.discardVect(); err != nil { // pkScript + return nil, err + } + } + } + if feats&0x8 != 0 { // lockHeight + _, err = d.readVLQ() + if err != nil { + return nil, err + } + } + if feats&0x10 != 0 { // stealth_excess pubkey + if err = d.discardBytes(33); err != nil { + return nil, err + } + } + if feats&0x20 != 0 { // extraData vector + if err = d.discardVect(); err != nil { + return nil, err + } + } + // "excess" commitment and signature + if err = d.discardBytes(33 + 64); err != nil { + return nil, err + } + + if i == 0 { + kern0 = d.tee.Bytes() + d.tee = nil + } + } // kernels + + return kern0, nil +} diff --git a/bisonwire/encoding.go b/bisonwire/encoding.go index f8b07f7..7a9963c 100644 --- a/bisonwire/encoding.go +++ b/bisonwire/encoding.go @@ -188,7 +188,7 @@ func ReadMessageWithEncodingN( // message. This function is the same as ReadMessage except it also returns the // number of bytes read. func ReadMessageN(r io.Reader, pver uint32, chain Chain, btcnet wire.BitcoinNet) (int, wire.Message, []byte, error) { - return ReadMessageWithEncodingN(r, pver, chain, btcnet, wire.BaseEncoding) + return ReadMessageWithEncodingN(r, pver, chain, btcnet, wire.LatestEncoding) } // ReadMessage reads, validates, and parses the next bitcoin Message from r for diff --git a/bisonwire/ltc-test-data/testnet4Block1821752.dat b/bisonwire/ltc-test-data/testnet4Block1821752.dat new file mode 100644 index 0000000..26cd515 Binary files /dev/null and b/bisonwire/ltc-test-data/testnet4Block1821752.dat differ diff --git a/bisonwire/ltc-test-data/testnet4Block2215584.dat b/bisonwire/ltc-test-data/testnet4Block2215584.dat new file mode 100644 index 0000000..02a5daa Binary files /dev/null and b/bisonwire/ltc-test-data/testnet4Block2215584.dat differ diff --git a/bisonwire/ltc-test-data/testnet4Block2215586.dat b/bisonwire/ltc-test-data/testnet4Block2215586.dat new file mode 100644 index 0000000..f23ae0e Binary files /dev/null and b/bisonwire/ltc-test-data/testnet4Block2215586.dat differ diff --git a/bisonwire/ltc-test-data/testnet4Block2319633.dat b/bisonwire/ltc-test-data/testnet4Block2319633.dat new file mode 100644 index 0000000..193246f Binary files /dev/null and b/bisonwire/ltc-test-data/testnet4Block2319633.dat differ diff --git a/bisonwire/ltc-test-data/testnet4Block2321749.dat b/bisonwire/ltc-test-data/testnet4Block2321749.dat new file mode 100644 index 0000000..b34a41e Binary files /dev/null and b/bisonwire/ltc-test-data/testnet4Block2321749.dat differ diff --git a/bisonwire/ltc-test-data/tx62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2_mp.dat b/bisonwire/ltc-test-data/tx62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2_mp.dat new file mode 100644 index 0000000..c60743d Binary files /dev/null and b/bisonwire/ltc-test-data/tx62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2_mp.dat differ diff --git a/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03.dat b/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03.dat new file mode 100644 index 0000000..3b86859 Binary files /dev/null and b/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03.dat differ diff --git a/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03_mp.dat b/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03_mp.dat new file mode 100644 index 0000000..acdd927 Binary files /dev/null and b/bisonwire/ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03_mp.dat differ diff --git a/bisonwire/ltc-test-data/txcc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349_mp.dat b/bisonwire/ltc-test-data/txcc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349_mp.dat new file mode 100644 index 0000000..9994dda Binary files /dev/null and b/bisonwire/ltc-test-data/txcc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349_mp.dat differ diff --git a/bisonwire/ltc-test-data/txde22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c.dat b/bisonwire/ltc-test-data/txde22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c.dat new file mode 100644 index 0000000..671578c Binary files /dev/null and b/bisonwire/ltc-test-data/txde22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c.dat differ diff --git a/bisonwire/ltc-test-data/txe6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049_mp.dat b/bisonwire/ltc-test-data/txe6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049_mp.dat new file mode 100644 index 0000000..c1094e8 Binary files /dev/null and b/bisonwire/ltc-test-data/txe6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049_mp.dat differ diff --git a/bisonwire/ltc.go b/bisonwire/ltc.go new file mode 100644 index 0000000..380db46 --- /dev/null +++ b/bisonwire/ltc.go @@ -0,0 +1,258 @@ +package bisonwire + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "lukechampine.com/blake3" +) + +func litcoinTxHash(tx *Tx) chainhash.Hash { + // A pure-MW tx can only be in mempool or the EB, not the canonical block. + if len(tx.Kern0) > 0 && len(tx.TxIn) == 0 && len(tx.TxOut) == 0 { + // CTransaction::ComputeHash in src/primitives/transaction.cpp. + // Fortunately also a 32 byte hash so we can use chainhash.Hash. + return blake3.Sum256(tx.Kern0) + } + return tx.MsgTx.TxHash() +} + +func deserializeLitecoinTx(r io.Reader, tx *Tx) error { + msgTx := &tx.MsgTx + dec := newDecoder(r) + + version, err := dec.readUint32() + if err != nil { + return err + } + // if version != 0 { + // return nil, fmt.Errorf("only tx version 0 supported, got %d", version) + // } + msgTx.Version = int32(version) + + count, err := dec.readCompactSize() + if err != nil { + return err + } + + // A count of zero (meaning no TxIn's to the uninitiated) means that the + // value is a TxFlagMarker, and hence indicates the presence of a flag. + var flag [1]byte + if count == 0 { + // The count varint was in fact the flag marker byte. Next, we need to + // read the flag value, which is a single byte. + if _, err = io.ReadFull(r, flag[:]); err != nil { + return err + } + + // Flag bits 0 or 3 must be set. + if flag[0]&0b1001 == 0 { + return fmt.Errorf("witness tx but flag byte is %x", flag) + } + + // With the Segregated Witness specific fields decoded, we can + // now read in the actual txin count. + count, err = dec.readCompactSize() + if err != nil { + return err + } + } + + if count > maxTxInPerMessage { + return fmt.Errorf("too many transaction inputs to fit into "+ + "max message size [count %d, max %d]", count, maxTxInPerMessage) + } + + msgTx.TxIn = make([]*wire.TxIn, count) + for i := range msgTx.TxIn { + txIn := &wire.TxIn{} + err = dec.readTxIn(txIn) + if err != nil { + return err + } + msgTx.TxIn[i] = txIn + } + + count, err = dec.readCompactSize() + if err != nil { + return err + } + if count > maxTxOutPerMessage { + return fmt.Errorf("too many transactions outputs to fit into "+ + "max message size [count %d, max %d]", count, maxTxOutPerMessage) + } + + msgTx.TxOut = make([]*wire.TxOut, count) + for i := range msgTx.TxOut { + txOut := &wire.TxOut{} + err = dec.readTxOut(txOut) + if err != nil { + return err + } + msgTx.TxOut[i] = txOut + } + + if flag[0]&0x01 != 0 { + for _, txIn := range msgTx.TxIn { + witCount, err := dec.readCompactSize() + if err != nil { + return err + } + if witCount > maxWitnessItemsPerInput { + return fmt.Errorf("too many witness items: %d > max %d", + witCount, maxWitnessItemsPerInput) + } + txIn.Witness = make(wire.TxWitness, witCount) + for j := range txIn.Witness { + txIn.Witness[j], err = wire.ReadVarBytes(r, pver, + maxWitnessItemSize, "script witness item") + if err != nil { + return err + } + } + } + } + + // check for a MW tx based on flag 0x08 + // src/primitives/transaction.h - class CTransaction + // Serialized like normal tx except with an optional MWEB::Tx after outputs + // and before locktime. + if flag[0]&0x08 != 0 { + tx.Kern0, tx.IsHogEx, err = dec.readMWTX() + if err != nil { + return err + } + if tx.IsHogEx && len(msgTx.TxOut) == 0 { + return errors.New("no outputs on HogEx txn") + } + } + + msgTx.LockTime, err = dec.readUint32() + return err +} + +// Block + +const ( + // mwebVer is the bit of the block header's version that indicates the + // presence of a MWEB. + mwebVer = 0x20000000 // 1 << 29 +) + +func parseMWEB(blk io.Reader) error { + dec := newDecoder(blk) + // src/mweb/mweb_models.h - struct Block + // "A convenience wrapper around a possibly-null extension block."" + // OptionalPtr around a mw::Block. Read the option byte: + hasMWEB, err := dec.readByte() + if err != nil { + return fmt.Errorf("failed to check MWEB option byte: %w", err) + } + if hasMWEB == 0 { + return nil + } + + // src/libmw/include/mw/models/block/Block.h - class Block + // (1) Header and (2) TxBody + + // src/libmw/include/mw/models/block/Header.h - class Header + // height + if _, err = dec.readVLQ(); err != nil { + return fmt.Errorf("failed to decode MWEB height: %w", err) + } + + // 3x Hash + 2x BlindingFactor + if err = dec.discardBytes(32*3 + 32*2); err != nil { + return fmt.Errorf("failed to decode MWEB junk: %w", err) + } + + // Number of TXOs: outputMMRSize + if _, err = dec.readVLQ(); err != nil { + return fmt.Errorf("failed to decode TXO count: %w", err) + } + + // Number of kernels: kernelMMRSize + if _, err = dec.readVLQ(); err != nil { + return fmt.Errorf("failed to decode kernel count: %w", err) + } + + // TxBody + _, err = dec.readMWTXBody() + if err != nil { + return fmt.Errorf("failed to decode MWEB tx: %w", err) + } + // if len(kern0) > 0 { + // mwebTxID := chainhash.Hash(blake3.Sum256(kern0)) + // fmt.Println(mwebTxID.String()) + // } + + return nil +} + +// DeserializeBlock decodes the bytes of a serialized Litecoin block. This +// function exists because MWEB changes both the block and transaction +// serializations. Blocks may have a MW "extension block" for "peg-out" +// transactions, and this EB is after all the transactions in the regular LTC +// block. After the canonical transactions in the regular block, there may be +// zero or more "peg-in" transactions followed by one integration transaction +// (also known as a HogEx transaction), all still in the regular LTC block. The +// peg-in txns decode correctly, but the integration tx is a special transaction +// with the witness tx flag with bit 3 set (8), which prevents correct +// wire.MsgTx deserialization. +// Refs: +// https://github.com/litecoin-project/lips/blob/master/lip-0002.mediawiki#PegOut_Transactions +// https://github.com/litecoin-project/lips/blob/master/lip-0003.mediawiki#Specification +// https://github.com/litecoin-project/litecoin/commit/9d1f530a5fa6d16871fdcc3b506be42b593d3ce4 +// https://github.com/litecoin-project/litecoin/commit/8c82032f45e644f413ec5c91e121a31c993aa831 +// (src/libmw/include/mw/models/tx/Transaction.h for the `mweb_tx` field of the +// `CTransaction` in the "primitives" commit). +func deserializeLitecoinBlock(r io.Reader) (*Block, error) { + // Block header + hdr := &wire.BlockHeader{} + err := hdr.Deserialize(r) + if err != nil { + return nil, fmt.Errorf("failed to deserialize block header: %w", err) + } + + // This block's transactions + txnCount, err := wire.ReadVarInt(r, 0) + if err != nil { + return nil, fmt.Errorf("failed to parse transaction count: %w", err) + } + + // We can only decode the canonical txns, not the mw peg-in txs in the EB. + var hasHogEx bool + txns := make([]*Tx, 0, int(txnCount)) + for i := 0; i < cap(txns); i++ { + tx := &Tx{Chain: "ltc"} + if err := deserializeLitecoinTx(r, tx); err != nil { + return nil, fmt.Errorf("failed to deserialize transaction %d of %d in block %v: %w", + i+1, txnCount, hdr.BlockHash(), err) + } + txns = append(txns, tx) // txns = append(txns, msgTx) + hasHogEx = tx.IsHogEx // hogex is the last txn + } + + // The mwebVer mask indicates it may contain a MWEB after a HogEx. + // src/primitives/block.h: SERIALIZE_NO_MWEB + if hdr.Version&mwebVer != 0 && hasHogEx { + if err = parseMWEB(r); err != nil { + return nil, err + } + } + + return &Block{ + Header: *hdr, + Transactions: txns, + }, nil +} + +// deserializeLitecoinBlockBytes wraps DeserializeBlock using bytes.NewReader for +// convenience. +func deserializeLitecoinBlockBytes(blk []byte) (*Block, error) { + return deserializeLitecoinBlock(bytes.NewReader(blk)) +} diff --git a/bisonwire/ltc_test.go b/bisonwire/ltc_test.go new file mode 100644 index 0000000..187f5b5 --- /dev/null +++ b/bisonwire/ltc_test.go @@ -0,0 +1,262 @@ +package bisonwire + +import ( + "bytes" + _ "embed" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +var ( + // peg-in txn 8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03 + // MEMPOOL, not in block, 2203 bytes, version 2 + // with: + // - flag 0x9 + // - 1 regular input / 1 regular output + // - 0 mw inputs / 2 mw outputs + // - 1 kernel with peg-in amt and fee amt and stealth pubkey + //go:embed ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03_mp.dat + tx8b83439mp []byte + + // peg-in txn 8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03 + // This is the same tx as above, but now in the block stripped of mw tx + // data and with the flag with the mw bit unset. + // IN BLOCK, 203 bytes, version 2 + // with: + // - flag 0x1 (mw removed) + // - 1 regular input / 1 regular output + //go:embed ltc-test-data/tx8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03.dat + tx8b83439 []byte + + // Pure MW-only txn (no canonical inputs or outputs). + // MEMPOOL, not in block, 2203 bytes, version 2 + // with: + // - flag 0x8 + // - 0 regular inputs / 0 regular outputs + // - 1 mw input / 2 mw outputs + // - 1 kernel with fee amt and stealth pubkey + //go:embed ltc-test-data/txe6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049_mp.dat + txe6f8fdbmp []byte + + // Pure MW-only txn (no canonical inputs or outputs). + // MEMPOOL, not in block, 2987 bytes, version 2 + // with: + // - flag 0x8 + // - 0 regular inputs / 0 regular outputs + // - 5 mw inputs / 2 mw outputs + // - 1 kernel with fee amt and stealth pubkey + //go:embed ltc-test-data/tx62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2_mp.dat + tx62e1756mp []byte + + // Pure MW-only txn (no canonical inputs or outputs). + // MEMPOOL, not in block, 1333 bytes, version 2 + // with: + // - flag 0x8 + // - 0 regular inputs / 0 regular outputs + // - 1 mw inputs / 1 mw outputs + // - 1 kernel with fee amt and pegouts and stealth pubkey + //go:embed ltc-test-data/txcc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349_mp.dat + txcc202c4mp []byte + + // HogEx with multiple inputs and outputs, IN BLOCK 2269979. + // Flag 0x8 (omit witness data), null mw tx (but not omitted), 169 bytes. + //go:embed ltc-test-data/txde22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c.dat + txde22f4d []byte +) + +func TestDeserializeTx(t *testing.T) { + tests := []struct { + name string + tx []byte + wantHash string + wantLockTime uint32 // last field after any mw tx data is the ideal check + }{ + { + "mempool peg-in tx 8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03", + tx8b83439mp, + "8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03", + 2269978, + }, + { + "block peg-in tx 8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03", + tx8b83439, + "8b8343978dbef95d54da796977e9a254565c0dc9ce54917d9111267547fcde03", + 2269978, + }, + { + "mempool MW tx e6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049", + txe6f8fdbmp, + "e6f8fdb15b27e603adcfa5aa4107a0e7a7b07e6dc76d2ba95a33710db02a0049", + 2269917, + }, + { + "mempool MW tx 62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2", + tx62e1756mp, + "62e17562a697e3167e2b68bce5a927ac355249ef5751800c4dd927ddf57d9db2", + 2269919, + }, + { + "mempool MW tx cc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349", + txcc202c4mp, + "cc202c44db4d6faec57f42566c6b1e03139f924eaf685ae964b3076594d65349", + 2269977, + }, + { + "HogEx tx de22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c", + txde22f4d, + "de22f4de7116b8482a691cc5e552c4212f0ae77e3c8d92c9cb85c29f4dc1f47c", + 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := &Tx{Chain: "ltc"} + err := deserializeLitecoinTx(bytes.NewReader(tt.tx), tx) + if err != nil { + t.Fatal(err) + } + if tx.LockTime != tt.wantLockTime { + t.Errorf("want locktime %d, got %d", tt.wantLockTime, tx.LockTime) + } + + txHash := tx.TxHash() + if txHash.String() != tt.wantHash { + t.Errorf("Wanted tx hash %v, got %v", tt.wantHash, txHash) + } + }) + } +} + +// Block Testing + +var ( + // Testnet4 block 1821752 is pre-MWEB activation, 3 txns. + // But version 20000000. + //go:embed ltc-test-data/testnet4Block1821752.dat + block1821752 []byte + + // Block 2215584 is the first with MW txns, a peg-in with witness version 9 + // script, an integ tx with witness version 8 script, block version 20000000 + // (with a MWEB), and 5 txns. + // 7e35fabe7b3c694ebeb0368a1a1c31e83962f3c5b4cc8dcede3ae94ed3deb306 + //go:embed ltc-test-data/testnet4Block2215584.dat + block2215584 []byte + + // Block 2321749 is version 20000000 with a MWEB, 4 txns, the last one being + // an integration / hogex txn that fails to decode. + // 57929846db4a92d937eb596354d10949e33c815ee45df0c9b3bbdfb283e15bcd + //go:embed ltc-test-data/testnet4Block2321749.dat + block2321749 []byte + + // Block 2319633 is version 20000000 with a MWEB, 2 txns, one coinbase and + // one integration. + // e9fe2c6496aedefa8bf6529bdc5c1f9fd4af565ca4c98cab73e3a1f616fb3502 + //go:embed ltc-test-data/testnet4Block2319633.dat + block2319633 []byte + + //go:embed ltc-test-data/testnet4Block2215586.dat + block2215586 []byte +) + +func TestDeserializeBlockBytes(t *testing.T) { + tests := []struct { + name string + blk []byte + wantHash string + wantNumTx int + wantLastTx string + }{ + { + "block 1821752 pre-MWEB activation", + block1821752, + "ece484c02e84e4b1c551fbbdde3045e9096c970fbd3e31f2586b68d50dad6b24", + 3, + "cb4d9d2d7ab7211ddf030a667d320fe499c849623e9d4a130e1901391e9d4947", + }, + { + "block 2215584 MWEB", + block2215584, + "7e35fabe7b3c694ebeb0368a1a1c31e83962f3c5b4cc8dcede3ae94ed3deb306", + 5, + "4c86658e64861c2f2b7fbbf26bbf7a6640ae3824d24293a009ad5ea1e8ab4418", + }, + { + "block 2215586 MWEB", + block2215586, + "3000cc2076a568a8eb5f56a06112a57264446e2c7d2cca28cdc85d91820dfa17", + 37, + "3a7299f5e6ee9975bdcc2d754ff5de3312d92db177b55c68753a1cdf9ce63a7c", + }, + { + "block 2321749 MWEB", + block2321749, + "57929846db4a92d937eb596354d10949e33c815ee45df0c9b3bbdfb283e15bcd", + 4, + "1bad5e78b145947d32eeeb1d24295891ba03359508d5f09921bada3be66bbe17", + }, + { + "block 2319633 MWEB", + block2319633, + "e9fe2c6496aedefa8bf6529bdc5c1f9fd4af565ca4c98cab73e3a1f616fb3502", + 2, + "3cd43df64e9382040eff0bf54ba1c2389d5111eb5ab0968ab7af67e3c30cac04", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msgBlk, err := deserializeLitecoinBlock(bytes.NewReader(tt.blk)) + if err != nil { + t.Fatal(err) + } + + blkHash := msgBlk.Header.BlockHash() + if blkHash.String() != tt.wantHash { + t.Errorf("Wanted block hash %v, got %v", tt.wantHash, blkHash) + } + + if len(msgBlk.Transactions) != tt.wantNumTx { + t.Errorf("Wanted %d txns, found %d", tt.wantNumTx, len(msgBlk.Transactions)) + } + + lastTxHash := msgBlk.Transactions[len(msgBlk.Transactions)-1].TxHash() + if lastTxHash.String() != tt.wantLastTx { + t.Errorf("Wanted last tx hash %v, got %v", tt.wantLastTx, lastTxHash) + } + }) + } +} + +func TestDecodeTransaction(t *testing.T) { + // pegin 84b7ea499d5650cc220afac8b972527cef10ed402da5a5b000f994199044f450 + // output has witness ver 9 + peginTx, _ := hex.DecodeString("02000000000101d4cfc0df00ced17d9f05460b20be3f8b362e213fbfb22514c5a7e566bd9bd6a10000000000feffffff012a" + + "c07b050000000022592056961cee5a05f60a40f4730bef10786b0b54e42e460ee9102b2756b69efe37210247304402205f44" + + "1fa690d41056ab5c635fd8f96427cdb7825ba5cdb4a977758fb09ac2bb2e02204c068a057469fd22176ec45bd1bc780bb6db" + + "34e299a601ae735b8f3db5abd4b10121029e94825ddd9ed088ca2c270b3ccb5cb5573a8204215912cac1f68f288190270c9f" + + "ce2100") + msgTx := &wire.MsgTx{} + err := msgTx.Deserialize(bytes.NewReader(peginTx)) + if err != nil { + t.Fatal(err) + } + if msgTx.TxOut[0].PkScript[0] != txscript.OP_9 { + t.Errorf("did not get witness version 9") + } + + // integ 3cd43df64e9382040eff0bf54ba1c2389d5111eb5ab0968ab7af67e3c30cac04 + // output has witness ver 8 + // tx flag has bit 8 set, indicating hogex + integTx, _ := hex.DecodeString("02000000000801bba0a561a904465fe2215f77822e04045ca01491c358bb16" + + "89200d71ada3836b0000000000ffffffff01ec069e659a320000225820399cda16" + + "3b49fdac1f669f69e63b56756d3bc6f2523eb10615710154959cc1360000000000") + msgTx = &wire.MsgTx{} + err = msgTx.Deserialize(bytes.NewReader(integTx)) + if err == nil { // witness tx but flag byte is 08 + t.Fatal("expected error") //fails because of flag + } +} diff --git a/bisonwire/tx.go b/bisonwire/tx.go new file mode 100644 index 0000000..448c133 --- /dev/null +++ b/bisonwire/tx.go @@ -0,0 +1,38 @@ +package bisonwire + +import ( + "fmt" + "io" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +type Tx struct { + wire.MsgTx + Chain string // TODO: Use bisonwire.Chain type + IsHogEx bool + Kern0 []byte +} + +func (tx *Tx) Deserialize(r io.Reader) error { + switch tx.Chain { + case "btc": + return tx.MsgTx.Deserialize(r) + case "ltc": + return deserializeLitecoinTx(r, tx) + default: + return fmt.Errorf("unknown chain %q specified for bisonwire tx deserialization", tx.Chain) + } +} + +func (tx *Tx) TxHash() chainhash.Hash { + switch tx.Chain { + case "btc": + return tx.MsgTx.TxHash() + case "ltc": + return litcoinTxHash(tx) + default: + panic(fmt.Sprintf("unknown chain %q specified for bisonwire tx hash", tx.Chain)) + } +} diff --git a/chain/block_filterer.go b/chain/block_filterer.go index fd2a648..de0a092 100644 --- a/chain/block_filterer.go +++ b/chain/block_filterer.go @@ -1,6 +1,7 @@ package chain import ( + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -59,7 +60,7 @@ type BlockFilterer struct { // RelevantTxns records the transactions found in a particular block // that contained matches from an address in either ExReverseFilter or // InReverseFilter. - RelevantTxns []*wire.MsgTx + RelevantTxns []*bisonwire.Tx } // NewBlockFilterer constructs the reverse indexes for the current set of @@ -105,7 +106,7 @@ func NewBlockFilterer(params *chaincfg.Params, // filters. This method return true iff the block contains a non-zero number of // addresses of interest, or a transaction in the block spends from outpoints // controlled by the wallet. -func (bf *BlockFilterer) FilterBlock(block *wire.MsgBlock) bool { +func (bf *BlockFilterer) FilterBlock(block *bisonwire.Block) bool { var hasRelevantTxns bool for _, tx := range block.Transactions { if bf.FilterTx(tx) { @@ -122,7 +123,7 @@ func (bf *BlockFilterer) FilterBlock(block *wire.MsgBlock) bool { // indexes. This method returns true iff the txn contains a non-zero number of // addresses of interest, or the transaction spends from an outpoint that // belongs to the wallet. -func (bf *BlockFilterer) FilterTx(tx *wire.MsgTx) bool { +func (bf *BlockFilterer) FilterTx(tx *bisonwire.Tx) bool { var isRelevant bool // First, check the inputs to this transaction to see if they spend any @@ -164,7 +165,7 @@ func (bf *BlockFilterer) FilterTx(tx *wire.MsgTx) bool { // found outpoints, so that the caller can update its global // set of watched outpoints. outPoint := wire.OutPoint{ - Hash: *btcutil.NewTx(tx).Hash(), + Hash: tx.TxHash(), Index: uint32(i), } diff --git a/chain/block_filterer_test.go b/chain/block_filterer_test.go index fe28c16..e8dae7f 100644 --- a/chain/block_filterer_test.go +++ b/chain/block_filterer_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/chain" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -268,8 +269,8 @@ var Block100000 = wire.MsgBlock{ func TestBlockFiltererOneInOneOut(t *testing.T) { // Watch for spend from prev in in first and last tx, both of which are // single input/single output. - firstTx := Block100000.Transactions[1] - lastTx := Block100000.Transactions[3] + firstTx := &bisonwire.Tx{Chain: "btc", MsgTx: *Block100000.Transactions[1]} + lastTx := &bisonwire.Tx{Chain: "btc", MsgTx: *Block100000.Transactions[3]} // Add each of their single previous outpoints to the set of watched // outpoints to filter for. @@ -286,7 +287,7 @@ func TestBlockFiltererOneInOneOut(t *testing.T) { // Filter block 100000, which should find matches for the watched // outpoints. - match := blockFilterer.FilterBlock(&Block100000) + match := blockFilterer.FilterBlock(bisonwire.BlockFromMsgBlock("btc", &Block100000)) if !match { t.Fatalf("failed to find matches when filtering for " + "1-in-1-out txns") @@ -312,7 +313,8 @@ func assertNumRelevantTxns(t *testing.T, bf *chain.BlockFilterer, size int) { // assertRelevantTxnsContains checks that the wantTx is found in the block // filterers set of relevant txns. -func assertRelevantTxnsContains(t *testing.T, bf *chain.BlockFilterer, wantTx *wire.MsgTx) { +func assertRelevantTxnsContains(t *testing.T, bf *chain.BlockFilterer, wantTx *bisonwire.Tx) { + t.Helper() for _, relevantTx := range bf.RelevantTxns { if reflect.DeepEqual(relevantTx, wantTx) { return diff --git a/chain/chainservice.go b/chain/chainservice.go index 4b8592b..9959095 100644 --- a/chain/chainservice.go +++ b/chain/chainservice.go @@ -1,10 +1,10 @@ package chain import ( + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv" "github.com/bisoncraft/utxowallet/spv/banman" "github.com/bisoncraft/utxowallet/spv/headerfs" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -15,7 +15,7 @@ import ( // methods of an *spv.ChainService type NeutrinoChainService interface { Start() error - GetBlock(chainhash.Hash, ...spv.QueryOption) (*btcutil.Block, error) + GetBlock(chainhash.Hash, ...spv.QueryOption) (*bisonwire.BlockWithHeight, error) GetBlockHeight(*chainhash.Hash) (int32, error) BestBlock() (*headerfs.BlockStamp, error) GetBlockHash(int64) (*chainhash.Hash, error) diff --git a/chain/interface.go b/chain/interface.go index a102366..b71b041 100644 --- a/chain/interface.go +++ b/chain/interface.go @@ -3,6 +3,7 @@ package chain import ( "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/bisoncraft/utxowallet/wtxmgr" "github.com/btcsuite/btcd/btcutil" @@ -35,7 +36,7 @@ type Interface interface { Stop() WaitForShutdown() GetBestBlock() (*chainhash.Hash, int32, error) - GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) + GetBlock(*chainhash.Hash) (*bisonwire.BlockWithHeight, error) GetBlockHash(int64) (*chainhash.Hash, error) GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) IsCurrent() bool @@ -95,7 +96,7 @@ type ( FoundExternalAddrs map[waddrmgr.KeyScope]map[uint32]struct{} FoundInternalAddrs map[waddrmgr.KeyScope]map[uint32]struct{} FoundOutPoints map[wire.OutPoint]btcutil.Address - RelevantTxns []*wire.MsgTx + RelevantTxns []*bisonwire.Tx } // BlockDisconnected is a notifcation that the block described by the diff --git a/chain/mocks_test.go b/chain/mocks_test.go index 6d3879d..159165e 100644 --- a/chain/mocks_test.go +++ b/chain/mocks_test.go @@ -4,6 +4,7 @@ import ( "container/list" "errors" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv" "github.com/bisoncraft/utxowallet/spv/banman" "github.com/bisoncraft/utxowallet/spv/headerfs" @@ -83,7 +84,7 @@ func (m *mockChainService) GetBlockHeader( } func (m *mockChainService) GetBlock(chainhash.Hash, - ...spv.QueryOption) (*btcutil.Block, error) { + ...spv.QueryOption) (*bisonwire.BlockWithHeight, error) { return nil, errNotImplemented } diff --git a/chain/neutrino.go b/chain/neutrino.go index 060a409..dcd74ce 100644 --- a/chain/neutrino.go +++ b/chain/neutrino.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/spv" "github.com/bisoncraft/utxowallet/spv/headerfs" @@ -16,7 +17,6 @@ import ( "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) @@ -155,7 +155,7 @@ func (s *NeutrinoClient) WaitForShutdown() { } // GetBlock replicates the RPC client's GetBlock command. -func (s *NeutrinoClient) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) { +func (s *NeutrinoClient) GetBlock(hash *chainhash.Hash) (*bisonwire.BlockWithHeight, error) { // TODO(roasbeef): add a block cache? // * which evication strategy? depends on use case // Should the block cache be INSIDE neutrino instead of in btcwallet? @@ -163,7 +163,7 @@ func (s *NeutrinoClient) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) if err != nil { return nil, err } - return block.MsgBlock(), nil + return block, nil } // GetBlockHeight gets the height of a block by its hash. It serves as a @@ -283,7 +283,7 @@ func (s *NeutrinoClient) FilterBlocks( return nil, err } - if !blockFilterer.FilterBlock(rawBlock) { + if !blockFilterer.FilterBlock(rawBlock.Block) { continue } @@ -464,7 +464,7 @@ func (s *NeutrinoClient) Rescan(startHash *chainhash.Hash, addrs []btcutil.Addre s.clientMtx.Lock() newRescan := s.newRescan( - spv.NotificationHandlers(rpcclient.NotificationHandlers{ + spv.NotificationHandlers(spv.NoteHandlers{ OnBlockConnected: s.onBlockConnected, OnFilteredBlockConnected: s.onFilteredBlockConnected, OnBlockDisconnected: s.onBlockDisconnected, @@ -520,7 +520,7 @@ func (s *NeutrinoClient) NotifyReceived(addrs []btcutil.Address) error { // Rescan with just the specified addresses. newRescan := s.newRescan( - spv.NotificationHandlers(rpcclient.NotificationHandlers{ + spv.NotificationHandlers(spv.NoteHandlers{ OnBlockConnected: s.onBlockConnected, OnFilteredBlockConnected: s.onFilteredBlockConnected, OnBlockDisconnected: s.onBlockDisconnected, @@ -556,7 +556,7 @@ func (s *NeutrinoClient) SetStartTime(startTime time.Time) { // onFilteredBlockConnected sends appropriate notifications to the notification // channel. func (s *NeutrinoClient) onFilteredBlockConnected(height int32, - header *wire.BlockHeader, relevantTxs []*btcutil.Tx) { + header *wire.BlockHeader, relevantTxs []*bisonwire.Tx) { ntfn := FilteredBlockConnected{ Block: &wtxmgr.BlockMeta{ Block: wtxmgr.Block{ @@ -567,7 +567,7 @@ func (s *NeutrinoClient) onFilteredBlockConnected(height int32, }, } for _, tx := range relevantTxs { - rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx(), + rec, err := wtxmgr.NewTxRecordFromMsgTx(s.chainParams.Chain, &tx.MsgTx, header.Timestamp) if err != nil { log.Errorf("Cannot create transaction record for "+ diff --git a/chain/utils_test.go b/chain/utils_test.go index bc8a43e..9befee3 100644 --- a/chain/utils_test.go +++ b/chain/utils_test.go @@ -1,11 +1,9 @@ package chain import ( - "fmt" "math" "runtime" "sync" - "time" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" @@ -120,73 +118,3 @@ func solveBlock(header *wire.BlockHeader) bool { return false } - -// genBlockChain generates a test chain with the given number of blocks. -func genBlockChain(numBlocks uint32) ([]*chainhash.Hash, map[chainhash.Hash]*wire.MsgBlock) { - prevHash := chainParams.GenesisHash - prevHeader := &chainParams.GenesisBlock.Header - - hashes := make([]*chainhash.Hash, numBlocks) - blocks := make(map[chainhash.Hash]*wire.MsgBlock, numBlocks) - - // Each block contains three transactions, including the coinbase - // transaction. Each non-coinbase transaction spends outputs from - // the previous block. We also need to produce blocks that succeed - // validation through blockchain.CheckBlockSanity. - script := []byte{0x01, 0x01} - createTx := func(prevOut wire.OutPoint) *wire.MsgTx { - return &wire.MsgTx{ - TxIn: []*wire.TxIn{{ - PreviousOutPoint: prevOut, - SignatureScript: script, - }}, - TxOut: []*wire.TxOut{{PkScript: script}}, - } - } - for i := uint32(0); i < numBlocks; i++ { - txs := []*wire.MsgTx{ - createTx(wire.OutPoint{Index: wire.MaxPrevOutIndex}), - createTx(wire.OutPoint{Hash: *prevHash, Index: 0}), - createTx(wire.OutPoint{Hash: *prevHash, Index: 1}), - } - header := &wire.BlockHeader{ - Version: 1, - PrevBlock: *prevHash, - MerkleRoot: calcMerkleRoot(txs), - Timestamp: prevHeader.Timestamp.Add(10 * time.Minute), - Bits: chainParams.PowLimitBits, - Nonce: 0, - } - if !solveBlock(header) { - panic(fmt.Sprintf("could not solve block at idx %v", i)) - } - block := &wire.MsgBlock{ - Header: *header, - Transactions: txs, - } - - blockHash := block.BlockHash() - hashes[i] = &blockHash - blocks[blockHash] = block - - prevHash = &blockHash - prevHeader = header - } - - return hashes, blocks -} - -// producesInvalidBlock produces a copy of the block that duplicates the last -// transaction. When the block has an odd number of transactions, this results -// in the invalid block maintaining the same hash as the valid block. -func produceInvalidBlock(block *wire.MsgBlock) *wire.MsgBlock { - numTxs := len(block.Transactions) - lastTx := block.Transactions[numTxs-1] - blockCopy := &wire.MsgBlock{ - Header: block.Header, - Transactions: make([]*wire.MsgTx, numTxs), - } - copy(blockCopy.Transactions, block.Transactions) - blockCopy.AddTransaction(lastTx) - return blockCopy -} diff --git a/cmd/utxowallet/config.go b/cmd/utxowallet/config.go index aee1790..f4e4738 100644 --- a/cmd/utxowallet/config.go +++ b/cmd/utxowallet/config.go @@ -52,6 +52,7 @@ type config struct { LogDir string `long:"logdir" description:"Directory to log output."` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` DBTimeout time.Duration `long:"dbtimeout" description:"The timeout value to use when opening the wallet database."` + DevRPC bool `long:"devrpc" description:"Turn on the dev RPC API on port 44825"` // Wallet options WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` @@ -364,5 +365,9 @@ func loadConfig() (*config, string, *netparams.ChainParams, error) { log.Warnf("%v", configFileError) } + if net == "simnet" && len(cfg.AddPeers) == 0 && len(cfg.ConnectPeers) == 0 { + cfg.AddPeers = append(cfg.AddPeers, "127.0.0.1:"+netParams.DefaultPort) + } + return &cfg, netDir, netParams, nil } diff --git a/cmd/utxowallet/rpc.sh b/cmd/utxowallet/rpc.sh new file mode 100755 index 0000000..6e883fb --- /dev/null +++ b/cmd/utxowallet/rpc.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Build JSON array manually +json='[' +first=1 +for arg in "$@"; do + # Escape double quotes and backslashes + escaped_arg=$(printf '%s' "$arg" | sed 's/\\/\\\\/g; s/"/\\"/g') + if [ $first -eq 1 ]; then + json+="\"$escaped_arg\"" + first=0 + else + json+=",\"$escaped_arg\"" + fi +done +json+=']' + +# Post to localhost:44825 +response=$(curl -sS -w "%{http_code}" -o /tmp/body.json -X POST http://localhost:44825/submit \ + -H "Content-Type: application/json" \ + -d "$json") + +status="${response: -3}" # last 3 characters = status code + +if [[ "$status" =~ ^2 ]]; then + cat /tmp/body.json | python3 -m json.tool +else + echo "Error ($status):" >&2 + cat /tmp/body.json >&2 +fi \ No newline at end of file diff --git a/cmd/utxowallet/utxowallet.go b/cmd/utxowallet/utxowallet.go index 43f568f..3768e98 100644 --- a/cmd/utxowallet/utxowallet.go +++ b/cmd/utxowallet/utxowallet.go @@ -5,6 +5,7 @@ package main import ( + "encoding/json" "fmt" "net" "net/http" @@ -12,14 +13,20 @@ import ( "os" "path/filepath" "runtime" + "strconv" "sync" "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/chain" "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/spv" + "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/bisoncraft/utxowallet/wallet" "github.com/bisoncraft/utxowallet/walletdb" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" ) var ( @@ -71,13 +78,13 @@ func walletMain() error { } loader := wallet.NewLoader( - netParams.BTCDParams(), netDir, true, cfg.DBTimeout, 250, + netParams, netDir, true, cfg.DBTimeout, 250, ) // Create and start chain RPC client so it's ready to connect to // the wallet when loaded later. if !cfg.NoInitialLoad { - go run(loader, netDir, netParams) + go run(loader, netDir, netParams, cfg.DevRPC) } if !cfg.NoInitialLoad { @@ -105,19 +112,10 @@ func walletMain() error { return nil } -func run(loader *wallet.Loader, netDir string, netParams *netparams.ChainParams) { +func run(loader *wallet.Loader, netDir string, chainParams *netparams.ChainParams, devRPC bool) { for { - var ( - chainClient chain.Interface - err error - ) - - var ( - chainService *spv.ChainService - spvdb walletdb.DB - ) - spvdb, err = walletdb.Create( + spvdb, err := walletdb.Create( "bdb", filepath.Join(netDir, "spv.db"), true, cfg.DBTimeout, ) @@ -126,12 +124,12 @@ func run(loader *wallet.Loader, netDir string, netParams *netparams.ChainParams) continue } defer spvdb.Close() - chainService, err = spv.NewChainService( + chainService, err := spv.NewChainService( spv.Config{ Chain: bisonwire.Chain(cfg.Chain), DataDir: netDir, Database: spvdb, - ChainParams: netParams, + ChainParams: chainParams, ConnectPeers: cfg.ConnectPeers, AddPeers: cfg.AddPeers, }) @@ -139,7 +137,7 @@ func run(loader *wallet.Loader, netDir string, netParams *netparams.ChainParams) log.Errorf("Couldn't create Neutrino ChainService: %s", err) continue } - chainClient = chain.NewNeutrinoClient(netParams, chainService) + chainClient := chain.NewNeutrinoClient(chainParams, chainService) err = chainClient.Start() if err != nil { log.Errorf("Couldn't start Neutrino client: %s", err) @@ -162,6 +160,10 @@ func run(loader *wallet.Loader, netDir string, netParams *netparams.ChainParams) if associate != nil { associate(w) } + // Run the dev rpc is not on mainnet + if devRPC { + go runDevRPC(w, chainService) + } }) chainClient.WaitForShutdown() @@ -188,3 +190,121 @@ func run(loader *wallet.Loader, netDir string, netParams *netparams.ChainParams) } } } + +func runDevRPC(wllt *wallet.Wallet, chainSvc *spv.ChainService) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Use POST", http.StatusMethodNotAllowed) + return + } + + var args []string + if err := json.NewDecoder(r.Body).Decode(&args); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + resp, err := handleRPCCall(wllt, chainSvc, args) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + b, err := json.Marshal(resp) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Errorf("JSON encode error: %v", err) + return + } + w.WriteHeader(http.StatusOK) + _, err = w.Write(append(b, byte('\n'))) + if err != nil { + log.Errorf("Write error: %v", err) + } + }) + http.ListenAndServe(":44825", nil) +} + +func handleRPCCall(w *wallet.Wallet, chainSvc *spv.ChainService, args []string) (any, error) { + if len(args) < 1 { + return nil, fmt.Errorf("no api method specified") + } + + method := args[0] + args = args[1:] + const acctNum, minConf, feePerKB = 0, 0, 10_000 + scope := waddrmgr.KeyScopeBIP0084 + switch method { + case "getbalance": + return w.CalculateAccountBalances(acctNum, minConf) + case "showaddress": + addr, err := w.CurrentAddress(acctNum, scope) + if err != nil { + return nil, err + } + return addr.String(), nil + case "getnewaddress": + addr, err := w.NewAddress(acctNum, scope) + if err != nil { + return nil, err + } + return addr.String(), nil + case "sendtoaddress": + if len(args) != 2 { + return nil, fmt.Errorf("wrong number of args. expected [address, amount]") + } + addrStr, amtStr := args[0], args[1] + addr, err := btcutil.DecodeAddress(addrStr, w.ChainParams()) + if err != nil { + return nil, fmt.Errorf("error decoding address %q: %w", addrStr, err) + } + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, fmt.Errorf("error generating pk script: %w", err) + } + amt, err := strconv.ParseFloat(amtStr, 64) + if err != nil { + return nil, fmt.Errorf("error parsing amount from %q: %w", amtStr, err) + } + v, err := btcutil.NewAmount(amt) + if err != nil { + return nil, fmt.Errorf("error creating amount from float: %w", err) + } + txOut := &wire.TxOut{ + Value: int64(v), + PkScript: pkScript, + } + msgTx, err := w.SendOutputs([]*wire.TxOut{txOut}, &scope, acctNum, minConf, feePerKB, wallet.CoinSelectionRandom, "dev-rpc") + if err != nil { + return nil, fmt.Errorf("error sending: %w", err) + } + h := msgTx.TxHash() + return h.String(), nil + case "gettransaction": + if len(args) != 1 { + return nil, fmt.Errorf("wrong number of args. expected [txid]") + } + txID := args[0] + txHash, err := chainhash.NewHashFromStr(txID) + if err != nil { + return nil, fmt.Errorf("error parsing tx hash from %q: %w", txID, err) + } + return w.GetTransaction(*txHash) + case "unlock": + if len(args) != 1 { + return nil, fmt.Errorf("wrong number of args. expected [password]") + } + if err := w.Unlock([]byte(args[0]), nil); err != nil { + return nil, fmt.Errorf("error unlocking: %w", err) + } + return "ok", nil + case "unbanall": + if err := chainSvc.UnbanPeers(); err != nil { + return nil, err + } + return "ok", nil + default: + return nil, fmt.Errorf("unknown method %q", method) + } +} diff --git a/cmd/utxowallet/walletsetup.go b/cmd/utxowallet/walletsetup.go index c841a44..557c943 100644 --- a/cmd/utxowallet/walletsetup.go +++ b/cmd/utxowallet/walletsetup.go @@ -21,7 +21,7 @@ import ( // provided path. func createWallet(cfg *config, netDir string, netParams *netparams.ChainParams) error { loader := wallet.NewLoader( - netParams.BTCDParams(), netDir, true, cfg.DBTimeout, 250, + netParams, netDir, true, cfg.DBTimeout, 250, ) // Start by prompting for the private passphrase. When there is an diff --git a/go.mod b/go.mod index 606852d..94f646e 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/bisoncraft/utxowallet go 1.23.4 -toolchain go1.23.6 - require ( github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcec/v2 v2.3.4 @@ -13,7 +11,6 @@ require ( github.com/btcsuite/btclog v1.0.0 github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd github.com/davecgh/go-spew v1.1.1 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/decred/dcrd/lru v1.0.0 github.com/golangci/golangci-lint v1.64.5 github.com/jessevdk/go-flags v1.6.1 @@ -28,6 +25,7 @@ require ( golang.org/x/crypto v0.35.0 golang.org/x/sync v0.11.0 golang.org/x/term v0.29.0 + lukechampine.com/blake3 v1.4.0 ) require ( @@ -72,6 +70,7 @@ require ( github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.5 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/ettle/strcase v0.2.0 // indirect @@ -123,6 +122,7 @@ require ( github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/kkdai/bstream v1.0.0 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect diff --git a/go.sum b/go.sum index 6343406..7470d1f 100644 --- a/go.sum +++ b/go.sum @@ -330,6 +330,8 @@ github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -845,6 +847,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= +lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= +lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= diff --git a/netparams/params.go b/netparams/params.go index b47d9d5..f7555c7 100644 --- a/netparams/params.go +++ b/netparams/params.go @@ -13,7 +13,10 @@ import ( "github.com/btcsuite/btcd/wire" ) +// ChainParams are an extended version of btcd/chaincfg.Params that omit some +// unnecessary fields and add some fields necessary for multi-asset needs. type ChainParams struct { + Chain string Name string Net wire.BitcoinNet GenesisBlock *wire.MsgBlock @@ -21,8 +24,12 @@ type ChainParams struct { TargetTimespan time.Duration TargetTimePerBlock time.Duration RetargetAdjustmentFactor int64 + ReduceMinDifficulty bool + MinDiffReductionTime time.Duration Checkpoints []chaincfg.Checkpoint PowLimit *big.Int + PowLimitBits uint32 + PoWNoRetargeting bool DNSSeeds []chaincfg.DNSSeed DefaultPort string @@ -49,15 +56,23 @@ type ChainParams struct { BIP0065Height int32 BIP0066Height int32 + CoinbaseMaturity uint16 + // CheckPoW is a function that will check the proof-of-work validity for a // block header. If CheckPoW is nil, the standard Bitcoin protocol is used. CheckPoW func(*wire.BlockHeader) error // MaxSatoshi varies between assets. MaxSatoshi int64 + + btcdParams *chaincfg.Params } +// BTCDParams are the btcd.chaincfg.Params that correspond the ChainParams. func (c *ChainParams) BTCDParams() *chaincfg.Params { - return &chaincfg.Params{ + if c.btcdParams != nil { + return c.btcdParams + } + c.btcdParams = &chaincfg.Params{ Name: c.Name, Net: c.Net, GenesisBlock: c.GenesisBlock, @@ -65,8 +80,12 @@ func (c *ChainParams) BTCDParams() *chaincfg.Params { TargetTimespan: c.TargetTimespan, TargetTimePerBlock: c.TargetTimePerBlock, RetargetAdjustmentFactor: c.RetargetAdjustmentFactor, + ReduceMinDifficulty: c.ReduceMinDifficulty, + MinDiffReductionTime: c.MinDiffReductionTime, Checkpoints: c.Checkpoints, PowLimit: c.PowLimit, + PowLimitBits: c.PowLimitBits, + PoWNoRetargeting: c.PoWNoRetargeting, DNSSeeds: c.DNSSeeds, DefaultPort: c.DefaultPort, Bech32HRPSegwit: c.Bech32HRPSegwit, @@ -81,5 +100,7 @@ func (c *ChainParams) BTCDParams() *chaincfg.Params { BIP0034Height: c.BIP0034Height, BIP0065Height: c.BIP0065Height, BIP0066Height: c.BIP0066Height, + CoinbaseMaturity: c.CoinbaseMaturity, } + return c.btcdParams } diff --git a/peer/log.go b/peer/log.go index 71bebd0..056d424 100644 --- a/peer/log.go +++ b/peer/log.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -176,9 +177,14 @@ func messageSummary(msg wire.Message) string { msg.TxHash(), len(msg.TxIn), len(msg.TxOut), formatLockTime(msg.LockTime)) - case *wire.MsgBlock: + case *bisonwire.Tx: + return fmt.Sprintf("hash %s, %d inputs, %d outputs, lock %s", + msg.TxHash(), len(msg.TxIn), len(msg.TxOut), + formatLockTime(msg.LockTime)) + + case *bisonwire.Block: header := &msg.Header - return fmt.Sprintf("hash %s, ver %d, %d tx, %s", msg.BlockHash(), + return fmt.Sprintf("hash %s, ver %d, %d tx, %s", msg.Header.BlockHash(), header.Version, len(msg.Transactions), header.Timestamp) case *wire.MsgInv: diff --git a/peer/peer.go b/peer/peer.go index 0c3a9c5..d71b2ae 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -1068,7 +1068,6 @@ func (p *Peer) handlePongMsg(msg *wire.MsgPong) { // readMessage reads the next bitcoin message from the peer with logging. func (p *Peer) readMessage(encoding wire.MessageEncoding) (wire.Message, []byte, error) { - n, msg, buf, err := bisonwire.ReadMessageWithEncodingN(p.conn, p.ProtocolVersion(), p.cfg.Chain, p.cfg.Net, encoding) atomic.AddUint64(&p.bytesReceived, uint64(n)) @@ -1420,9 +1419,8 @@ out: // matches bitcoind's behavior and is necessary since // compact blocks negotiation occurs after the // handshake. - if err == wire.ErrUnknownMessage { - log.Debugf("Received unknown message from %s:"+ - " %v", p, err) + if errors.Is(err, wire.ErrUnknownMessage) { + log.Debugf("Received unknown message from %s: %v", p, err) idleTimer.Reset(idleTimeout) continue } @@ -1510,14 +1508,14 @@ out: p.cfg.Listeners.OnMemPool(p, msg) } - case *wire.MsgTx: + case *bisonwire.Tx: if p.cfg.Listeners.OnTx != nil { - p.cfg.Listeners.OnTx(p, msg) + p.cfg.Listeners.OnTx(p, &msg.MsgTx) } - case *wire.MsgBlock: + case *bisonwire.Block: if p.cfg.Listeners.OnBlock != nil { - p.cfg.Listeners.OnBlock(p, msg, buf) + p.cfg.Listeners.OnBlock(p, msg.MsgBlock(), buf) } case *wire.MsgInv: @@ -1892,7 +1890,7 @@ out: // // This function is safe for concurrent access. func (p *Peer) QueueMessage(msg wire.Message, doneChan chan<- struct{}) { - p.QueueMessageWithEncoding(msg, doneChan, wire.BaseEncoding) + p.QueueMessageWithEncoding(msg, doneChan, wire.LatestEncoding) } // QueueMessageWithEncoding adds the passed bitcoin message to the peer send @@ -2174,7 +2172,7 @@ func (p *Peer) waitToFinishNegotiation(pver uint32) error { // have to wait for verack. for { remoteMsg, _, err := p.readMessage(wire.LatestEncoding) - if err == wire.ErrUnknownMessage { + if errors.Is(err, wire.ErrUnknownMessage) { continue } else if err != nil { return err @@ -2383,7 +2381,7 @@ func newPeerBase(origCfg *Config, inbound bool) *Peer { p := Peer{ inbound: inbound, - wireEncoding: wire.BaseEncoding, + wireEncoding: wire.LatestEncoding, knownInventory: lru.NewCache(maxKnownInventory), stallControl: make(chan stallControlMsg, 1), // nonblocking sync outputQueue: make(chan outMsg, outputBufferSize), diff --git a/spv/bamboozle_unit_test.go b/spv/bamboozle_unit_test.go index 8e5ad99..1fcd16b 100644 --- a/spv/bamboozle_unit_test.go +++ b/spv/bamboozle_unit_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/bisoncraft/utxowallet/assets" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/headerfs" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcutil/gcs" @@ -116,7 +117,7 @@ var ( // For the purpose of the cfheader mismatch test, we actually only need // to have the scripts of each transaction present. - block = &wire.MsgBlock{ + msgBlock = &wire.MsgBlock{ Transactions: []*wire.MsgTx{ { TxOut: []*wire.TxOut{ @@ -148,21 +149,21 @@ var ( }, }, } - correctFilter, _ = builder.BuildBasicFilter(block, nil) - oldFilter, _ = buildNonPushScriptFilter(block) - oldOldFilter, _ = buildAllPkScriptsFilter(block) + correctFilter, _ = builder.BuildBasicFilter(msgBlock, nil) + oldFilter, _ = buildNonPushScriptFilter(msgBlock) + oldOldFilter, _ = buildAllPkScriptsFilter(msgBlock) // a filter missing the first output of the block. missingElementFilter, _ = builder.BuildBasicFilter( &wire.MsgBlock{ - Transactions: block.Transactions[1:], + Transactions: msgBlock.Transactions[1:], }, nil, ) // a filter with one extra output script. extraElementFilter, _ = builder.BuildBasicFilter( &wire.MsgBlock{ - Transactions: append(block.Transactions, + Transactions: append(msgBlock.Transactions, &wire.MsgTx{ TxOut: []*wire.TxOut{ { @@ -684,7 +685,7 @@ func TestResolveFilterMismatchFromBlock(t *testing.T) { testCase := testCase t.Run(testCase.name, func(t *testing.T) { badPeers, err := resolveFilterMismatchFromBlock( - block, wire.GCSFilterRegular, testCase.peerFilters, + bisonwire.BlockFromMsgBlock("btc", msgBlock), wire.GCSFilterRegular, testCase.peerFilters, testCase.banThreshold, ) if err != nil { diff --git a/spv/banman/store.go b/spv/banman/store.go index 68b049e..82602a9 100644 --- a/spv/banman/store.go +++ b/spv/banman/store.go @@ -69,6 +69,9 @@ type Store interface { // UnbanIPNet removes the ban imposed on the specified peer. UnbanIPNet(ipNet *net.IPNet) error + + // UnbanAll removes all imposed bans. + UnbanAll() error } // NewStore returns a Store backed by a database. @@ -171,6 +174,32 @@ func (s *banStore) UnbanIPNet(ipNet *net.IPNet) error { return err } +// UnbanAll removes all imposed bans. +func (s *banStore) UnbanAll() error { + err := walletdb.Update(s.db, func(tx walletdb.ReadWriteTx) error { + banStore := tx.ReadWriteBucket(banStoreBucket) + if banStore == nil { + return ErrCorruptedStore + } + + banIndex := banStore.NestedReadWriteBucket(banBucket) + if banIndex == nil { + return ErrCorruptedStore + } + + reasonIndex := banStore.NestedReadWriteBucket(reasonBucket) + if reasonIndex == nil { + return ErrCorruptedStore + } + + return banIndex.ForEach(func(k, _ []byte) error { + return removeBannedIPNet(banIndex, reasonIndex, k) + }) + }) + + return err +} + // addBannedIPNet adds an entry to the ban store for the given IP network. func addBannedIPNet(banIndex, reasonIndex walletdb.ReadWriteBucket, ipNetKey []byte, reason Reason, duration time.Duration) error { @@ -246,8 +275,7 @@ func fetchStatus(banIndex, reasonIndex walletdb.ReadWriteBucket, // removeBannedIPNet removes all references to a banned IP network within the // ban store. -func removeBannedIPNet(banIndex, reasonIndex walletdb.ReadWriteBucket, - ipNetKey []byte) error { +func removeBannedIPNet(banIndex, reasonIndex walletdb.ReadWriteBucket, ipNetKey []byte) error { if err := banIndex.Delete(ipNetKey); err != nil { return err diff --git a/spv/batch_spend_reporter.go b/spv/batch_spend_reporter.go index 22e2008..6744087 100644 --- a/spv/batch_spend_reporter.go +++ b/spv/batch_spend_reporter.go @@ -1,6 +1,7 @@ package spv import ( + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) @@ -77,7 +78,8 @@ func (b *batchSpendReporter) NotifyUnspentAndUnfound() { // FailRemaining will return an error to all remaining requests in the event we // experience a critical rescan error. The error is threaded through to allow // the syntax: -// return reporter.FailRemaining(err) +// +// return reporter.FailRemaining(err) func (b *batchSpendReporter) FailRemaining(err error) error { for outpoint, requests := range b.requests { op := outpoint @@ -111,7 +113,7 @@ func (b *batchSpendReporter) notifyRequests( // spends may occur. Afterwards, any spends detected in the block are // immediately dispatched, and the watchlist updated in preparation of filtering // the next block. -func (b *batchSpendReporter) ProcessBlock(blk *wire.MsgBlock, +func (b *batchSpendReporter) ProcessBlock(blk *bisonwire.Block, newReqs []*GetUtxoRequest, height uint32) { // If any requests want the UTXOs at this height, scan the block to find @@ -165,7 +167,7 @@ func (b *batchSpendReporter) addNewRequests(reqs []*GetUtxoRequest) { // outpoint is not spent later on. Requests corresponding to outpoints that are // not found in the block will return a nil spend report to indicate that the // UTXO was not found. -func (b *batchSpendReporter) findInitialTransactions(block *wire.MsgBlock, +func (b *batchSpendReporter) findInitialTransactions(block *bisonwire.Block, newReqs []*GetUtxoRequest, height uint32) map[wire.OutPoint]*SpendReport { // First, construct a reverse index from txid to all a list of requests @@ -209,7 +211,7 @@ func (b *batchSpendReporter) findInitialTransactions(block *wire.MsgBlock, continue } - h := block.BlockHash() + h := block.Header.BlockHash() initialTxns[op] = &SpendReport{ Output: txOuts[op.Index], @@ -247,7 +249,7 @@ func (b *batchSpendReporter) findInitialTransactions(block *wire.MsgBlock, // notifySpends finds any transactions in the block that spend from our watched // outpoints. If a spend is detected, it is immediately delivered and cleaned up // from the reporter's internal state. -func (b *batchSpendReporter) notifySpends(block *wire.MsgBlock, +func (b *batchSpendReporter) notifySpends(block *bisonwire.Block, height uint32) map[wire.OutPoint]*SpendReport { spends := make(map[wire.OutPoint]*SpendReport) diff --git a/spv/blockmanager.go b/spv/blockmanager.go index 9b0a358..33c9a53 100644 --- a/spv/blockmanager.go +++ b/spv/blockmanager.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/spv/banman" "github.com/bisoncraft/utxowallet/spv/blockntfns" @@ -98,7 +99,7 @@ type blockManagerCfg struct { BanPeer func(addr string, reason banman.Reason) error // GetBlock fetches a block from the p2p network. - GetBlock func(chainhash.Hash, ...QueryOption) (*btcutil.Block, error) + GetBlock func(chainhash.Hash, ...QueryOption) (*bisonwire.BlockWithHeight, error) // firstPeerSignal is a channel that's sent upon once the main daemon // has made its first peer connection. We use this to ensure we don't @@ -1608,7 +1609,7 @@ func (b *blockManager) detectBadPeers(headers map[string]*wire.MsgCFHeaders, len(headers)) return resolveFilterMismatchFromBlock( - block.MsgBlock(), fType, filtersFromPeers, + block.Block, fType, filtersFromPeers, // We'll require a strict majority of our peers to agree on // filters. @@ -1633,7 +1634,7 @@ func (b *blockManager) detectBadPeers(headers map[string]*wire.MsgCFHeaders, // 3. If we cannot detect which filters are invalid from the block // contents, we ban peers serving filters different from the majority of // peers. -func resolveFilterMismatchFromBlock(block *wire.MsgBlock, +func resolveFilterMismatchFromBlock(block *bisonwire.Block, fType wire.FilterType, filtersFromPeers map[string]*gcs.Filter, threshold int) ([]string, error) { @@ -1664,7 +1665,7 @@ func resolveFilterMismatchFromBlock(block *wire.MsgBlock, // the inputs we can also derive most of the scripts of the // outputs being spent (at least for standard scripts). numOpReturns, err := VerifyBasicBlockFilter( - filter, btcutil.NewBlock(block), + filter, block, ) if err != nil { // Mark peer bad if we cannot verify its filter. @@ -2789,6 +2790,10 @@ func (b *blockManager) checkHeaderSanity(blockHeader *wire.BlockHeader, flags := blockchain.BehaviorFlags(0) if b.cfg.ChainParams.CheckPoW != nil { + // Adding BFFastAdd because of a slight difference in how Litecoin and + // Bitcoin handle block header sanity checks. See ltcd's + // calcNextRequiredDifficulty + // "Litecoin fixes an issue where a 51% can change..." flags |= blockchain.BFNoPoWCheck | blockchain.BFFastAdd if err := b.cfg.ChainParams.CheckPoW(blockHeader); err != nil { return err @@ -3065,16 +3070,21 @@ func ruleError(c blockchain.ErrorCode, desc string) blockchain.RuleError { // // The flags do not modify the behavior of this function directly, however they // are needed to pass along to checkBlockHeaderSanity. -func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, timeSource blockchain.MedianTimeSource) error { +func checkBlockSanity(block *bisonwire.Block, chainParams *netparams.ChainParams, timeSource blockchain.MedianTimeSource) error { flags := blockchain.BehaviorFlags(0) + msgBlock := block.MsgBlock() + if chainParams.CheckPoW != nil { + // Adding BFFastAdd because of a slight difference in how Litecoin and + // Bitcoin handle block header sanity checks. See ltcd's + // calcNextRequiredDifficulty + // "Litecoin fixes an issue where a 51% can change..." flags |= blockchain.BFNoPoWCheck | blockchain.BFFastAdd - if err := chainParams.CheckPoW(&block.MsgBlock().Header); err != nil { + if err := chainParams.CheckPoW(&msgBlock.Header); err != nil { return err } } - msgBlock := block.MsgBlock() header := &msgBlock.Header err := blockchain.CheckBlockHeaderSanity(header, chainParams.PowLimit, timeSource, flags) if err != nil { @@ -3106,15 +3116,15 @@ func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, } // The first transaction in a block must be a coinbase. - transactions := block.Transactions() - if !blockchain.IsCoinBase(transactions[0]) { + msgTxs := msgBlock.Transactions + if !blockchain.IsCoinBaseTx(msgTxs[0]) { return ruleError(blockchain.ErrFirstTxNotCoinbase, "first transaction in "+ "block is not a coinbase") } // A block must not have more than one coinbase. - for i, tx := range transactions[1:] { - if blockchain.IsCoinBase(tx) { + for i, tx := range msgTxs[1:] { + if blockchain.IsCoinBaseTx(tx) { str := fmt.Sprintf("block contains second coinbase at "+ "index %d", i+1) return ruleError(blockchain.ErrMultipleCoinbases, str) @@ -3123,7 +3133,7 @@ func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, // Do some preliminary checks on each transaction to ensure they are // sane before continuing. - for _, tx := range transactions { + for _, tx := range block.Transactions { err := checkTransactionSanity(tx, chainParams) if err != nil { return err @@ -3136,7 +3146,11 @@ func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, // checks. Bitcoind builds the tree here and checks the merkle root // after the following checks, but there is no reason not to check the // merkle root matches here. - calcMerkleRoot := blockchain.CalcMerkleRoot(block.Transactions(), false) + btcutilTxs := make([]*btcutil.Tx, len(block.Transactions)) + for i, tx := range block.Transactions { + btcutilTxs[i] = btcutil.NewTx(&tx.MsgTx) + } + calcMerkleRoot := blockchain.CalcMerkleRoot(btcutilTxs, false) if !header.MerkleRoot.IsEqual(&calcMerkleRoot) { str := fmt.Sprintf("block merkle root is invalid - block "+ "header indicates %v, but calculated value is %v", @@ -3148,7 +3162,7 @@ func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, // since the transaction hashes are already cached due to building the // merkle tree above. existingTxHashes := make(map[chainhash.Hash]struct{}) - for _, tx := range transactions { + for _, tx := range btcutilTxs { hash := tx.Hash() if _, exists := existingTxHashes[*hash]; exists { str := fmt.Sprintf("block contains duplicate "+ @@ -3161,7 +3175,7 @@ func checkBlockSanity(block *btcutil.Block, chainParams *netparams.ChainParams, // The number of signature operations must be less than the maximum // allowed per block. totalSigOps := 0 - for _, tx := range transactions { + for _, tx := range btcutilTxs { // We could potentially overflow the accumulator so check for // overflow. lastSigOps := totalSigOps @@ -3188,9 +3202,9 @@ func isNullOutpoint(outpoint *wire.OutPoint) bool { // checkTransactionSanity performs some preliminary checks on a transaction to // ensure it is sane. These checks are context free. -func checkTransactionSanity(tx *btcutil.Tx, chainParams *netparams.ChainParams) error { +func checkTransactionSanity(tx *bisonwire.Tx, chainParams *netparams.ChainParams) error { // A transaction must have at least one input. - msgTx := tx.MsgTx() + msgTx := &tx.MsgTx if len(msgTx.TxIn) == 0 { return ruleError(blockchain.ErrNoTxInputs, "transaction has no inputs") } @@ -3202,7 +3216,7 @@ func checkTransactionSanity(tx *btcutil.Tx, chainParams *netparams.ChainParams) // A transaction must not exceed the maximum allowed block payload when // serialized. - serializedTxSize := tx.MsgTx().SerializeSizeStripped() + serializedTxSize := msgTx.SerializeSizeStripped() if serializedTxSize > blockchain.MaxBlockBaseSize { str := fmt.Sprintf("serialized transaction is too big - got "+ "%d, max %d", serializedTxSize, blockchain.MaxBlockBaseSize) @@ -3260,7 +3274,7 @@ func checkTransactionSanity(tx *btcutil.Tx, chainParams *netparams.ChainParams) } // Coinbase script length must be between min and max length. - if blockchain.IsCoinBase(tx) { + if blockchain.IsCoinBaseTx(&tx.MsgTx) { slen := len(msgTx.TxIn[0].SignatureScript) if slen < blockchain.MinCoinbaseScriptLen || slen > blockchain.MaxCoinbaseScriptLen { str := fmt.Sprintf("coinbase transaction script length "+ diff --git a/spv/blockmanager_test.go b/spv/blockmanager_test.go index 4b7c237..2f7cd7c 100644 --- a/spv/blockmanager_test.go +++ b/spv/blockmanager_test.go @@ -752,7 +752,7 @@ func TestBlockManagerDetectBadPeers(t *testing.T) { filterBytes, _ = correctFilter.NBytes() filterHash, _ = builder.GetFilterHash(correctFilter) blockHeader = wire.BlockHeader{} - targetBlockHash = block.BlockHash() + targetBlockHash = msgBlock.BlockHash() peers = []string{"good1:1", "good2:1", "bad:1", "good3:1"} expBad = map[string]struct{}{ diff --git a/spv/cache_test.go b/spv/cache_test.go index 14df5f8..f00fc7c 100644 --- a/spv/cache_test.go +++ b/spv/cache_test.go @@ -4,10 +4,10 @@ import ( "crypto/rand" "testing" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/cache" "github.com/bisoncraft/utxowallet/spv/cache/lru" "github.com/bisoncraft/utxowallet/spv/filterdb" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -40,7 +40,7 @@ func TestBlockFilterCaches(t *testing.T) { var ( blockHashes []chainhash.Hash filters []*gcs.Filter - blocks []*btcutil.Block + blocks []*bisonwire.Block ) for i := 0; i < numElements; i++ { var blockHash chainhash.Hash @@ -65,7 +65,7 @@ func TestBlockFilterCaches(t *testing.T) { } msgBlock := &wire.MsgBlock{} - block := btcutil.NewBlock(msgBlock) + block := bisonwire.BlockFromMsgBlock("btc", msgBlock) blocks = append(blocks, block) // Add the block to the block caches, using the block INV @@ -74,7 +74,7 @@ func TestBlockFilterCaches(t *testing.T) { wire.InvTypeWitnessBlock, &blockHash, ) for _, c := range blockCaches { - _, _ = c.Put(*blockKey, &CacheableBlock{block}) + _, _ = c.Put(*blockKey, &CacheableBlock{&bisonwire.BlockWithHeight{Block: block}}) } } diff --git a/spv/cacheable_block.go b/spv/cacheable_block.go index 35cd523..4fec579 100644 --- a/spv/cacheable_block.go +++ b/spv/cacheable_block.go @@ -1,11 +1,11 @@ package spv -import "github.com/btcsuite/btcd/btcutil" +import "github.com/bisoncraft/utxowallet/bisonwire" // CacheableBlock is a wrapper around the btcutil.Block type which provides a // Size method used by the cache to target certain memory usage. type CacheableBlock struct { - *btcutil.Block + *bisonwire.BlockWithHeight } // Size returns size of this block in bytes. diff --git a/spv/neutrino.go b/spv/neutrino.go index c4e485a..35cc6c1 100644 --- a/spv/neutrino.go +++ b/spv/neutrino.go @@ -1110,6 +1110,11 @@ func (s *ChainService) UnbanPeer(addr string, parmanent bool) error { return s.ConnectNode(addr, parmanent) } +// UnbanPeers removes all imposed bans. +func (s *ChainService) UnbanPeers() error { + return s.banStore.UnbanAll() +} + // IsBanned returns true if the peer is banned, and false otherwise. func (s *ChainService) IsBanned(addr string) bool { ipNet, err := banman.ParseIPNet(addr, nil) diff --git a/spv/query.go b/spv/query.go index 2741f19..c841406 100644 --- a/spv/query.go +++ b/spv/query.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/banman" "github.com/bisoncraft/utxowallet/spv/cache" "github.com/bisoncraft/utxowallet/spv/filterdb" @@ -58,7 +59,7 @@ var ( // QueryEncoding specifies the default encoding (witness or not) for // `getdata` and other similar messages. - QueryEncoding = wire.WitnessEncoding + QueryEncoding = wire.LatestEncoding // ErrFilterFetchFailed is returned in case fetching a compact filter // fails. @@ -797,7 +798,7 @@ func (s *ChainService) GetCFilter(blockHash chainhash.Hash, // time, until one answers. If the block is found in the cache, it will be // returned immediately. func (s *ChainService) GetBlock(blockHash chainhash.Hash, - options ...QueryOption) (*btcutil.Block, error) { + options ...QueryOption) (*bisonwire.BlockWithHeight, error) { // Fetch the corresponding block header from the database. If this // isn't found, then we don't have the header for this block so we @@ -824,7 +825,7 @@ func (s *ChainService) GetBlock(blockHash chainhash.Hash, // If the block is already in the cache, we can return it immediately. blockValue, err := s.BlockCache.Get(*inv) if err == nil && blockValue != nil { - return blockValue.Block, err + return blockValue.BlockWithHeight, err } if err != nil && err != cache.ErrElementNotFound { return nil, err @@ -834,7 +835,7 @@ func (s *ChainService) GetBlock(blockHash chainhash.Hash, getData := wire.NewMsgGetData() _ = getData.AddInvVect(inv) - var foundBlock *btcutil.Block + var foundBlock *bisonwire.BlockWithHeight // handleResp will be called for each message received from a peer. It // will be used to signal to the work manager whether progress has been @@ -847,26 +848,24 @@ func (s *ChainService) GetBlock(blockHash chainhash.Hash, } // We're only interested in "block" responses. - response, ok := resp.(*wire.MsgBlock) + response, ok := resp.(*bisonwire.Block) if !ok { return noProgress } // If this isn't the block we asked for, ignore it. - if response.BlockHash() != blockHash { + if response.Header.BlockHash() != blockHash { return noProgress } - block := btcutil.NewBlock(response) - - // Only set height if btcutil hasn't automagically put one in. - if block.Height() == btcutil.BlockHeightUnknown { - block.SetHeight(int32(height)) + block := &bisonwire.BlockWithHeight{ + Block: response, + Height: height, } // If this claims our block but doesn't pass the sanity check, // the peer is trying to bamboozle us. if err := checkBlockSanity( - block, + block.Block, // We don't need to check PoW because by the time we get // here, it's been checked during header synchronization s.chainParams, @@ -886,7 +885,7 @@ func (s *ChainService) GetBlock(blockHash chainhash.Hash, } if err := blockchain.ValidateWitnessCommitment( - block, + btcutil.NewBlock(block.Block.MsgBlock()), ); err != nil { log.Warnf("Invalid block for %s received from %s: %v "+ "-- disconnecting peer", blockHash, peer, err) @@ -940,7 +939,7 @@ func (s *ChainService) GetBlock(blockHash chainhash.Hash, } // Add block to the cache before returning it. - _, err = s.BlockCache.Put(*inv, &CacheableBlock{Block: foundBlock}) + _, err = s.BlockCache.Put(*inv, &CacheableBlock{BlockWithHeight: foundBlock}) if err != nil { log.Warnf("couldn't write block to cache: %v", err) } diff --git a/spv/query/interface.go b/spv/query/interface.go index dca5f42..95cb6ac 100644 --- a/spv/query/interface.go +++ b/spv/query/interface.go @@ -11,15 +11,17 @@ const ( // allowed to be retried before it will fail. defaultQueryTimeout = time.Second * 30 - // defaultQueryEncoding specifies the default encoding (witness or not) - // for `getdata` and other similar messages. - defaultQueryEncoding = wire.WitnessEncoding - // defaultNumRetries is the default number of times that a query job // will be retried. defaultNumRetries = 2 ) +var ( + // defaultQueryEncoding specifies the default encoding (witness or not) + // for `getdata` and other similar messages. + defaultQueryEncoding = wire.LatestEncoding +) + // queries are a set of options that can be modified per-query, unlike global // options. type queryOptions struct { diff --git a/spv/query_test.go b/spv/query_test.go index cc42e3f..3a6b39d 100644 --- a/spv/query_test.go +++ b/spv/query_test.go @@ -1,6 +1,7 @@ package spv import ( + "bytes" "compress/bzip2" "encoding/binary" "fmt" @@ -12,13 +13,13 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/spv/cache/lru" "github.com/bisoncraft/utxowallet/spv/filterdb" "github.com/bisoncraft/utxowallet/spv/headerfs" "github.com/bisoncraft/utxowallet/spv/query" "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg" @@ -46,7 +47,7 @@ var ( // // NOTE: copied from btcsuite/btcd/database/ffldb/interface_test.go. func loadBlocks(t *testing.T, dataFile string, network wire.BitcoinNet) ( - []*btcutil.Block, error) { + []*bisonwire.Block, error) { // Open the file that contains the blocks for reading. fi, err := os.Open(dataFile) if err != nil { @@ -62,8 +63,8 @@ func loadBlocks(t *testing.T, dataFile string, network wire.BitcoinNet) ( dr := bzip2.NewReader(fi) // Set the first block as the genesis block. - blocks := make([]*btcutil.Block, 0, 256) - genesis := btcutil.NewBlock(chaincfg.MainNetParams.GenesisBlock) + blocks := make([]*bisonwire.Block, 0, 256) + genesis := bisonwire.BlockFromMsgBlock("btc", chaincfg.MainNetParams.GenesisBlock) blocks = append(blocks, genesis) // Load the remaining blocks. @@ -101,9 +102,8 @@ func loadBlocks(t *testing.T, dataFile string, network wire.BitcoinNet) ( return nil, err } - // Deserialize and store the block. - block, err := btcutil.NewBlockFromBytes(blockBytes) - if err != nil { + block := &bisonwire.Block{Chain: "btc"} + if err := block.BtcDecode(bytes.NewReader(blockBytes), 0, wire.LatestEncoding); err != nil { t.Errorf("Failed to parse block %v: %v", height, err) return nil, err } @@ -273,7 +273,7 @@ func TestBlockCache(t *testing.T) { } headers.WriteHeaders(header) - sz, _ := (&CacheableBlock{Block: b}).Size() + sz, _ := (&CacheableBlock{BlockWithHeight: &bisonwire.BlockWithHeight{Block: b}}).Size() if i < len(blocks)/2 { size += sz } @@ -314,11 +314,12 @@ func TestBlockCache(t *testing.T) { // Serve the block that matches the requested block header. for _, b := range blocks { - if *b.Hash() != inv.Hash { + blockHash := b.Header.BlockHash() + if blockHash != inv.Hash { continue } - header, _, err := headers.FetchHeader(b.Hash()) + header, _, err := headers.FetchHeader(&blockHash) require.NoError(t, err) resp := &wire.MsgBlock{ @@ -353,9 +354,11 @@ func TestBlockCache(t *testing.T) { t.Fatalf("error getting block: %v", err) } - if *found.Hash() != hash { + foundHash := found.Block.Header.BlockHash() + + if foundHash != hash { t.Fatalf("requested block with hash %v, got %v", - hash, found.Hash()) + hash, foundHash) } select { @@ -377,9 +380,11 @@ func TestBlockCache(t *testing.T) { t.Fatalf("error getting block: %v", err) } - if *found.Hash() != hash { + foundHash := found.Block.Header.BlockHash() + + if foundHash != hash { t.Fatalf("requested block with hash %v, got %v", - hash, found.Hash()) + hash, foundHash) } // Make sure we didn't query the peers for this block. @@ -393,19 +398,19 @@ func TestBlockCache(t *testing.T) { // Get the first half of the blocks. Since this is the first time we // request them, we expect them all to be fetched from peers. for _, b := range blocks[:len(blocks)/2] { - fetchAndAssertPeersQueried(*b.Hash()) + fetchAndAssertPeersQueried(b.Header.BlockHash()) } // Get the first half of the blocks again. This time we expect them all // to be fetched from the cache. for _, b := range blocks[:len(blocks)/2] { - fetchAndAssertInCache(*b.Hash()) + fetchAndAssertInCache(b.Header.BlockHash()) } // Get the second half of the blocks. These have not been fetched // before, and we expect them to be fetched from peers. for _, b := range blocks[len(blocks)/2:] { - fetchAndAssertPeersQueried(*b.Hash()) + fetchAndAssertPeersQueried(b.Header.BlockHash()) } // Since the cache only had capacity for the first half of the blocks, @@ -413,5 +418,5 @@ func TestBlockCache(t *testing.T) { // one, since we cannot know for sure how many because of the variable // size. b := blocks[0] - fetchAndAssertPeersQueried(*b.Hash()) + fetchAndAssertPeersQueried(b.Header.BlockHash()) } diff --git a/spv/rescan.go b/spv/rescan.go index b343b08..3c49fd6 100644 --- a/spv/rescan.go +++ b/spv/rescan.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/blockntfns" "github.com/bisoncraft/utxowallet/spv/headerfs" "github.com/btcsuite/btcd/btcjson" @@ -18,7 +19,6 @@ import ( "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) @@ -55,7 +55,7 @@ type ChainSource interface { GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, uint32, error) // GetBlock returns the block with the given hash. - GetBlock(chainhash.Hash, ...QueryOption) (*btcutil.Block, error) + GetBlock(chainhash.Hash, ...QueryOption) (*bisonwire.BlockWithHeight, error) // GetFilterHeaderByHeight returns the filter header of the block with // the given height. @@ -84,11 +84,21 @@ type ChainSource interface { // rescan progress. type ScanProgressHandler func(lastProcessedBlock uint32) +type NoteHandlers struct { + OnBlockConnected func(hash *chainhash.Hash, height int32, t time.Time) + OnFilteredBlockConnected func(height int32, header *wire.BlockHeader, txs []*bisonwire.Tx) + OnBlockDisconnected func(hash *chainhash.Hash, height int32, t time.Time) + OnFilteredBlockDisconnected func(height int32, header *wire.BlockHeader) + + // Just used in testing + OnRecvTx func(transaction *bisonwire.Tx, details *btcjson.BlockDetails) +} + // rescanOptions holds the set of functional parameters for Rescan. type rescanOptions struct { queryOptions []QueryOption - ntfn rpcclient.NotificationHandlers + ntfn NoteHandlers progressHandler ScanProgressHandler startTime time.Time @@ -123,7 +133,7 @@ func QueryOptions(options ...QueryOption) RescanOption { // NotificationHandlers specifies notification handlers for the rescan. These // will always run in the same goroutine as the caller. -func NotificationHandlers(ntfn rpcclient.NotificationHandlers) RescanOption { +func NotificationHandlers(ntfn NoteHandlers) RescanOption { return func(ro *rescanOptions) { ro.ntfn = ntfn } @@ -856,7 +866,7 @@ func (rs *rescanState) notifyBlock() error { // Find relevant transactions based on watch list. If scanning is // false, we can safely assume this block has no relevant transactions. - var relevantTxs []*btcutil.Tx + var relevantTxs []*bisonwire.Tx if len(ro.watchList) != 0 && rs.scanning { // If we have a non-empty watch list, then we need to see if it // matches the rescan's filters, so we get the basic filter @@ -985,7 +995,7 @@ func (rs *rescanState) handleBlockConnected(ntfn *blockntfns.Connected) error { // extractBlockMatches fetches the target block from the network, and filters // out any relevant transactions found within the block. func extractBlockMatches(chain ChainSource, ro *rescanOptions, - curStamp *headerfs.BlockStamp, filter *gcs.Filter) ([]*btcutil.Tx, + curStamp *headerfs.BlockStamp, filter *gcs.Filter) ([]*bisonwire.Tx, error) { // We've matched. Now we actually get the block and cycle through the @@ -999,26 +1009,27 @@ func extractBlockMatches(chain ChainSource, ro *rescanOptions, "network", curStamp.Height, curStamp.Hash) } + blockHeader := &block.Block.Header + // Before we go through the transactions, let's make sure the filter we // got from our peer is valid and includes all spent previous output // scripts. If there's a problem, the error returned here will be // interpreted by the block manager to disconnect/ban said peer. - if _, err := VerifyBasicBlockFilter(filter, block); err != nil { + if _, err := VerifyBasicBlockFilter(filter, block.Block); err != nil { return nil, fmt.Errorf("error verifying filter against "+ "downloaded block %d (%s), possibly got invalid "+ "filter from peer: %v", curStamp.Height, curStamp.Hash, err) } - blockHeader := block.MsgBlock().Header blockDetails := btcjson.BlockDetails{ - Height: block.Height(), - Hash: block.Hash().String(), + Height: int32(block.Height), + Hash: blockHeader.BlockHash().String(), Time: blockHeader.Timestamp.Unix(), } - relevantTxs := make([]*btcutil.Tx, 0, len(block.Transactions())) - for txIdx, tx := range block.Transactions() { + relevantTxs := make([]*bisonwire.Tx, 0, len(block.Block.Transactions)) + for txIdx, tx := range block.Block.Transactions { txDetails := blockDetails txDetails.Index = txIdx @@ -1026,9 +1037,6 @@ func extractBlockMatches(chain ChainSource, ro *rescanOptions, if ro.spendsWatchedInput(tx) { relevant = true - if ro.ntfn.OnRedeemingTx != nil { // nolint:staticcheck - ro.ntfn.OnRedeemingTx(tx, &txDetails) // nolint:staticcheck - } } // Even though the transaction may already be known as relevant @@ -1042,9 +1050,6 @@ func extractBlockMatches(chain ChainSource, ro *rescanOptions, if pays { relevant = true - if ro.ntfn.OnRecvTx != nil { // nolint:staticcheck - ro.ntfn.OnRecvTx(tx, &txDetails) // nolint:staticcheck - } } if relevant { @@ -1053,7 +1058,7 @@ func extractBlockMatches(chain ChainSource, ro *rescanOptions, chainSource, ok := chain.(*RescanChainSource) if ok { chainSource.broadcaster.MarkAsConfirmed( - *tx.Hash(), + tx.TxHash(), ) } } @@ -1107,7 +1112,7 @@ func (rs *rescanState) notifyBlockWithFilter(header *wire.BlockHeader, // Based on what we find within the block or the filter, we'll be // sending out a set of notifications with transactions that are // relevant to the rescan. - var relevantTxs []*btcutil.Tx + var relevantTxs []*bisonwire.Tx // If we actually have a filter, then we'll go ahead an attempt to // match the items within the filter to ensure we create any relevant @@ -1270,8 +1275,8 @@ func (ro *rescanOptions) updateFilter(chain ChainSource, update *updateOptions, // spendsWatchedInput returns whether the transaction matches the filter by // spending a watched input. -func (ro *rescanOptions) spendsWatchedInput(tx *btcutil.Tx) bool { - for _, in := range tx.MsgTx().TxIn { +func (ro *rescanOptions) spendsWatchedInput(tx *bisonwire.Tx) bool { + for _, in := range tx.TxIn { for _, input := range ro.watchInputs { switch { // If we're watching for a zero outpoint, then we should @@ -1300,11 +1305,11 @@ func (ro *rescanOptions) spendsWatchedInput(tx *btcutil.Tx) bool { // paysWatchedAddr returns whether the transaction matches the filter by having // an output paying to a watched address. If that is the case, this also // updates the filter to watch the newly created output going forward. -func (ro *rescanOptions) paysWatchedAddr(tx *btcutil.Tx) (bool, error) { +func (ro *rescanOptions) paysWatchedAddr(tx *bisonwire.Tx) (bool, error) { anyMatchingOutputs := false txOutLoop: - for outIdx, out := range tx.MsgTx().TxOut { + for outIdx, out := range tx.TxOut { pkScript := out.PkScript for _, addr := range ro.watchAddrs { @@ -1328,9 +1333,9 @@ txOutLoop: // Update the filter by also watching this created // outpoint for the event in the future that it's // spent. - hash := tx.Hash() + hash := tx.TxHash() outPoint := wire.OutPoint{ - Hash: *hash, + Hash: hash, Index: uint32(outIdx), } ro.watchInputs = append(ro.watchInputs, InputWithScript{ @@ -1505,7 +1510,7 @@ type SpendReport struct { // // NOTE: This field will only be populated if the target output has // been spent. - SpendingTx *wire.MsgTx + SpendingTx *bisonwire.Tx // SpendingTxIndex is the input index of the transaction above which // spends the target output. diff --git a/spv/rescan_test.go b/spv/rescan_test.go index df8cd69..f87133b 100644 --- a/spv/rescan_test.go +++ b/spv/rescan_test.go @@ -9,14 +9,13 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/blockntfns" "github.com/bisoncraft/utxowallet/spv/headerfs" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" ) @@ -32,7 +31,7 @@ type mockChainSource struct { blockHeightIndex map[chainhash.Hash]uint32 blockHashesByHeight map[uint32]*chainhash.Hash blockHeaders map[chainhash.Hash]*wire.BlockHeader - blocks map[chainhash.Hash]*btcutil.Block + blocks map[chainhash.Hash]*bisonwire.BlockWithHeight failGetFilter bool // if true, returns nil filter in GetCFilter filters map[chainhash.Hash]*gcs.Filter filterHeadersByHeight map[uint32]*chainhash.Hash @@ -50,7 +49,7 @@ func newMockChainSource(numBlocks int) *mockChainSource { blockHeightIndex: make(map[chainhash.Hash]uint32), blockHashesByHeight: make(map[uint32]*chainhash.Hash), blockHeaders: make(map[chainhash.Hash]*wire.BlockHeader), - blocks: make(map[chainhash.Hash]*btcutil.Block), + blocks: make(map[chainhash.Hash]*bisonwire.BlockWithHeight), filterHeadersByHeight: make(map[uint32]*chainhash.Hash), filters: make(map[chainhash.Hash]*gcs.Filter), } @@ -61,7 +60,12 @@ func newMockChainSource(numBlocks int) *mockChainSource { chain.blockHeightIndex[*genesisHash] = 0 chain.blockHashesByHeight[0] = genesisHash chain.blockHeaders[*genesisHash] = &genesisBlock.Header - chain.blocks[*genesisHash] = btcutil.NewBlock(genesisBlock) + chain.blocks[*genesisHash] = &bisonwire.BlockWithHeight{ + Block: &bisonwire.Block{ + Header: genesisBlock.Header, + Transactions: []*bisonwire.Tx{{Chain: "btc", MsgTx: *genesisBlock.Transactions[0]}}, + }, + } filter, _ := gcs.FromBytes(0, builder.DefaultP, builder.DefaultM, nil) chain.filters[*genesisHash] = filter @@ -115,7 +119,7 @@ func (c *mockChainSource) addNewBlockWithHeader(header *wire.BlockHeader, c.blockHeightIndex[newHash] = newHeight c.blockHashesByHeight[newHeight] = &newHash c.blockHeaders[newHash] = header - c.blocks[newHash] = btcutil.NewBlock(wire.NewMsgBlock(header)) + c.blocks[newHash] = &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", wire.NewMsgBlock(header))} newFilter, _ := gcs.FromBytes(0, builder.DefaultP, builder.DefaultM, nil) c.filters[newHash] = newFilter @@ -238,7 +242,7 @@ func (c *mockChainSource) GetBlockHeader( // GetBlock returns the block with the given hash. func (c *mockChainSource) GetBlock(hash chainhash.Hash, - _ ...QueryOption) (*btcutil.Block, error) { + _ ...QueryOption) (*bisonwire.BlockWithHeight, error) { c.mu.Lock() defer c.mu.Unlock() @@ -247,6 +251,7 @@ func (c *mockChainSource) GetBlock(hash chainhash.Hash, if !ok { return nil, errors.New("block not found") } + return block, nil } @@ -355,9 +360,9 @@ func newRescanTestContext(t *testing.T, numBlocks int, // nolint:unparam blocksConnected := make(chan headerfs.BlockStamp) blocksDisconnected := make(chan headerfs.BlockStamp) - ntfnHandlers := rpcclient.NotificationHandlers{ + ntfnHandlers := NoteHandlers{ OnFilteredBlockConnected: func(height int32, - header *wire.BlockHeader, _ []*btcutil.Tx) { + header *wire.BlockHeader, _ []*bisonwire.Tx) { blocksConnected <- headerfs.BlockStamp{ Hash: header.BlockHash(), diff --git a/spv/sync_test.go b/spv/sync_test.go index 7149ca4..a83f80b 100644 --- a/spv/sync_test.go +++ b/spv/sync_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/bisoncraft/utxowallet/assets" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv" "github.com/bisoncraft/utxowallet/spv/banman" "github.com/bisoncraft/utxowallet/spv/headerfs" @@ -181,13 +182,13 @@ var ( // transactions we're interested in that are in the blockchain we're // following as signalled by OnBlockConnected, OnBlockDisconnected, // OnRecvTx, and OnRedeemingTx. - ourKnownTxsByBlock = make(map[chainhash.Hash][]*btcutil.Tx) + ourKnownTxsByBlock = make(map[chainhash.Hash][]*bisonwire.Tx) // ourKnownTxsByFilteredBlock lets the rescan goroutine keep track of // transactions we're interested in that are in the blockchain we're // following as signalled by OnFilteredBlockConnected and // OnFilteredBlockDisconnected. - ourKnownTxsByFilteredBlock = make(map[chainhash.Hash][]*btcutil.Tx) + ourKnownTxsByFilteredBlock = make(map[chainhash.Hash][]*bisonwire.Tx) ) // secSource is an implementation of btcwallet/txauthor/SecretsSource that @@ -885,23 +886,24 @@ func testRandomBlocks(harness *neutrinoHarness, t *testing.T) { return } // Check that network and RPC blocks match. + msgBlock := haveBlock.Block.MsgBlock() if !reflect.DeepEqual( - *haveBlock.MsgBlock(), *wantBlock, + *msgBlock, *wantBlock, ) { errChan <- fmt.Errorf("Block from network "+ "doesn't match block from RPC. Want: "+ "%s, RPC: %s, network: %s", blockHash, wantBlock.BlockHash(), - haveBlock.MsgBlock().BlockHash()) + msgBlock.BlockHash()) return } // Check that block height matches what we have. - if height != uint32(haveBlock.Height()) { + if height != uint32(haveBlock.Height) { errChan <- fmt.Errorf("Block height from "+ "network doesn't match expected "+ "height. Want: %v, network: %v", - height, haveBlock.Height()) + height, haveBlock.Height) return } // Get basic cfilter from network. @@ -943,7 +945,7 @@ func testRandomBlocks(harness *neutrinoHarness, t *testing.T) { } inputScripts, err := fetchPrevInputScripts( - haveBlock.MsgBlock(), + msgBlock, harness.h1, ) if err != nil { @@ -954,7 +956,7 @@ func testRandomBlocks(harness *neutrinoHarness, t *testing.T) { // Calculate basic filter from block. calcFilter, err := builder.BuildBasicFilter( - haveBlock.MsgBlock(), inputScripts, + msgBlock, inputScripts, ) if err != nil { errChan <- fmt.Errorf("Couldn't build basic "+ @@ -1396,7 +1398,7 @@ func startRescan(t *testing.T, svc *spv.ChainService, addr btcutil.Address, spv.WatchAddrs(addr), spv.StartBlock(startBlock), spv.NotificationHandlers( - rpcclient.NotificationHandlers{ + spv.NoteHandlers{ OnBlockConnected: func( hash *chainhash.Hash, height int32, time time.Time) { @@ -1418,7 +1420,7 @@ func startRescan(t *testing.T, svc *spv.ChainService, addr btcutil.Address, curBlockHeight = height - 1 rescanMtx.Unlock() }, - OnRecvTx: func(tx *btcutil.Tx, + OnRecvTx: func(tx *bisonwire.Tx, details *btcjson.BlockDetails) { rescanMtx.Lock() @@ -1439,31 +1441,31 @@ func startRescan(t *testing.T, svc *spv.ChainService, addr btcutil.Address, []byte("rv")...) rescanMtx.Unlock() }, - OnRedeemingTx: func(tx *btcutil.Tx, - details *btcjson.BlockDetails) { - - rescanMtx.Lock() - hash, err := chainhash. - NewHashFromStr( - details.Hash) - if err != nil { - t.Errorf("Couldn't "+ - "decode hash "+ - "%s: %s", - details.Hash, - err) - } - ourKnownTxsByBlock[*hash] = append( - ourKnownTxsByBlock[*hash], - tx) - gotLog = append(gotLog, - []byte("rd")...) - rescanMtx.Unlock() - }, + // OnRedeemingTx: func(tx *btcutil.Tx, + // details *btcjson.BlockDetails) { + + // rescanMtx.Lock() + // hash, err := chainhash. + // NewHashFromStr( + // details.Hash) + // if err != nil { + // t.Errorf("Couldn't "+ + // "decode hash "+ + // "%s: %s", + // details.Hash, + // err) + // } + // ourKnownTxsByBlock[*hash] = append( + // ourKnownTxsByBlock[*hash], + // tx) + // gotLog = append(gotLog, + // []byte("rd")...) + // rescanMtx.Unlock() + // }, OnFilteredBlockConnected: func( height int32, header *wire.BlockHeader, - relevantTxs []*btcutil.Tx) { + relevantTxs []*bisonwire.Tx) { rescanMtx.Lock() ourKnownTxsByFilteredBlock[header.BlockHash()] = diff --git a/spv/utxoscanner.go b/spv/utxoscanner.go index d6a0240..5df2728 100644 --- a/spv/utxoscanner.go +++ b/spv/utxoscanner.go @@ -6,8 +6,8 @@ import ( "sync/atomic" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/headerfs" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" ) @@ -94,7 +94,7 @@ type UtxoScannerConfig struct { BlockFilterMatches func(ro *rescanOptions, blockHash *chainhash.Hash) (bool, error) // GetBlock fetches a block from the p2p network. - GetBlock func(chainhash.Hash, ...QueryOption) (*btcutil.Block, error) + GetBlock func(chainhash.Hash, ...QueryOption) (*bisonwire.BlockWithHeight, error) } // UtxoScanner batches calls to GetUtxo so that a single scan can search for @@ -368,7 +368,7 @@ scanToEnd: log.Debugf("Processing block height=%d hash=%s", height, hash) - reporter.ProcessBlock(block.MsgBlock(), newReqs, height) + reporter.ProcessBlock(block.Block, newReqs, height) reporter.NotifyProgress(height) } diff --git a/spv/utxoscanner_test.go b/spv/utxoscanner_test.go index f448f2f..8b0020f 100644 --- a/spv/utxoscanner_test.go +++ b/spv/utxoscanner_test.go @@ -7,15 +7,15 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/spv/headerfs" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) type MockChainClient struct { - getBlockResponse map[chainhash.Hash]*btcutil.Block + getBlockResponse map[chainhash.Hash]*bisonwire.BlockWithHeight getBlockHashResponse map[int64]*chainhash.Hash getBestBlockHash *chainhash.Hash getBestBlockHeight int32 @@ -24,18 +24,18 @@ type MockChainClient struct { func NewMockChainClient() *MockChainClient { return &MockChainClient{ - getBlockResponse: make(map[chainhash.Hash]*btcutil.Block), + getBlockResponse: make(map[chainhash.Hash]*bisonwire.BlockWithHeight), getBlockHashResponse: make(map[int64]*chainhash.Hash), getCFilterResponse: make(map[chainhash.Hash]*gcs.Filter), } } -func (c *MockChainClient) SetBlock(hash *chainhash.Hash, block *btcutil.Block) { +func (c *MockChainClient) SetBlock(hash *chainhash.Hash, block *bisonwire.BlockWithHeight) { c.getBlockResponse[*hash] = block } func (c *MockChainClient) GetBlockFromNetwork(blockHash chainhash.Hash, - options ...QueryOption) (*btcutil.Block, error) { + options ...QueryOption) (*bisonwire.BlockWithHeight, error) { return c.getBlockResponse[blockHash], nil } @@ -93,7 +93,7 @@ func TestFindSpends(t *testing.T) { // Test that finding spends with an empty outpoints index returns no // spends. r := newBatchSpendReporter() - spends := r.notifySpends(&Block100000, height) + spends := r.notifySpends(bisonwire.BlockFromMsgBlock("btc", &Block100000), height) if len(spends) != 0 { t.Fatalf("unexpected number of spend reports -- "+ "want %d, got %d", 0, len(spends)) @@ -103,7 +103,7 @@ func TestFindSpends(t *testing.T) { r.addNewRequests(reqs) // Ensure that a spend report is now returned. - spends = r.notifySpends(&Block100000, height) + spends = r.notifySpends(bisonwire.BlockFromMsgBlock("btc", &Block100000), height) if len(spends) != 1 { t.Fatalf("unexpected number of spend reports -- "+ "want %d, got %d", 1, len(spends)) @@ -131,7 +131,7 @@ func TestFindInitialTransactions(t *testing.T) { // First, try to find the outpoint within the block. r := newBatchSpendReporter() - initialTxns := r.findInitialTransactions(&Block100000, reqs, height) + initialTxns := r.findInitialTransactions(bisonwire.BlockFromMsgBlock("btc", &Block100000), reqs, height) if len(initialTxns) != 1 { t.Fatalf("unexpected number of spend reports -- "+ "want %v, got %v", 1, len(initialTxns)) @@ -148,7 +148,7 @@ func TestFindInitialTransactions(t *testing.T) { // Try to find the invalid outpoint in the same block. r = newBatchSpendReporter() - initialTxns = r.findInitialTransactions(&Block100000, reqs, height) + initialTxns = r.findInitialTransactions(bisonwire.BlockFromMsgBlock("btc", &Block100000), reqs, height) if len(initialTxns) != 1 { t.Fatalf("unexpected number of spend reports -- "+ "want %v, got %v", 1, len(initialTxns)) @@ -167,7 +167,7 @@ func TestFindInitialTransactions(t *testing.T) { // Try to find the outpoint with an invalid txid in the same block. r = newBatchSpendReporter() - initialTxns = r.findInitialTransactions(&Block100000, reqs, height) + initialTxns = r.findInitialTransactions(bisonwire.BlockFromMsgBlock("btc", &Block100000), reqs, height) if len(initialTxns) != 1 { t.Fatalf("unexpected number of spend reports -- "+ "want %v, got %v", 1, len(initialTxns)) @@ -328,11 +328,11 @@ func TestUtxoScannerScanBasic(t *testing.T) { block99999Hash := Block99999.BlockHash() mockChainClient.SetBlockHash(99999, &block99999Hash) - mockChainClient.SetBlock(&block99999Hash, btcutil.NewBlock(&Block99999)) + mockChainClient.SetBlock(&block99999Hash, &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", &Block99999)}) block100000Hash := Block100000.BlockHash() mockChainClient.SetBlockHash(100000, &block100000Hash) - mockChainClient.SetBlock(&block100000Hash, btcutil.NewBlock(&Block100000)) + mockChainClient.SetBlock(&block100000Hash, &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", &Block100000)}) mockChainClient.SetBestSnapshot(&block100000Hash, 100000) scanner := NewUtxoScanner(&UtxoScannerConfig{ @@ -384,12 +384,12 @@ func TestUtxoScannerScanAddBlocks(t *testing.T) { block99999Hash := Block99999.BlockHash() mockChainClient.SetBlockHash(99999, &block99999Hash) - mockChainClient.SetBlock(&block99999Hash, btcutil.NewBlock(&Block99999)) + mockChainClient.SetBlock(&block99999Hash, &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", &Block99999)}) mockChainClient.SetBestSnapshot(&block99999Hash, 99999) block100000Hash := Block100000.BlockHash() mockChainClient.SetBlockHash(100000, &block100000Hash) - mockChainClient.SetBlock(&block100000Hash, btcutil.NewBlock(&Block100000)) + mockChainClient.SetBlock(&block100000Hash, &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", &Block100000)}) var snapshotLock sync.Mutex waitForSnapshot := make(chan struct{}) @@ -465,7 +465,7 @@ func TestUtxoScannerCancelRequest(t *testing.T) { block100000Hash := Block100000.BlockHash() mockChainClient.SetBlockHash(100000, &block100000Hash) - mockChainClient.SetBlock(&block100000Hash, btcutil.NewBlock(&Block100000)) + mockChainClient.SetBlock(&block100000Hash, &bisonwire.BlockWithHeight{Block: bisonwire.BlockFromMsgBlock("btc", &Block100000)}) mockChainClient.SetBestSnapshot(&block100000Hash, 100000) fetchErr := errors.New("cannot fetch block") @@ -476,7 +476,7 @@ func TestUtxoScannerCancelRequest(t *testing.T) { block := make(chan struct{}) scanner := NewUtxoScanner(&UtxoScannerConfig{ GetBlock: func(chainhash.Hash, - ...QueryOption) (*btcutil.Block, error) { + ...QueryOption) (*bisonwire.BlockWithHeight, error) { <-block return nil, fetchErr diff --git a/spv/verification.go b/spv/verification.go index 8a50a3f..8702450 100644 --- a/spv/verification.go +++ b/spv/verification.go @@ -3,7 +3,7 @@ package spv import ( "fmt" - "github.com/btcsuite/btcd/btcutil" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/btcsuite/btcd/btcutil/gcs" "github.com/btcsuite/btcd/btcutil/gcs/builder" "github.com/btcsuite/btcd/txscript" @@ -12,21 +12,23 @@ import ( // VerifyBasicBlockFilter asserts that a given block filter was constructed // correctly and according to the rules of BIP-0158 to contain both the output's // pk scripts as well as the pk scripts the inputs are spending. -func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, +func VerifyBasicBlockFilter(filter *gcs.Filter, block *bisonwire.Block) (int, error) { + blockHash := block.Header.BlockHash() + var ( opReturnMatches int - key = builder.DeriveKey(block.Hash()) + key = builder.DeriveKey(&blockHash) ) - for idx, tx := range block.Transactions() { + for idx, tx := range block.Transactions { // Skip coinbase transaction. if idx == 0 { continue } // Check outputs first. - for outIdx, txOut := range tx.MsgTx().TxOut { + for outIdx, txOut := range tx.TxOut { switch { // If the script itself is blank, then we'll skip this // as it doesn't contain any useful information. @@ -53,7 +55,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, return 0, fmt.Errorf("error "+ "validating block %v outpoint "+ "%v:%d script %x: %v", - block.Hash(), tx.Hash(), outIdx, + blockHash, tx.TxHash(), outIdx, txOut.PkScript, err) } @@ -72,7 +74,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, if err != nil { return 0, fmt.Errorf("error validating block "+ "%v outpoint %v:%d script %x: %v", - block.Hash(), tx.Hash(), outIdx, + block.Header.BlockHash(), tx.TxHash(), outIdx, txOut.PkScript, err) } @@ -80,7 +82,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, return 0, fmt.Errorf("filter for block %v is "+ "invalid, outpoint %v:%d script %x "+ "wasn't matched by filter", - block.Hash(), tx.Hash(), outIdx, + block.Header.BlockHash(), tx.TxHash(), outIdx, txOut.PkScript) } } @@ -89,7 +91,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, // also included any pk scripts of the outputs being _spent_. // We can do this for witness items since the witness always // contains the full script as the last element on the stack. - for inIdx, in := range tx.MsgTx().TxIn { + for inIdx, in := range tx.TxIn { // There are too many edge cases to cover for non- // witness scripts. And in LN land we're interested in // witness spends only anyway. Therefore let's skip any @@ -115,7 +117,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, "input %d of tx %v in block %v "+ "because script type is not supported "+ "for validating against filter", inIdx, - tx.Hash(), block.Hash()) + tx.TxHash(), block.Header.BlockHash()) continue } @@ -127,7 +129,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, log.Debug("Skipping filter validation for "+ "input %d of tx %v in block %v "+ "because computing the script failed: "+ - "%v", inIdx, block.Hash(), err) + "%v", inIdx, block.Header.BlockHash(), err) continue } @@ -136,7 +138,7 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, if err != nil { return 0, fmt.Errorf("error validating block "+ "%v input %d of tx %v script %x: %v", - block.Hash(), inIdx, tx.Hash(), + block.Header.BlockHash(), inIdx, tx.TxHash(), script.Script(), err) } @@ -146,8 +148,8 @@ func VerifyBasicBlockFilter(filter *gcs.Filter, block *btcutil.Block) (int, "pk script %x which wasn't matched by "+ "filter. The input likely spends a "+ "taproot output which is not yet"+ - "supported", block.Hash(), inIdx, - tx.Hash(), script.Script()) + "supported", block.Header.BlockHash(), inIdx, + tx.TxHash(), script.Script()) } } } diff --git a/spv/verification_test.go b/spv/verification_test.go index 9838535..499d957 100644 --- a/spv/verification_test.go +++ b/spv/verification_test.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "testing" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" @@ -116,9 +117,8 @@ func TestVerifyBlockFilter(t *testing.T) { // contains all the entries we require according to BIP-158. utxoSet := []*wire.MsgTx{prevTx} validFilter := filterFromBlock(t, utxoSet, spendBlock, true) - b := btcutil.NewBlock(spendBlock) - opReturnValid, err := VerifyBasicBlockFilter(validFilter, b) + opReturnValid, err := VerifyBasicBlockFilter(validFilter, bisonwire.BlockFromMsgBlock("btc", spendBlock)) require.NoError(t, err) require.Equal(t, 1, opReturnValid) } diff --git a/waddrmgr/address.go b/waddrmgr/address.go index fe4c021..836ab3d 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -397,7 +397,7 @@ func (a *managedAddress) ExportPrivKey() (*btcutil.WIF, error) { return nil, err } - return btcutil.NewWIF(pk, a.manager.rootManager.chainParams, a.compressed) + return btcutil.NewWIF(pk, a.manager.rootManager.btcParams, a.compressed) } // DerivationInfo contains the information required to derive the key that @@ -535,7 +535,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, // First, we'll generate a normal p2wkh address from the pubkey hash. witAddr, err := btcutil.NewAddressWitnessPubKeyHash( - pubKeyHash, m.rootManager.chainParams, + pubKeyHash, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -553,7 +553,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, // witnessProgram as the sigScript, then present the proper // pair as the witness. address, err = btcutil.NewAddressScriptHash( - witnessProgram, m.rootManager.chainParams, + witnessProgram, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -561,7 +561,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, case PubKeyHash: address, err = btcutil.NewAddressPubKeyHash( - pubKeyHash, m.rootManager.chainParams, + pubKeyHash, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -569,7 +569,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, case WitnessPubKey: address, err = btcutil.NewAddressWitnessPubKeyHash( - pubKeyHash, m.rootManager.chainParams, + pubKeyHash, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -578,7 +578,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, case TaprootPubKey: tapKey := txscript.ComputeTaprootKeyNoScript(pubKey) address, err = btcutil.NewAddressTaproot( - schnorr.SerializePubKey(tapKey), m.rootManager.chainParams, + schnorr.SerializePubKey(tapKey), m.rootManager.btcParams, ) if err != nil { return nil, err @@ -881,7 +881,7 @@ func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, scriptEncrypted []byte) (*scriptAddress, error) { address, err := btcutil.NewAddressScriptHashFromHash( - scriptHash, m.rootManager.chainParams, + scriptHash, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -988,7 +988,7 @@ func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent, switch witnessVersion { case witnessVersionV0: address, err := btcutil.NewAddressWitnessScriptHash( - scriptIdent, m.rootManager.chainParams, + scriptIdent, m.rootManager.btcParams, ) if err != nil { return nil, err @@ -1007,7 +1007,7 @@ func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent, case witnessVersionV1: address, err := btcutil.NewAddressTaproot( - scriptIdent, m.rootManager.chainParams, + scriptIdent, m.rootManager.btcParams, ) if err != nil { return nil, err diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index 3b6668b..6f89c4b 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/assets" "github.com/bisoncraft/utxowallet/walletdb" _ "github.com/bisoncraft/utxowallet/walletdb/bdb" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -287,12 +288,12 @@ func setupManager(t *testing.T) (tearDownFunc func(), db walletdb.DB, mgr *Manag } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if err != nil { diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 09900be..6e49bae 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -13,6 +13,7 @@ import ( "time" "github.com/bisoncraft/utxowallet/internal/zero" + "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/snacl" "github.com/bisoncraft/utxowallet/spv/cache/lru" "github.com/bisoncraft/utxowallet/walletdb" @@ -351,7 +352,8 @@ type Manager struct { locked atomic.Bool closed bool - chainParams *chaincfg.Params + chainParams *netparams.ChainParams + btcParams *chaincfg.Params // masterKeyPub is the secret key used to secure the cryptoKeyPub key // and masterKeyPriv is the secret key used to secure the cryptoKeyPriv @@ -866,7 +868,7 @@ func (m *Manager) ChainParams() *chaincfg.Params { // NOTE: No need for mutex here since the net field does not change // after the manager instance is created. - return m.chainParams + return m.btcParams } // ChangePassphrase changes either the public or private passphrase to the @@ -1369,7 +1371,7 @@ func (m *Manager) Decrypt(keyType CryptoKeyType, in []byte) ([]byte, error) { } // newManager returns a new locked address manager with the given parameters. -func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, +func newManager(chainParams *netparams.ChainParams, masterKeyPub *snacl.SecretKey, masterKeyPriv *snacl.SecretKey, cryptoKeyPub EncryptorDecryptor, cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, syncInfo *syncState, birthday time.Time, privPassphraseSalt [saltSize]byte, @@ -1377,6 +1379,7 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, m := &Manager{ chainParams: chainParams, + btcParams: chainParams.BTCDParams(), syncState: *syncInfo, birthday: birthday, masterKeyPub: masterKeyPub, @@ -1504,7 +1507,7 @@ func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error { // the passed opened database. The public passphrase is required to decrypt // the public keys. func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte, - chainParams *chaincfg.Params) (*Manager, error) { + chainParams *netparams.ChainParams) (*Manager, error) { // Verify the version is neither too old or too new. version, err := fetchManagerVersion(ns) @@ -1650,7 +1653,7 @@ func loadManager(ns walletdb.ReadBucket, pubPassphrase []byte, // A ManagerError with an error code of ErrNoExist will be returned if the // passed manager does not exist in the specified namespace. func Open(ns walletdb.ReadBucket, pubPassphrase []byte, - chainParams *chaincfg.Params) (*Manager, error) { + chainParams *netparams.ChainParams) (*Manager, error) { // Return an error if the manager has NOT already been created in the // given database namespace. @@ -1792,7 +1795,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, // namespace. func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey, pubPassphrase, privPassphrase []byte, - chainParams *chaincfg.Params, config *ScryptOptions, + chainParams *netparams.ChainParams, config *ScryptOptions, birthday time.Time) error { // If the seed argument is nil we create in watchingOnly mode. diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index 6bf826d..1cf5220 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/assets" "github.com/bisoncraft/utxowallet/snacl" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcec/v2" @@ -1818,7 +1819,7 @@ func testConvertWatchingOnly(tc *testContext) bool { err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if err != nil { @@ -1858,7 +1859,7 @@ func testConvertWatchingOnly(tc *testContext) bool { err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if err != nil { @@ -1986,7 +1987,7 @@ func testManagerCase(t *testing.T, caseName string, // returned. err := walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) - _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) + _, err := Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if !checkManagerError(t, "Open non-existent", err, ErrNoExist) { @@ -2003,12 +2004,12 @@ func testManagerCase(t *testing.T, caseName string, } err = Create( ns, caseKey, pubPassphrase, casePrivPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2033,7 +2034,7 @@ func testManagerCase(t *testing.T, caseName string, ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) return Create( ns, caseKey, pubPassphrase, casePrivPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) }) if !checkManagerError(t, fmt.Sprintf("(%s) Create existing", caseName), @@ -2092,7 +2093,7 @@ func testManagerCase(t *testing.T, caseName string, err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if err != nil { @@ -2183,7 +2184,7 @@ func TestManagerHigherVersion(t *testing.T) { // should expect to see the error ErrUpgrade. err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) - _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) + _, err := Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if !checkManagerError(t, "Upgrade needed", err, ErrUpgrade) { @@ -2207,7 +2208,7 @@ func TestManagerHigherVersion(t *testing.T) { // ErrUpgrade. err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) - _, err := Open(ns, pubPassphrase, &chaincfg.MainNetParams) + _, err := Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if !checkManagerError(t, "Upgrade needed", err, ErrUpgrade) { @@ -2329,13 +2330,13 @@ func TestScopedKeyManagerManagement(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2495,7 +2496,7 @@ func TestScopedKeyManagerManagement(t *testing.T) { err = walletdb.View(db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2586,13 +2587,13 @@ func TestRootHDKeyNeutering(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2678,13 +2679,13 @@ func TestNewRawAccount(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2737,12 +2738,12 @@ func TestNewRawAccountWatchingOnly(t *testing.T) { } err = Create( ns, nil, pubPassphrase, nil, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -2804,12 +2805,12 @@ func TestNewRawAccountHybrid(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) return err }) if err != nil { @@ -2924,12 +2925,12 @@ func TestDeriveFromKeyPathCache(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -3022,12 +3023,12 @@ func TestTaprootPubKeyDerivation(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } @@ -3164,12 +3165,12 @@ func TestManagedAddressValidation(t *testing.T) { } err = Create( ns, rootKey, pubPassphrase, privPassphrase, - &chaincfg.MainNetParams, fastScrypt, time.Time{}, + assets.BTCParams["mainnet"], fastScrypt, time.Time{}, ) if err != nil { return err } - mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + mgr, err = Open(ns, pubPassphrase, assets.BTCParams["mainnet"]) if err != nil { return err } diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index e6ac9b5..3c84603 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1075,7 +1075,7 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, nextIndex) return nil, managerError(ErrKeyChain, str, err) } - key.SetNet(s.rootManager.chainParams) + key.SetNet(s.rootManager.btcParams) nextIndex++ nextKey = key @@ -1305,7 +1305,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, nextIndex) return managerError(ErrKeyChain, str, err) } - key.SetNet(s.rootManager.chainParams) + key.SetNet(s.rootManager.btcParams) nextIndex++ nextKey = key @@ -1910,7 +1910,7 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, // Ensure the address is intended for network the address manager is // associated with. - if !wif.IsForNet(s.rootManager.chainParams) { + if !wif.IsForNet(s.rootManager.btcParams) { str := fmt.Sprintf("private key is not for the same network the "+ "address manager is configured for (%s)", s.rootManager.chainParams.Name) @@ -1992,7 +1992,7 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, case NestedWitnessPubKey: pubKeyHash := btcutil.Hash160(serializedPubKey) p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash( - pubKeyHash, s.rootManager.chainParams, + pubKeyHash, s.rootManager.btcParams, ) if err != nil { return err @@ -2380,7 +2380,7 @@ func (s *ScopedKeyManager) ChainParams() *chaincfg.Params { // NOTE: No need for mutex here since the net field does not change // after the manager instance is created. - return s.rootManager.chainParams + return s.rootManager.btcParams } // AccountName returns the account name for the given account number stored in diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 79b25f0..b1af835 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -103,9 +103,7 @@ func (w *Wallet) handleChainNotifications() { return ErrWalletShuttingDown } - log.Errorf("Unable to synchronize "+ - "wallet to chain, trying "+ - "again in %s: %v", + log.Errorf("Unable to synchronize wallet to chain, trying again in %s: %v", w.syncRetryInterval, err) continue @@ -152,8 +150,7 @@ func (w *Wallet) handleChainNotifications() { err = waitForSync(birthdayBlock) if err != nil { - log.Infof("Stopped waiting for wallet "+ - "sync due to error: %v", err) + log.Infof("Stopped waiting for wallet sync due to error: %v", err) return } @@ -338,9 +335,9 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, // Check every output to determine whether it is controlled by a wallet // key. If so, mark the output as a credit. - for i, output := range rec.MsgTx.TxOut { + for i, output := range rec.Tx.TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, - w.chainParams) + w.btcParams) if err != nil { // Non-standard outputs are skipped. continue diff --git a/wallet/createtx.go b/wallet/createtx.go index 272dd77..1684fc2 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -316,7 +316,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, if tx.ChangeIndex >= 0 { changePkScript := tx.Tx.TxOut[tx.ChangeIndex].PkScript _, addrs, _, err := txscript.ExtractPkScriptAddrs( - changePkScript, w.chainParams, + changePkScript, w.btcParams, ) if err != nil { return err @@ -388,7 +388,7 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, // TODO: Handle multisig outputs by determining if enough of the // addresses are controlled. _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) + output.PkScript, w.btcParams) if err != nil || len(addrs) != 1 { continue } diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index b9f6e2f..9a2c9f7 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -177,7 +177,7 @@ func addUtxo(t *testing.T, w *Wallet, incomingTx *wire.MsgTx) { } txBytes := b.Bytes() - rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) + rec, err := wtxmgr.NewTxRecord(w.chainParams.Chain, txBytes, time.Now()) if err != nil { t.Fatalf("unable to create tx record: %v", err) } @@ -367,7 +367,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { require.Len(t, tx1.Tx.TxOut, 2) for _, txOut := range tx1.Tx.TxOut { scriptType, _, _, err := txscript.ExtractPkScriptAddrs( - txOut.PkScript, w.chainParams, + txOut.PkScript, w.btcParams, ) require.NoError(t, err) @@ -397,7 +397,7 @@ func TestCreateSimpleCustomChange(t *testing.T) { } scriptType, _, _, err := txscript.ExtractPkScriptAddrs( - txOut.PkScript, w.chainParams, + txOut.PkScript, w.btcParams, ) require.NoError(t, err) diff --git a/wallet/example_test.go b/wallet/example_test.go index 617d290..6ec9856 100644 --- a/wallet/example_test.go +++ b/wallet/example_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/assets" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/btcsuite/btcd/chaincfg" ) // defaultDBTimeout specifies the timeout value when opening the wallet @@ -38,7 +38,7 @@ func testWallet(t *testing.T) (*Wallet, func()) { privPass := []byte("world") loader := NewLoader( - &chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250, + assets.BTCParams["testnet"], dir, true, defaultDBTimeout, 250, WithWalletSyncRetryInterval(10*time.Millisecond), ) w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now()) @@ -70,7 +70,7 @@ func testWalletWatchingOnly(t *testing.T) (*Wallet, func()) { pubPass := []byte("hello") loader := NewLoader( - &chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250, + assets.BTCParams["testnet"], dir, true, defaultDBTimeout, 250, WithWalletSyncRetryInterval(10*time.Millisecond), ) w, err := loader.CreateNewWatchingOnlyWallet(pubPass, time.Now()) diff --git a/wallet/import.go b/wallet/import.go index 7efa9ed..472e9c0 100644 --- a/wallet/import.go +++ b/wallet/import.go @@ -157,29 +157,24 @@ func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey, // The public key must have a version corresponding to the current // chain. if !w.isPubKeyForNet(pubKey) { - return fmt.Errorf("expected extended public key for current "+ - "network %v", w.chainParams.Name) + return fmt.Errorf("expected extended public key for current network %v", w.chainParams.Name) } // Verify the extended public key's depth and child index based on // whether it's an account key or not. if isAccountKey { if pubKey.Depth() != accountPubKeyDepth { - return errors.New("invalid account key, must be of the " + - "form m/purpose'/coin_type'/account'") + return errors.New("invalid account key, must be of the form m/purpose'/coin_type'/account'") } if pubKey.ChildIndex() < hdkeychain.HardenedKeyStart { return errors.New("invalid account key, must be hardened") } } else { if pubKey.Depth() != pubKeyDepth { - return errors.New("invalid account key, must be of the " + - "form m/purpose'/coin_type'/account'/change/" + - "address_index") + return errors.New("invalid account key, must be of the form m/purpose'/coin_type'/account'/change/address_index") } if pubKey.ChildIndex() >= hdkeychain.HardenedKeyStart { - return errors.New("invalid pulic key, must not be " + - "hardened") + return errors.New("invalid pulic key, must not be hardened") } } diff --git a/wallet/loader.go b/wallet/loader.go index 2e9eff7..2ed0ce5 100644 --- a/wallet/loader.go +++ b/wallet/loader.go @@ -13,6 +13,7 @@ import ( "time" "github.com/bisoncraft/utxowallet/internal/prompt" + "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -75,7 +76,8 @@ func WithWalletSyncRetryInterval(interval time.Duration) LoaderOption { type Loader struct { cfg *loaderConfig callbacks []func(*Wallet) - chainParams *chaincfg.Params + chainParams *netparams.ChainParams + btcParams *chaincfg.Params netDir string noFreelistSync bool timeout time.Duration @@ -91,7 +93,7 @@ type Loader struct { // NewLoader constructs a Loader with an optional recovery window. If the // recovery window is non-zero, the wallet will attempt to recovery addresses // starting from the last SyncedTo height. -func NewLoader(chainParams *chaincfg.Params, netDir string, +func NewLoader(chainParams *netparams.ChainParams, netDir string, noFreelistSync bool, timeout time.Duration, recoveryWindow uint32, opts ...LoaderOption) *Loader { @@ -102,6 +104,7 @@ func NewLoader(chainParams *chaincfg.Params, netDir string, return &Loader{ cfg: cfg, + btcParams: chainParams.BTCDParams(), chainParams: chainParams, netDir: netDir, noFreelistSync: noFreelistSync, @@ -115,7 +118,7 @@ func NewLoader(chainParams *chaincfg.Params, netDir string, // users are free to use their own walletdb implementation (eg. leveldb, etcd) // to store the wallet. Given that the external DB may be shared an additional // function is also passed which will override Loader.WalletExists(). -func NewLoaderWithDB(chainParams *chaincfg.Params, recoveryWindow uint32, +func NewLoaderWithDB(chainParams *netparams.ChainParams, recoveryWindow uint32, db walletdb.DB, walletExists func() (bool, error), opts ...LoaderOption) (*Loader, error) { @@ -198,7 +201,7 @@ func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte, } // Derive the master extended key from the seed. - rootKey, err = hdkeychain.NewMaster(seed, l.chainParams) + rootKey, err = hdkeychain.NewMaster(seed, l.btcParams) if err != nil { return nil, fmt.Errorf("failed to derive master " + "extended key") diff --git a/wallet/mock.go b/wallet/mock.go index af904fa..a235274 100644 --- a/wallet/mock.go +++ b/wallet/mock.go @@ -3,6 +3,7 @@ package wallet import ( "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/chain" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/btcsuite/btcd/btcutil" @@ -31,7 +32,7 @@ func (m *mockChainClient) GetBestBlock() (*chainhash.Hash, int32, error) { return nil, m.getBestBlockHeight, nil } -func (m *mockChainClient) GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) { +func (m *mockChainClient) GetBlock(*chainhash.Hash) (*bisonwire.BlockWithHeight, error) { return nil, nil } diff --git a/wallet/multisig.go b/wallet/multisig.go index 0e2a6cd..46726d5 100644 --- a/wallet/multisig.go +++ b/wallet/multisig.go @@ -60,7 +60,7 @@ func (w *Wallet) MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]b PubKey().SerializeCompressed() pubKeyAddr, err := btcutil.NewAddressPubKey( - serializedPubKey, w.chainParams) + serializedPubKey, w.btcParams) if err != nil { return nil, err } @@ -101,7 +101,7 @@ func (w *Wallet) ImportP2SHRedeemScript(script []byte) (*btcutil.AddressScriptHa // This function will never error as it always // hashes the script to the correct length. p2shAddr, _ = btcutil.NewAddressScriptHash(script, - w.chainParams) + w.btcParams) return nil } return err diff --git a/wallet/notifications.go b/wallet/notifications.go index ee7152a..ef53188 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -52,7 +52,7 @@ func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetai // TODO: Debits should record which account(s?) they // debit from so this doesn't need to be looked up. - prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint + prevOP := &details.Tx.TxIn[deb.Index].PreviousOutPoint prev, err := w.TxStore.TxDetails(txmgrNs, &prevOP.Hash) if err != nil { log.Errorf("Cannot query previous transaction details for %v: %v", prevOP.Hash, err) @@ -62,8 +62,8 @@ func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetai log.Errorf("Missing previous transaction %v", prevOP.Hash) return 0 } - prevOut := prev.MsgTx.TxOut[prevOP.Index] - _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOut.PkScript, w.chainParams) + prevOut := prev.Tx.TxOut[prevOP.Index] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOut.PkScript, w.btcParams) var inputAcct uint32 if err == nil && len(addrs) > 0 { _, inputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) @@ -80,8 +80,8 @@ func lookupOutputChain(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetail addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) - output := details.MsgTx.TxOut[cred.Index] - _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + output := details.Tx.TxOut[cred.Index] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.btcParams) var ma waddrmgr.ManagedAddress if err == nil && len(addrs) > 0 { ma, err = w.Manager.Address(addrmgrNs, addrs[0]) @@ -99,18 +99,18 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T serializedTx := details.SerializedTx if serializedTx == nil { var buf bytes.Buffer - err := details.MsgTx.Serialize(&buf) + err := details.Tx.Serialize(&buf) if err != nil { log.Errorf("Transaction serialization: %v", err) } serializedTx = buf.Bytes() } var fee btcutil.Amount - if len(details.Debits) == len(details.MsgTx.TxIn) { + if len(details.Debits) == len(details.Tx.TxIn) { for _, deb := range details.Debits { fee += deb.Amount } - for _, txOut := range details.MsgTx.TxOut { + for _, txOut := range details.Tx.TxOut { fee -= btcutil.Amount(txOut.Value) } } @@ -125,8 +125,8 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T } } } - outputs := make([]TransactionSummaryOutput, 0, len(details.MsgTx.TxOut)) - for i := range details.MsgTx.TxOut { + outputs := make([]TransactionSummaryOutput, 0, len(details.Tx.TxOut)) + for i := range details.Tx.TxOut { credIndex := len(outputs) mine := len(details.Credits) > credIndex && details.Credits[credIndex].Index == uint32(i) if !mine { @@ -161,7 +161,7 @@ func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]btcutil.Amount) output := &unspent[i] var outputAcct uint32 _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) + output.PkScript, w.btcParams) if err == nil && len(addrs) > 0 { _, outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } diff --git a/wallet/rescan.go b/wallet/rescan.go index ca52aa9..eaf89ec 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -285,7 +285,7 @@ func (w *Wallet) rescanWithTarget(addrs []btcutil.Address, outpoints := make(map[wire.OutPoint]btcutil.Address, len(unspent)) for _, output := range unspent { _, outputAddrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams, + output.PkScript, w.btcParams, ) if err != nil { return err diff --git a/wallet/signer.go b/wallet/signer.go index 9410718..3293420 100644 --- a/wallet/signer.go +++ b/wallet/signer.go @@ -50,7 +50,7 @@ func (w *Wallet) ScriptForOutput(output *wire.TxOut) ( // single push of the p2wkh witness program corresponding to // the matching public key of this address. p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash( - pubKeyHash, w.chainParams, + pubKeyHash, w.btcParams, ) if err != nil { return nil, nil, nil, err diff --git a/wallet/utxos.go b/wallet/utxos.go index 1e5e0fc..182f761 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -61,7 +61,7 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut // Ignore outputs that are not controlled by the account. _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, - w.chainParams) + w.btcParams) if err != nil || len(addrs) == 0 { // Cannot determine which account this belongs // to without a valid address. TODO: Fix this @@ -129,7 +129,7 @@ func (w *Wallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, // passed output script. This function is used to look up the proper key which // should be used to sign a specified input. func (w *Wallet) fetchOutputAddr(script []byte) (waddrmgr.ManagedAddress, error) { - _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.chainParams) + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.btcParams) if err != nil { return nil, err } @@ -167,14 +167,14 @@ func (w *Wallet) FetchOutpointInfo(prevOut *wire.OutPoint) (*wire.MsgTx, // we actually have control of this output. We do this because the // check above only guarantees that the transaction is somehow relevant // to us, like in the event of us being the sender of the transaction. - numOutputs := uint32(len(txDetail.TxRecord.MsgTx.TxOut)) + numOutputs := uint32(len(txDetail.TxRecord.Tx.TxOut)) if prevOut.Index >= numOutputs { return nil, nil, 0, fmt.Errorf("invalid output index %v for "+ "transaction with %v outputs", prevOut.Index, numOutputs) } - pkScript := txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].PkScript + pkScript := txDetail.TxRecord.Tx.TxOut[prevOut.Index].PkScript // Determine the number of confirmations the output currently has. _, currentHeight, err := w.chainClient.GetBestBlock() @@ -188,8 +188,8 @@ func (w *Wallet) FetchOutpointInfo(prevOut *wire.OutPoint) (*wire.MsgTx, confs = int64(currentHeight - txDetail.Block.Height) } - return &txDetail.TxRecord.MsgTx, &wire.TxOut{ - Value: txDetail.TxRecord.MsgTx.TxOut[prevOut.Index].Value, + return &txDetail.TxRecord.Tx.MsgTx, &wire.TxOut{ + Value: txDetail.TxRecord.Tx.TxOut[prevOut.Index].Value, PkScript: pkScript, }, confs, nil } diff --git a/wallet/wallet.go b/wallet/wallet.go index 36eeedb..696c8f9 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -16,6 +16,7 @@ import ( "time" "github.com/bisoncraft/utxowallet/chain" + "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/waddrmgr" "github.com/bisoncraft/utxowallet/wallet/txauthor" "github.com/bisoncraft/utxowallet/wallet/txrules" @@ -163,7 +164,8 @@ type Wallet struct { NtfnServer *NotificationServer - chainParams *chaincfg.Params + chainParams *netparams.ChainParams + btcParams *chaincfg.Params wg sync.WaitGroup started bool @@ -704,7 +706,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, // We'll initialize the recovery manager with a default batch size of // 2000. recoveryMgr := NewRecoveryManager( - w.recoveryWindow, recoveryBatchSize, w.chainParams, + w.recoveryWindow, recoveryBatchSize, w.btcParams, ) // In the event that this recovery is being resumed, we will need to @@ -908,8 +910,8 @@ expandHorizons: // in the filter blocks response. This ensures that these transactions // and their outputs are tracked when the final rescan is performed. for _, txn := range filterResp.RelevantTxns { - txRecord, err := wtxmgr.NewTxRecordFromMsgTx( - txn, filterResp.BlockMeta.Time, + txRecord, err := wtxmgr.NewTxRecordBisonTx( + w.chainParams.Chain, txn, filterResp.BlockMeta.Time, ) if err != nil { return err @@ -1667,7 +1669,7 @@ func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balan var outputAcct uint32 _, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) + output.PkScript, w.btcParams) if err == nil && len(addrs) > 0 { _, outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } @@ -2079,7 +2081,7 @@ func (c CreditCategory) String() string { // TODO: This is intended for use by the RPC server and should be moved out of // this package at a later time. func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params) CreditCategory { - if blockchain.IsCoinBaseTx(&details.MsgTx) { + if blockchain.IsCoinBaseTx(&details.Tx.MsgTx) { if confirmed(int32(net.CoinbaseMaturity), details.Block.Height, syncHeight) { return CreditGenerate @@ -2112,20 +2114,20 @@ func listTransactions(tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *wa results := []btcjson.ListTransactionsResult{} txHashStr := details.Hash.String() received := details.Received.Unix() - generated := blockchain.IsCoinBaseTx(&details.MsgTx) + generated := blockchain.IsCoinBaseTx(&details.Tx.MsgTx) recvCat := RecvCategory(details, syncHeight, net).String() send := len(details.Debits) != 0 // Fee can only be determined if every input is a debit. var feeF64 float64 - if len(details.Debits) == len(details.MsgTx.TxIn) { + if len(details.Debits) == len(details.Tx.TxIn) { var debitTotal btcutil.Amount for _, deb := range details.Debits { debitTotal += deb.Amount } var outputTotal btcutil.Amount - for _, output := range details.MsgTx.TxOut { + for _, output := range details.Tx.TxOut { outputTotal += btcutil.Amount(output.Value) } // Note: The actual fee is debitTotal - outputTotal. However, @@ -2135,7 +2137,7 @@ func listTransactions(tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *wa } outputs: - for i, output := range details.MsgTx.TxOut { + for i, output := range details.Tx.TxOut { // Determine if this output is a credit, and if so, determine // its spentness. var isCredit bool @@ -2232,7 +2234,7 @@ func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTra jsonResults := listTransactions( tx, &detail, w.Manager, syncHeight, - w.chainParams, + w.btcParams, ) txList = append(txList, jsonResults...) } @@ -2279,7 +2281,7 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe } jsonResults := listTransactions(tx, &details[i], - w.Manager, syncBlock.Height, w.chainParams) + w.Manager, syncBlock.Height, w.btcParams) txList = append(txList, jsonResults...) if len(jsonResults) > 0 { @@ -2314,9 +2316,9 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjso detail := &details[i] for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + pkScript := detail.Tx.TxOut[cred.Index].PkScript _, addrs, _, err := txscript.ExtractPkScriptAddrs( - pkScript, w.chainParams) + pkScript, w.btcParams) if err != nil || len(addrs) != 1 { continue } @@ -2330,7 +2332,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjso } jsonResults := listTransactions(tx, detail, - w.Manager, syncBlock.Height, w.chainParams) + w.Manager, syncBlock.Height, w.btcParams) txList = append(txList, jsonResults...) continue loopDetails } @@ -2362,7 +2364,7 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // reverse order they were marked mined. for i := len(details) - 1; i >= 0; i-- { jsonResults := listTransactions(tx, &details[i], w.Manager, - syncBlock.Height, w.chainParams) + syncBlock.Height, w.btcParams) txList = append(txList, jsonResults...) } return false, nil @@ -2611,7 +2613,7 @@ func (w *Wallet) Accounts(scope waddrmgr.KeyScope) (*AccountsResult, error) { for i := range unspent { output := unspent[i] var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.btcParams) if err == nil && len(addrs) > 0 { _, outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } @@ -2689,7 +2691,7 @@ func (w *Wallet) AccountBalances(scope waddrmgr.KeyScope, output.Height, syncBlock.Height) { continue } - _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.btcParams) if err != nil || len(addrs) == 0 { continue } @@ -2805,7 +2807,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, // grouped under the associated account in the db. outputAcctName := defaultAccountName sc, addrs, _, err := txscript.ExtractPkScriptAddrs( - output.PkScript, w.chainParams) + output.PkScript, w.btcParams) if err != nil { continue } @@ -2915,7 +2917,7 @@ func (w *Wallet) ListLeasedOutputs() ([]*ListLeasedOutputResult, error) { continue } - txOut := details.MsgTx.TxOut[output.Outpoint.Index] + txOut := details.Tx.TxOut[output.Outpoint.Index] result := &ListLeasedOutputResult{ LockedOutput: output, @@ -3333,9 +3335,9 @@ func (w *Wallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope, for i := range details { detail := &details[i] for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + pkScript := detail.Tx.TxOut[cred.Index].PkScript var outputAcct uint32 - _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, w.chainParams) + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, w.btcParams) if err == nil && len(addrs) > 0 { _, outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0]) } @@ -3382,9 +3384,9 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu for i := range details { detail := &details[i] for _, cred := range detail.Credits { - pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + pkScript := detail.Tx.TxOut[cred.Index].PkScript _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, - w.chainParams) + w.btcParams) // An error creating addresses from the output script only // indicates a non-standard script, so ignore this credit. if err != nil { @@ -3528,7 +3530,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, return fmt.Errorf("%v not found", txIn.PreviousOutPoint) } - prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript + prevOutScript = txDetails.Tx.TxOut[prevIndex].PkScript } inputFetcher.AddPrevOut(txIn.PreviousOutPoint, &wire.TxOut{ PkScript: prevOutScript, @@ -3732,7 +3734,7 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, // we'll write this tx to disk as an unconfirmed transaction. This way, // upon restarts, we'll always rebroadcast it, and also add it to our // set of records. - txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + txRec, err := wtxmgr.NewTxRecordFromMsgTx(w.chainParams.Chain, tx, time.Now()) if err != nil { return nil, err } @@ -3744,7 +3746,7 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, addrmgrNs := dbTx.ReadWriteBucket(waddrmgrNamespaceKey) for _, txOut := range tx.TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs( - txOut.PkScript, w.chainParams, + txOut.PkScript, w.btcParams, ) if err != nil { // Non-standard outputs can safely be skipped because @@ -3816,7 +3818,7 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) - txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + txRec, err := wtxmgr.NewTxRecordFromMsgTx(w.chainParams.Chain, tx, time.Now()) if err != nil { return err } @@ -3842,7 +3844,7 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { // wallet won't be accurate. dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) - txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + txRec, err := wtxmgr.NewTxRecordFromMsgTx(w.chainParams.Chain, tx, time.Now()) if err != nil { return err } @@ -3877,7 +3879,7 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { // ChainParams returns the network parameters for the blockchain the wallet // belongs to. func (w *Wallet) ChainParams() *chaincfg.Params { - return w.chainParams + return w.btcParams } // Database returns the underlying walletdb database. This method is provided @@ -3890,11 +3892,11 @@ func (w *Wallet) Database() walletdb.DB { // CreateWithCallback is the same as Create with an added callback that will be // called in the same transaction the wallet structure is initialized. func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte, - rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params, + rootKey *hdkeychain.ExtendedKey, chainParams *netparams.ChainParams, birthday time.Time, cb func(walletdb.ReadWriteTx) error) error { return create( - db, pubPass, privPass, rootKey, params, birthday, false, cb, + db, pubPass, privPass, rootKey, chainParams, birthday, false, cb, ) } @@ -3902,11 +3904,11 @@ func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte, // added callback that will be called in the same transaction the wallet // structure is initialized. func CreateWatchingOnlyWithCallback(db walletdb.DB, pubPass []byte, - params *chaincfg.Params, birthday time.Time, + chainParams *netparams.ChainParams, birthday time.Time, cb func(walletdb.ReadWriteTx) error) error { return create( - db, pubPass, nil, nil, params, birthday, true, cb, + db, pubPass, nil, nil, chainParams, birthday, true, cb, ) } @@ -3914,11 +3916,11 @@ func CreateWatchingOnlyWithCallback(db walletdb.DB, pubPass []byte, // root key is non-nil, it is used. Otherwise, a secure random seed of the // recommended length is generated. func Create(db walletdb.DB, pubPass, privPass []byte, - rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params, + rootKey *hdkeychain.ExtendedKey, chainParams *netparams.ChainParams, birthday time.Time) error { return create( - db, pubPass, privPass, rootKey, params, birthday, false, nil, + db, pubPass, privPass, rootKey, chainParams, birthday, false, nil, ) } @@ -3927,15 +3929,15 @@ func Create(db walletdb.DB, pubPass, privPass []byte, // watching only. Likewise no private passphrase may be provided // either. func CreateWatchingOnly(db walletdb.DB, pubPass []byte, - params *chaincfg.Params, birthday time.Time) error { + chainParams *netparams.ChainParams, birthday time.Time) error { return create( - db, pubPass, nil, nil, params, birthday, true, nil, + db, pubPass, nil, nil, chainParams, birthday, true, nil, ) } func create(db walletdb.DB, pubPass, privPass []byte, - rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params, + rootKey *hdkeychain.ExtendedKey, chainParams *netparams.ChainParams, birthday time.Time, isWatchingOnly bool, cb func(walletdb.ReadWriteTx) error) error { @@ -3951,7 +3953,7 @@ func create(db walletdb.DB, pubPass, privPass []byte, } // Derive the master extended key from the seed. - rootKey, err = hdkeychain.NewMaster(hdSeed, params) + rootKey, err = hdkeychain.NewMaster(hdSeed, chainParams.BTCDParams()) if err != nil { return fmt.Errorf("failed to derive master extended " + "key") @@ -3975,7 +3977,7 @@ func create(db walletdb.DB, pubPass, privPass []byte, } err = waddrmgr.Create( - addrmgrNs, rootKey, pubPass, privPass, params, nil, + addrmgrNs, rootKey, pubPass, privPass, chainParams, nil, birthday, ) if err != nil { @@ -3997,10 +3999,10 @@ func create(db walletdb.DB, pubPass, privPass []byte, // Open loads an already-created wallet from the passed database and namespaces. func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, - params *chaincfg.Params, recoveryWindow uint32) (*Wallet, error) { + chainParams *netparams.ChainParams, recoveryWindow uint32) (*Wallet, error) { return OpenWithRetry( - db, pubPass, cbs, params, recoveryWindow, + db, pubPass, cbs, chainParams, recoveryWindow, defaultSyncRetryInterval, ) } @@ -4008,7 +4010,7 @@ func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, // OpenWithRetry loads an already-created wallet from the passed database and // namespaces and re-tries on errors during initial sync. func OpenWithRetry(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, - params *chaincfg.Params, recoveryWindow uint32, + chainParams *netparams.ChainParams, recoveryWindow uint32, syncRetryInterval time.Duration) (*Wallet, error) { var ( @@ -4037,11 +4039,11 @@ func OpenWithRetry(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, return err } - addrMgr, err = waddrmgr.Open(addrMgrBucket, pubPass, params) + addrMgr, err = waddrmgr.Open(addrMgrBucket, pubPass, chainParams) if err != nil { return err } - txMgr, err = wtxmgr.Open(txMgrBucket, params) + txMgr, err = wtxmgr.Open(txMgrBucket, chainParams) if err != nil { return err } @@ -4073,7 +4075,8 @@ func OpenWithRetry(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, lockState: make(chan bool), changePassphrase: make(chan changePassphraseRequest), changePassphrases: make(chan changePassphrasesRequest), - chainParams: params, + chainParams: chainParams, + btcParams: chainParams.BTCDParams(), quit: make(chan struct{}), syncRetryInterval: syncRetryInterval, } diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index e80e102..2c88bac 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -178,7 +178,7 @@ func TestLabelTransaction(t *testing.T) { // write txdetail to disk. if test.txKnown { rec, err := wtxmgr.NewTxRecord( - TstSerializedTx, time.Now(), + "btc", TstSerializedTx, time.Now(), ) if err != nil { t.Fatal(err) @@ -228,7 +228,7 @@ func TestLabelTransaction(t *testing.T) { // and a non-existing transaction from the wallet like we expect. func TestGetTransaction(t *testing.T) { t.Parallel() - rec, err := wtxmgr.NewTxRecord(TstSerializedTx, time.Now()) + rec, err := wtxmgr.NewTxRecord("btc", TstSerializedTx, time.Now()) require.NoError(t, err) tests := []struct { diff --git a/wallet/watchingonly_test.go b/wallet/watchingonly_test.go index eb275cc..8bf829c 100644 --- a/wallet/watchingonly_test.go +++ b/wallet/watchingonly_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/assets" _ "github.com/bisoncraft/utxowallet/walletdb/bdb" - "github.com/btcsuite/btcd/chaincfg" ) // TestCreateWatchingOnly checks that we can construct a watching-only @@ -26,7 +26,7 @@ func TestCreateWatchingOnly(t *testing.T) { pubPass := []byte("hello") loader := NewLoader( - &chaincfg.TestNet3Params, dir, true, defaultDBTimeout, 250, + assets.BTCParams["testnet"], dir, true, defaultDBTimeout, 250, WithWalletSyncRetryInterval(10*time.Millisecond), ) _, err = loader.CreateNewWatchingOnlyWallet(pubPass, time.Now()) diff --git a/wtxmgr/db.go b/wtxmgr/db.go index e3afb3d..5973bac 100644 --- a/wtxmgr/db.go +++ b/wtxmgr/db.go @@ -11,6 +11,7 @@ import ( "fmt" "time" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -381,9 +382,9 @@ func keyTxRecord(txHash *chainhash.Hash, block *Block) []byte { func valueTxRecord(rec *TxRecord) ([]byte, error) { var v []byte if rec.SerializedTx == nil { - txSize := rec.MsgTx.SerializeSize() + txSize := rec.Tx.SerializeSize() v = make([]byte, 8, 8+txSize) - err := rec.MsgTx.Serialize(bytes.NewBuffer(v[8:])) + err := rec.Tx.Serialize(bytes.NewBuffer(v[8:])) if err != nil { str := fmt.Sprintf("unable to serialize transaction %v", rec.Hash) return nil, storeError(ErrInput, str, err) @@ -428,7 +429,7 @@ func readRawTxRecord(txHash *chainhash.Hash, v []byte, rec *TxRecord) error { } rec.Hash = *txHash rec.Received = time.Unix(int64(byteOrder.Uint64(v)), 0) - err := rec.MsgTx.Deserialize(bytes.NewReader(v[8:])) + err := rec.Tx.Deserialize(bytes.NewReader(v[8:])) if err != nil { str := fmt.Sprintf("%s: failed to deserialize transaction %v", bucketTxRecords, txHash) @@ -448,29 +449,33 @@ func readRawTxRecordBlock(k []byte, block *Block) error { return nil } -func fetchTxRecord(ns walletdb.ReadBucket, txHash *chainhash.Hash, block *Block) (*TxRecord, error) { +func fetchTxRecord(ns walletdb.ReadBucket, chain string, txHash *chainhash.Hash, block *Block) (*TxRecord, error) { k := keyTxRecord(txHash, block) v := ns.NestedReadBucket(bucketTxRecords).Get(k) - rec := new(TxRecord) + rec := &TxRecord{ + Tx: bisonwire.Tx{Chain: chain}, + } err := readRawTxRecord(txHash, v, rec) return rec, err } // TODO: This reads more than necessary. Pass the pkscript location instead to // avoid the wire.MsgTx deserialization. -func fetchRawTxRecordPkScript(k, v []byte, index uint32) ([]byte, error) { - var rec TxRecord +func fetchRawTxRecordPkScript(chain string, k, v []byte, index uint32) ([]byte, error) { + rec := TxRecord{ + Tx: bisonwire.Tx{Chain: chain}, + } copy(rec.Hash[:], k) // Silly but need an array err := readRawTxRecord(&rec.Hash, v, &rec) if err != nil { return nil, err } - if int(index) >= len(rec.MsgTx.TxOut) { + if int(index) >= len(rec.Tx.TxOut) { str := "missing transaction output for credit index" return nil, storeError(ErrData, str, nil) } - return rec.MsgTx.TxOut[index].PkScript, nil + return rec.Tx.TxOut[index].PkScript, nil } func existsTxRecord(ns walletdb.ReadBucket, txHash *chainhash.Hash, block *Block) (k, v []byte) { diff --git a/wtxmgr/example_test.go b/wtxmgr/example_test.go index 902e3f9..ac8ff55 100644 --- a/wtxmgr/example_test.go +++ b/wtxmgr/example_test.go @@ -7,8 +7,8 @@ package wtxmgr import ( "fmt" + "github.com/bisoncraft/utxowallet/assets" "github.com/bisoncraft/utxowallet/walletdb" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" ) @@ -25,14 +25,14 @@ var ( func init() { tx := spendOutput(&chainhash.Hash{}, 0, 10e8) - rec, err := NewTxRecordFromMsgTx(tx, timeNow()) + rec, err := NewTxRecordFromMsgTx("btc", tx, timeNow()) if err != nil { panic(err) } exampleTxRecordA = rec tx = spendOutput(&exampleTxRecordA.Hash, 0, 5e8, 5e8) - rec, err = NewTxRecordFromMsgTx(tx, timeNow()) + rec, err = NewTxRecordFromMsgTx("btc", tx, timeNow()) if err != nil { panic(err) } @@ -187,7 +187,7 @@ func Example_basicUsage() { fmt.Println(err) return } - s, err := Open(b, &chaincfg.TestNet3Params) + s, err := Open(b, assets.BTCParams["testnet"]) if err != nil { fmt.Println(err) return diff --git a/wtxmgr/migrations_test.go b/wtxmgr/migrations_test.go index 4d10a3c..809fc11 100644 --- a/wtxmgr/migrations_test.go +++ b/wtxmgr/migrations_test.go @@ -133,7 +133,7 @@ func TestMigrationDropTransactionHistory(t *testing.T) { // while the unconfirmed will spend an output from the confirmed // transaction. cb := newCoinBase(1e8) - cbRec, err := NewTxRecordFromMsgTx(cb, timeNow()) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, timeNow()) if err != nil { return err } @@ -141,8 +141,9 @@ func TestMigrationDropTransactionHistory(t *testing.T) { b := &BlockMeta{Block: Block{Height: 100}} confirmedSpend := spendOutput(&cbRec.Hash, 0, 5e7, 4e7) confirmedSpendRec, err := NewTxRecordFromMsgTx( - confirmedSpend, timeNow(), + "btc", confirmedSpend, timeNow(), ) + if err := s.InsertTx(ns, confirmedSpendRec, b); err != nil { return err } @@ -155,7 +156,7 @@ func TestMigrationDropTransactionHistory(t *testing.T) { &confirmedSpendRec.Hash, 0, 5e6, 5e6, ) unconfirmedSpendRec, err := NewTxRecordFromMsgTx( - unconfimedSpend, timeNow(), + "btc", unconfimedSpend, timeNow(), ) if err != nil { return err @@ -163,6 +164,7 @@ func TestMigrationDropTransactionHistory(t *testing.T) { if err := s.InsertTx(ns, unconfirmedSpendRec, nil); err != nil { return err } + err = s.AddCredit(ns, unconfirmedSpendRec, nil, 1, true) if err != nil { return err diff --git a/wtxmgr/query.go b/wtxmgr/query.go index c1ba4f6..3badf81 100644 --- a/wtxmgr/query.go +++ b/wtxmgr/query.go @@ -8,6 +8,7 @@ package wtxmgr import ( "fmt" + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -46,6 +47,7 @@ type TxDetails struct { // txHash and the passed tx record key and value. func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, recKey, recVal []byte) (*TxDetails, error) { var details TxDetails + details.Tx.Chain = s.chainParams.Chain // Parse transaction record k/v, lookup the full block record for the // block time, and read all matching credits, debits. @@ -64,7 +66,7 @@ func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, r credIter := makeReadCreditIterator(ns, recKey) for credIter.next() { - if int(credIter.elem.Index) >= len(details.MsgTx.TxOut) { + if int(credIter.elem.Index) >= len(details.Tx.TxOut) { str := "saved credit index exceeds number of outputs" return nil, storeError(ErrData, str, nil) } @@ -84,7 +86,7 @@ func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, r debIter := makeReadDebitIterator(ns, recKey) for debIter.next() { - if int(debIter.elem.Index) >= len(details.MsgTx.TxIn) { + if int(debIter.elem.Index) >= len(details.Tx.TxIn) { str := "saved debit index exceeds number of inputs" return nil, storeError(ErrData, str, nil) } @@ -108,7 +110,8 @@ func (s *Store) minedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, r // hash txHash and the passed unmined record value. func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, v []byte) (*TxDetails, error) { details := TxDetails{ - Block: BlockMeta{Block: Block{Height: -1}}, + TxRecord: TxRecord{Tx: bisonwire.Tx{Chain: s.chainParams.Chain}}, + Block: BlockMeta{Block: Block{Height: -1}}, } err := readRawTxRecord(txHash, v, &details.TxRecord) if err != nil { @@ -117,7 +120,7 @@ func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, it := makeReadUnminedCreditIterator(ns, txHash) for it.next() { - if int(it.elem.Index) >= len(details.MsgTx.TxOut) { + if int(it.elem.Index) >= len(details.Tx.TxOut) { str := "saved credit index exceeds number of outputs" return nil, storeError(ErrData, str, nil) } @@ -136,7 +139,7 @@ func (s *Store) unminedTxDetails(ns walletdb.ReadBucket, txHash *chainhash.Hash, // transaction: mined unspent outputs (which remain marked unspent even // when spent by an unmined transaction), and credits from other unmined // transactions. Both situations must be considered. - for i, output := range details.MsgTx.TxIn { + for i, output := range details.Tx.TxIn { opKey := canonicalOutPoint(&output.PreviousOutPoint.Hash, output.PreviousOutPoint.Index) credKey := existsRawUnspent(ns, opKey) @@ -395,7 +398,7 @@ func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block * var pkScripts [][]byte if block == nil { - for _, input := range rec.MsgTx.TxIn { + for _, input := range rec.Tx.TxIn { prevOut := &input.PreviousOutPoint // Input may spend a previous unmined output, a @@ -413,7 +416,7 @@ func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block * } pkScript, err := fetchRawTxRecordPkScript( - prevOut.Hash[:], v, prevOut.Index) + s.chainParams.Chain, prevOut.Hash[:], v, prevOut.Index) if err != nil { return nil, err } @@ -425,8 +428,9 @@ func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block * if credKey != nil { k := extractRawCreditTxRecordKey(credKey) v = existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, - prevOut.Index) + pkScript, err := fetchRawTxRecordPkScript( + s.chainParams.Chain, k, v, prevOut.Index, + ) if err != nil { return nil, err } @@ -444,7 +448,7 @@ func (s *Store) PreviousPkScripts(ns walletdb.ReadBucket, rec *TxRecord, block * index := extractRawCreditIndex(credKey) k := extractRawCreditTxRecordKey(credKey) v := existsRawTxRecord(ns, k) - pkScript, err := fetchRawTxRecordPkScript(k, v, index) + pkScript, err := fetchRawTxRecordPkScript(s.chainParams.Chain, k, v, index) if err != nil { return nil, err } diff --git a/wtxmgr/query_test.go b/wtxmgr/query_test.go index 579dd61..0ac1f38 100644 --- a/wtxmgr/query_test.go +++ b/wtxmgr/query_test.go @@ -52,7 +52,7 @@ func (q *queryState) deepCopy() *queryState { func deepCopyTxDetails(d *TxDetails) *TxDetails { cpy := *d - cpy.MsgTx = *d.MsgTx.Copy() + cpy.Tx.MsgTx = *d.Tx.MsgTx.Copy() if cpy.SerializedTx != nil { cpy.SerializedTx = make([]byte, len(cpy.SerializedTx)) copy(cpy.SerializedTx, d.SerializedTx) @@ -150,7 +150,7 @@ func (q *queryState) compare(s *Store, ns walletdb.ReadBucket, func equalTxDetails(got, exp *TxDetails) error { // Need to avoid using reflect.DeepEqual against slices, since it // returns false for nil vs non-nil zero length slices. - if err := equalTxs(&got.MsgTx, &exp.MsgTx); err != nil { + if err := equalTxs(&got.Tx.MsgTx, &exp.Tx.MsgTx); err != nil { return err } @@ -264,7 +264,7 @@ func TestStoreQueries(t *testing.T) { // Insert an unmined transaction. Mark no credits yet. txA := spendOutput(&chainhash.Hash{}, 0, 100e8) - recA, err := NewTxRecordFromMsgTx(txA, timeNow()) + recA, err := NewTxRecordFromMsgTx("btc", txA, timeNow()) if err != nil { t.Fatal(err) } @@ -294,7 +294,7 @@ func TestStoreQueries(t *testing.T) { newState.blocks[0][0].Credits = []CreditRecord{ { Index: 0, - Amount: btcutil.Amount(recA.MsgTx.TxOut[0].Value), + Amount: btcutil.Amount(recA.Tx.TxOut[0].Value), Spent: false, Change: true, }, @@ -312,7 +312,7 @@ func TestStoreQueries(t *testing.T) { // Insert another unmined transaction which spends txA:0, splitting the // amount into outputs of 40 and 60 BTC. txB := spendOutput(&recA.Hash, 0, 40e8, 60e8) - recB, err := NewTxRecordFromMsgTx(txB, timeNow()) + recB, err := NewTxRecordFromMsgTx("btc", txB, timeNow()) if err != nil { t.Fatal(err) } @@ -323,7 +323,7 @@ func TestStoreQueries(t *testing.T) { Block: BlockMeta{Block: Block{Height: -1}}, Debits: []DebitRecord{ { - Amount: btcutil.Amount(recA.MsgTx.TxOut[0].Value), + Amount: btcutil.Amount(recA.Tx.TxOut[0].Value), Index: 0, // recB.MsgTx.TxIn index }, }, @@ -342,7 +342,7 @@ func TestStoreQueries(t *testing.T) { newState.blocks[0][1].Credits = []CreditRecord{ { Index: 0, - Amount: btcutil.Amount(recB.MsgTx.TxOut[0].Value), + Amount: btcutil.Amount(recB.Tx.TxOut[0].Value), Spent: false, Change: false, }, @@ -412,7 +412,7 @@ func TestStoreQueries(t *testing.T) { ns := tx.ReadWriteBucket(namespaceKey) missingTx := spendOutput(&recB.Hash, 0, 40e8) - missingRec, err := NewTxRecordFromMsgTx(missingTx, timeNow()) + missingRec, err := NewTxRecordFromMsgTx("btc", missingTx, timeNow()) if err != nil { return err } @@ -565,7 +565,7 @@ func TestPreviousPkScripts(t *testing.T) { } newTxRecordFromMsgTx := func(tx *wire.MsgTx) *TxRecord { - rec, err := NewTxRecordFromMsgTx(tx, timeNow()) + rec, err := NewTxRecordFromMsgTx("btc", tx, timeNow()) if err != nil { t.Fatal(err) } diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index 676b444..d52d47e 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -12,6 +12,8 @@ import ( "fmt" "time" + "github.com/bisoncraft/utxowallet/bisonwire" + "github.com/bisoncraft/utxowallet/netparams" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" @@ -120,7 +122,7 @@ type credit struct { // TxRecord represents a transaction managed by the Store. type TxRecord struct { - MsgTx wire.MsgTx + Tx bisonwire.Tx Hash chainhash.Hash Received time.Time SerializedTx []byte // Optional: may be nil @@ -137,12 +139,13 @@ type LockedOutput struct { // NewTxRecord creates a new transaction record that may be inserted into the // store. It uses memoization to save the transaction hash and the serialized // transaction. -func NewTxRecord(serializedTx []byte, received time.Time) (*TxRecord, error) { +func NewTxRecord(chain string, serializedTx []byte, received time.Time) (*TxRecord, error) { rec := &TxRecord{ + Tx: bisonwire.Tx{Chain: chain}, Received: received, SerializedTx: serializedTx, } - err := rec.MsgTx.Deserialize(bytes.NewReader(serializedTx)) + err := rec.Tx.Deserialize(bytes.NewReader(serializedTx)) if err != nil { str := "failed to deserialize transaction" return nil, storeError(ErrInput, str, err) @@ -153,7 +156,7 @@ func NewTxRecord(serializedTx []byte, received time.Time) (*TxRecord, error) { // NewTxRecordFromMsgTx creates a new transaction record that may be inserted // into the store. -func NewTxRecordFromMsgTx(msgTx *wire.MsgTx, received time.Time) (*TxRecord, error) { +func NewTxRecordFromMsgTx(chain string, msgTx *wire.MsgTx, received time.Time) (*TxRecord, error) { buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize())) err := msgTx.Serialize(buf) if err != nil { @@ -161,7 +164,10 @@ func NewTxRecordFromMsgTx(msgTx *wire.MsgTx, received time.Time) (*TxRecord, err return nil, storeError(ErrInput, str, err) } rec := &TxRecord{ - MsgTx: *msgTx, + Tx: bisonwire.Tx{ + MsgTx: *msgTx, + Chain: chain, + }, Received: received, SerializedTx: buf.Bytes(), Hash: msgTx.TxHash(), @@ -170,6 +176,26 @@ func NewTxRecordFromMsgTx(msgTx *wire.MsgTx, received time.Time) (*TxRecord, err return rec, nil } +func NewTxRecordBisonTx(chain string, tx *bisonwire.Tx, received time.Time) (*TxRecord, error) { + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + err := tx.BtcEncode(buf, 0, wire.LatestEncoding) + if err != nil { + str := "failed to serialize transaction" + return nil, storeError(ErrInput, str, err) + } + rec := &TxRecord{ + Tx: bisonwire.Tx{ + MsgTx: tx.MsgTx, + Chain: chain, + }, + Received: received, + SerializedTx: buf.Bytes(), + Hash: tx.TxHash(), + } + + return rec, nil +} + // Credit is the type representing a transaction output which was spent or // is still spendable by wallet. A UTXO is an unspent Credit, but not all // Credits are UTXOs. @@ -188,7 +214,8 @@ type LockID [32]byte // Store implements a transaction store for storing and managing wallet // transactions. type Store struct { - chainParams *chaincfg.Params + chainParams *netparams.ChainParams + btcParams *chaincfg.Params // clock is used to determine when outputs locks have expired. clock clock.Clock @@ -201,15 +228,19 @@ type Store struct { // Open opens the wallet transaction store from a walletdb namespace. If the // store does not exist, ErrNoExist is returned. `lockDuration` represents how // long outputs are locked for. -func Open(ns walletdb.ReadBucket, chainParams *chaincfg.Params) (*Store, error) { +func Open(ns walletdb.ReadBucket, chainParams *netparams.ChainParams) (*Store, error) { // Open the store. err := openStore(ns) if err != nil { return nil, err } - s := &Store{chainParams, clock.NewDefaultClock(), nil} // TODO: set callbacks - return s, nil + return &Store{ + chainParams: chainParams, + btcParams: chainParams.BTCDParams(), + clock: clock.NewDefaultClock(), + NotifyUnspent: nil, // TODO: set callbacks + }, nil } // Create creates a new persistent transaction store in the walletdb namespace. @@ -240,7 +271,7 @@ func (s *Store) updateMinedBalance(ns walletdb.ReadWriteBucket, rec *TxRecord, } newMinedBalance := minedBalance - for i, input := range rec.MsgTx.TxIn { + for i, input := range rec.Tx.TxIn { unspentKey, credKey := existsUnspent(ns, &input.PreviousOutPoint) if credKey == nil { // Debits for unmined transactions are not explicitly @@ -339,14 +370,14 @@ func (s *Store) updateMinedBalance(ns walletdb.ReadWriteBucket, rec *TxRecord, // // NOTE: This should only be used once the transaction has been mined. func (s *Store) deleteUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error { - for _, input := range rec.MsgTx.TxIn { + for _, input := range rec.Tx.TxIn { prevOut := input.PreviousOutPoint k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) if err := deleteRawUnminedInput(ns, k, rec.Hash); err != nil { return err } } - for i := range rec.MsgTx.TxOut { + for i := range rec.Tx.TxOut { k := canonicalOutPoint(&rec.Hash, uint32(i)) if err := deleteRawUnminedCredit(ns, k); err != nil { return err @@ -418,25 +449,26 @@ func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, var err error blockKey, blockValue := existsBlockRecord(ns, block.Height) if blockValue == nil { - err = putBlockRecord(ns, block, &rec.Hash) + if err = putBlockRecord(ns, block, &rec.Hash); err != nil { + return fmt.Errorf("error putting block record: %w", err) + } } else { blockValue, err = appendRawBlockRecord(blockValue, &rec.Hash) if err != nil { - return err + return fmt.Errorf("error appending raw block record: %w", err) + } + if err = putRawBlockRecord(ns, blockKey, blockValue); err != nil { + return fmt.Errorf("error putting raw block record: %w", err) } - err = putRawBlockRecord(ns, blockKey, blockValue) - } - if err != nil { - return err } if err := putTxRecord(ns, rec, &block.Block); err != nil { - return err + return fmt.Errorf("error putting tx record: %w", err) } // Determine if this transaction has affected our balance, and if so, // update it. if err := s.updateMinedBalance(ns, rec, block); err != nil { - return err + return fmt.Errorf("error updating mined balance: %w", err) } // If this transaction previously existed within the store as unmined, @@ -446,7 +478,7 @@ func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, &rec.Hash, block.Height) if err := s.deleteUnminedTx(ns, rec); err != nil { - return err + return fmt.Errorf("error deleting unmined tx: %w", err) } } @@ -456,14 +488,14 @@ func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, // transaction spend chains if any other unconfirmed transactions spend // outputs of the removed double spend. if err := s.removeDoubleSpends(ns, rec); err != nil { - return err + return fmt.Errorf("error removing double spends: %w", err) } // Clear any locked outputs since we now have a confirmed spend for // them, making them not eligible for coin selection anyway. - for _, txIn := range rec.MsgTx.TxIn { + for _, txIn := range rec.Tx.TxIn { if err := unlockOutput(ns, txIn.PreviousOutPoint); err != nil { - return err + return fmt.Errorf("error unlocking outputs: %w", err) } } @@ -478,7 +510,7 @@ func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, // that are known to contain credits when a transaction or merkleblock is // inserted into the store. func (s *Store) AddCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) error { - if int(index) >= len(rec.MsgTx.TxOut) { + if int(index) >= len(rec.Tx.TxOut) { str := "transaction output does not exist" return storeError(ErrInput, str, nil) } @@ -507,7 +539,7 @@ func (s *Store) addCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *Blo rec.Hash.String()) return false, nil } - v := valueUnminedCredit(btcutil.Amount(rec.MsgTx.TxOut[index].Value), change) + v := valueUnminedCredit(btcutil.Amount(rec.Tx.TxOut[index].Value), change) return true, putRawUnminedCredit(ns, k, v) } @@ -516,7 +548,7 @@ func (s *Store) addCredit(ns walletdb.ReadWriteBucket, rec *TxRecord, block *Blo return false, nil } - txOutAmt := btcutil.Amount(rec.MsgTx.TxOut[index].Value) + txOutAmt := btcutil.Amount(rec.Tx.TxOut[index].Value) log.Debugf("Marking transaction %v output %d (%v) spendable", rec.Hash, index, txOutAmt) @@ -587,7 +619,9 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { recKey := keyTxRecord(txHash, &b.Block) recVal := existsRawTxRecord(ns, recKey) - var rec TxRecord + rec := TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } err = readRawTxRecord(txHash, recVal, &rec) if err != nil { return err @@ -602,9 +636,9 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { // not moved to the unconfirmed store. A coinbase cannot // contain any debits, but all credits should be removed // and the mined balance decremented. - if blockchain.IsCoinBaseTx(&rec.MsgTx) { + if blockchain.IsCoinBaseTx(&rec.Tx.MsgTx) { op := wire.OutPoint{Hash: rec.Hash} - for i, output := range rec.MsgTx.TxOut { + for i, output := range rec.Tx.TxOut { k, v := existsCredit(ns, &rec.Hash, uint32(i), &b.Block) if v == nil { @@ -641,7 +675,7 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { // exists) and delete the debit. The previous output is // recorded in the unconfirmed store for every previous // output, not just debits. - for i, input := range rec.MsgTx.TxIn { + for i, input := range rec.Tx.TxIn { prevOut := &input.PreviousOutPoint prevOutKey := canonicalOutPoint(&prevOut.Hash, prevOut.Index) @@ -703,7 +737,7 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { // mined balance is decremented. // // TODO: use a credit iterator - for i, output := range rec.MsgTx.TxOut { + for i, output := range rec.Tx.TxOut { k, v := existsCredit(ns, &rec.Hash, uint32(i), &b.Block) if v == nil { @@ -775,7 +809,9 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { continue } - var unminedRec TxRecord + unminedRec := TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } unminedRec.Hash = unminedSpendTxHashKey err = readRawTxRecord(&unminedRec.Hash, unminedVal, &unminedRec) if err != nil { @@ -831,12 +867,12 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { // TODO(jrick): reading the entire transaction should // be avoidable. Creating the credit only requires the // output amount and pkScript. - rec, err := fetchTxRecord(ns, &op.Hash, &block) + rec, err := fetchTxRecord(ns, s.chainParams.Chain, &op.Hash, &block) if err != nil { return fmt.Errorf("unable to retrieve transaction %v: "+ "%w", op.Hash, err) } - txOut := rec.MsgTx.TxOut[op.Index] + txOut := rec.Tx.TxOut[op.Index] cred := Credit{ OutPoint: op, BlockMeta: BlockMeta{ @@ -846,7 +882,7 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { Amount: btcutil.Amount(txOut.Value), PkScript: txOut.PkScript, Received: rec.Received, - FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx), + FromCoinBase: blockchain.IsCoinBaseTx(&rec.Tx.MsgTx), } unspent = append(unspent, cred) return nil @@ -879,14 +915,16 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { // TODO(jrick): Reading/parsing the entire transaction record // just for the output amount and script can be avoided. recVal := existsRawUnmined(ns, op.Hash[:]) - var rec TxRecord + rec := TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } err = readRawTxRecord(&op.Hash, recVal, &rec) if err != nil { return fmt.Errorf("unable to retrieve raw transaction "+ "%v: %w", op.Hash, err) } - txOut := rec.MsgTx.TxOut[op.Index] + txOut := rec.Tx.TxOut[op.Index] cred := Credit{ OutPoint: op, BlockMeta: BlockMeta{ @@ -895,7 +933,7 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { Amount: btcutil.Amount(txOut.Value), PkScript: txOut.PkScript, Received: rec.Received, - FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx), + FromCoinBase: blockchain.IsCoinBaseTx(&rec.Tx.MsgTx), } unspent = append(unspent, cred) return nil @@ -974,7 +1012,7 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) // Decrement the balance for any unspent credit with less than // minConf confirmations and any (unspent) immature coinbase credit. - coinbaseMaturity := int32(s.chainParams.CoinbaseMaturity) + coinbaseMaturity := int32(s.btcParams.CoinbaseMaturity) stopConf := minConf if coinbaseMaturity > stopConf { stopConf = coinbaseMaturity @@ -990,11 +1028,11 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) for i := range block.transactions { txHash := &block.transactions[i] - rec, err := fetchTxRecord(ns, txHash, &block.Block) + rec, err := fetchTxRecord(ns, s.chainParams.Chain, txHash, &block.Block) if err != nil { return 0, err } - numOuts := uint32(len(rec.MsgTx.TxOut)) + numOuts := uint32(len(rec.Tx.TxOut)) for i := uint32(0); i < numOuts; i++ { // Avoid double decrementing the credit amount // if it was already removed for being spent by @@ -1023,7 +1061,7 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) continue } confs := syncHeight - block.Height + 1 - if confs < minConf || (blockchain.IsCoinBaseTx(&rec.MsgTx) && + if confs < minConf || (blockchain.IsCoinBaseTx(&rec.Tx.MsgTx) && confs < coinbaseMaturity) { bal -= amt } diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 75fcc38..79d8bf3 100644 --- a/wtxmgr/tx_test.go +++ b/wtxmgr/tx_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/bisoncraft/utxowallet/assets" "github.com/bisoncraft/utxowallet/walletdb" _ "github.com/bisoncraft/utxowallet/walletdb/bdb" "github.com/btcsuite/btcd/btcutil" @@ -93,7 +94,7 @@ func testStore() (*Store, walletdb.DB, func(), error) { if err != nil { return err } - s, err = Open(ns, &chaincfg.TestNet3Params) + s, err = Open(ns, assets.BTCParams["testnet"]) return err }) @@ -156,7 +157,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "txout insert", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } @@ -183,7 +184,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert duplicate unconfirmed", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } @@ -217,7 +218,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "confirmed txout insert", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } @@ -242,7 +243,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert duplicate confirmed", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } @@ -292,7 +293,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert confirmed double spend", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstDoubleSpendSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstDoubleSpendSerializedTx, time.Now()) if err != nil { return nil, err } @@ -317,7 +318,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert unconfirmed debit", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } @@ -334,7 +335,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert unconfirmed debit again", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstDoubleSpendSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstDoubleSpendSerializedTx, time.Now()) if err != nil { return nil, err } @@ -351,7 +352,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert change (index 0)", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } @@ -378,7 +379,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert output back to this own wallet (index 1)", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } @@ -408,7 +409,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "confirm signed tx", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstSpendingSerializedTx, time.Now()) if err != nil { return nil, err } @@ -491,7 +492,7 @@ func TestInsertsCreditsDebitsRollbacks(t *testing.T) { { name: "insert original recv txout", f: func(s *Store, ns walletdb.ReadWriteBucket) (*Store, error) { - rec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + rec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { return nil, err } @@ -596,7 +597,7 @@ func TestFindingSpentCredits(t *testing.T) { ns := dbtx.ReadWriteBucket(namespaceKey) // Insert transaction and credit which will be spent. - recvRec, err := NewTxRecord(TstRecvSerializedTx, time.Now()) + recvRec, err := NewTxRecord("btc", TstRecvSerializedTx, time.Now()) if err != nil { t.Fatal(err) } @@ -611,7 +612,7 @@ func TestFindingSpentCredits(t *testing.T) { } // Insert confirmed transaction which spends the above credit. - spendingRec, err := NewTxRecord(TstSpendingSerializedTx, time.Now()) + spendingRec, err := NewTxRecord("btc", TstSpendingSerializedTx, time.Now()) if err != nil { t.Fatal(err) } @@ -708,7 +709,7 @@ func TestCoinbases(t *testing.T) { } cb := newCoinBase(20e8, 10e8, 30e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -824,7 +825,7 @@ func TestCoinbases(t *testing.T) { // the next block will mature the coinbase. spenderATime := time.Now() spenderA := spendOutput(&cbRec.Hash, 0, 5e8, 15e8) - spenderARec, err := NewTxRecordFromMsgTx(spenderA, spenderATime) + spenderARec, err := NewTxRecordFromMsgTx("btc", spenderA, spenderATime) if err != nil { t.Fatal(err) } @@ -992,7 +993,7 @@ func TestCoinbases(t *testing.T) { // This will mean the balance tests should report identical results. spenderBTime := time.Now() spenderB := spendOutput(&spenderARec.Hash, 0, 5e8) - spenderBRec, err := NewTxRecordFromMsgTx(spenderB, spenderBTime) + spenderBRec, err := NewTxRecordFromMsgTx("btc", spenderB, spenderBTime) if err != nil { t.Fatal(err) } @@ -1114,7 +1115,7 @@ func TestMoveMultipleToSameBlock(t *testing.T) { } cb := newCoinBase(20e8, 30e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -1137,7 +1138,7 @@ func TestMoveMultipleToSameBlock(t *testing.T) { // outputs. spenderATime := time.Now() spenderA := spendOutput(&cbRec.Hash, 0, 1e8, 2e8, 18e8) - spenderARec, err := NewTxRecordFromMsgTx(spenderA, spenderATime) + spenderARec, err := NewTxRecordFromMsgTx("btc", spenderA, spenderATime) if err != nil { t.Fatal(err) } @@ -1155,7 +1156,7 @@ func TestMoveMultipleToSameBlock(t *testing.T) { } spenderBTime := time.Now() spenderB := spendOutput(&cbRec.Hash, 1, 4e8, 8e8, 18e8) - spenderBRec, err := NewTxRecordFromMsgTx(spenderB, spenderBTime) + spenderBRec, err := NewTxRecordFromMsgTx("btc", spenderB, spenderBTime) if err != nil { t.Fatal(err) } @@ -1286,7 +1287,7 @@ func TestInsertUnserializedTx(t *testing.T) { ns := dbtx.ReadWriteBucket(namespaceKey) tx := newCoinBase(50e8) - rec, err := NewTxRecordFromMsgTx(tx, timeNow()) + rec, err := NewTxRecordFromMsgTx("btc", tx, timeNow()) if err != nil { t.Fatal(err) } @@ -1301,7 +1302,7 @@ func TestInsertUnserializedTx(t *testing.T) { if err != nil { t.Fatal(err) } - rec2, err := NewTxRecordFromMsgTx(&details.MsgTx, rec.Received) + rec2, err := NewTxRecordFromMsgTx("btc", &details.Tx.MsgTx, rec.Received) if err != nil { t.Fatal(err) } @@ -1311,7 +1312,7 @@ func TestInsertUnserializedTx(t *testing.T) { // Now test that path with an unmined transaction. tx = spendOutput(&rec.Hash, 0, 50e8) - rec, err = NewTxRecordFromMsgTx(tx, timeNow()) + rec, err = NewTxRecordFromMsgTx("btc", tx, timeNow()) if err != nil { t.Fatal(err) } @@ -1323,7 +1324,7 @@ func TestInsertUnserializedTx(t *testing.T) { if err != nil { t.Fatal(err) } - rec2, err = NewTxRecordFromMsgTx(&details.MsgTx, rec.Received) + rec2, err = NewTxRecordFromMsgTx("btc", &details.Tx.MsgTx, rec.Received) if err != nil { t.Fatal(err) } @@ -1356,7 +1357,7 @@ func TestRemoveUnminedTx(t *testing.T) { } initialBalance := int64(1e8) cb := newCoinBase(initialBalance) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -1416,7 +1417,7 @@ func TestRemoveUnminedTx(t *testing.T) { } changeAmount := int64(4e7) spendTx := spendOutput(&cbRec.Hash, 0, 5e7, changeAmount) - spendTxRec, err := NewTxRecordFromMsgTx(spendTx, b101.Time) + spendTxRec, err := NewTxRecordFromMsgTx("btc", spendTx, b101.Time) if err != nil { t.Fatal(err) } @@ -1504,7 +1505,7 @@ func TestInsertMempoolTxAlreadyConfirmed(t *testing.T) { Time: time.Now(), } tx := newCoinBase(1e8) - txRec, err := NewTxRecordFromMsgTx(tx, b100.Time) + txRec, err := NewTxRecordFromMsgTx("btc", tx, b100.Time) if err != nil { t.Fatal(err) } @@ -1562,7 +1563,7 @@ func TestInsertMempoolTxAfterSpentOutput(t *testing.T) { Time: time.Now(), } cb := newCoinBase(1e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -1583,7 +1584,7 @@ func TestInsertMempoolTxAfterSpentOutput(t *testing.T) { } amt := int64(1e7) spend := spendOutput(&cbRec.Hash, 0, amt) - spendRec, err := NewTxRecordFromMsgTx(spend, time.Now()) + spendRec, err := NewTxRecordFromMsgTx("btc", spend, time.Now()) if err != nil { t.Fatal(err) } @@ -1643,7 +1644,7 @@ func TestOutputsAfterRemoveDoubleSpend(t *testing.T) { Time: time.Now(), } cb := newCoinBase(1e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -1664,7 +1665,7 @@ func TestOutputsAfterRemoveDoubleSpend(t *testing.T) { for i := 0; i < numSpendRecs; i++ { amt := int64((i + 1) * 1e7) spend := spendOutput(&cbRec.Hash, 0, amt) - spendRec, err := NewTxRecordFromMsgTx(spend, time.Now()) + spendRec, err := NewTxRecordFromMsgTx("btc", spend, time.Now()) if err != nil { t.Fatal(err) } @@ -1691,7 +1692,7 @@ func TestOutputsAfterRemoveDoubleSpend(t *testing.T) { ops := make(map[wire.OutPoint]struct{}) for _, tx := range txs { - for i := range tx.MsgTx.TxOut { + for i := range tx.Tx.TxOut { ops[wire.OutPoint{ Hash: tx.Hash, Index: uint32(i), @@ -1776,7 +1777,7 @@ func testInsertMempoolDoubleSpendTx(t *testing.T, first bool) { Time: time.Now(), } cb := newCoinBase(1e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -1793,12 +1794,12 @@ func testInsertMempoolDoubleSpendTx(t *testing.T, first bool) { // Then, we'll create two spends from the same coinbase output, in order // to replicate a double spend scenario. firstSpend := spendOutput(&cbRec.Hash, 0, 5e7, 5e7) - firstSpendRec, err := NewTxRecordFromMsgTx(firstSpend, time.Now()) + firstSpendRec, err := NewTxRecordFromMsgTx("btc", firstSpend, time.Now()) if err != nil { t.Fatal(err) } secondSpend := spendOutput(&cbRec.Hash, 0, 4e7, 6e7) - secondSpendRec, err := NewTxRecordFromMsgTx(secondSpend, time.Now()) + secondSpendRec, err := NewTxRecordFromMsgTx("btc", secondSpend, time.Now()) if err != nil { t.Fatal(err) } @@ -1934,7 +1935,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { Time: time.Now(), } cb1 := newCoinBase(1e8) - cbRec1, err := NewTxRecordFromMsgTx(cb1, b100.Time) + cbRec1, err := NewTxRecordFromMsgTx("btc", cb1, b100.Time) if err != nil { t.Fatal(err) } @@ -1952,7 +1953,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { // first two will remain unconfirmed, while the last should confirm and // remove the remaining unconfirmed from the wallet's store. firstSpend1 := spendOutput(&cbRec1.Hash, 0, 5e7) - firstSpendRec1, err := NewTxRecordFromMsgTx(firstSpend1, time.Now()) + firstSpendRec1, err := NewTxRecordFromMsgTx("btc", firstSpend1, time.Now()) if err != nil { t.Fatal(err) } @@ -1967,7 +1968,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { }) secondSpend1 := spendOutput(&cbRec1.Hash, 0, 4e7) - secondSpendRec1, err := NewTxRecordFromMsgTx(secondSpend1, time.Now()) + secondSpendRec1, err := NewTxRecordFromMsgTx("btc", secondSpend1, time.Now()) if err != nil { t.Fatal(err) } @@ -1984,7 +1985,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { // We'll also create another output and have one unconfirmed and one // confirmed spending transaction also spend it. cb2 := newCoinBase(2e8) - cbRec2, err := NewTxRecordFromMsgTx(cb2, b100.Time) + cbRec2, err := NewTxRecordFromMsgTx("btc", cb2, b100.Time) if err != nil { t.Fatal(err) } @@ -1999,7 +2000,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { }) firstSpend2 := spendOutput(&cbRec2.Hash, 0, 5e7) - firstSpendRec2, err := NewTxRecordFromMsgTx(firstSpend2, time.Now()) + firstSpendRec2, err := NewTxRecordFromMsgTx("btc", firstSpend2, time.Now()) if err != nil { t.Fatal(err) } @@ -2039,7 +2040,7 @@ func TestInsertConfirmedDoubleSpendTx(t *testing.T) { } confirmedSpend := spendOutputs(outputsToSpend, 3e7) confirmedSpendRec, err := NewTxRecordFromMsgTx( - confirmedSpend, bMaturity.Time, + "btc", confirmedSpend, bMaturity.Time, ) if err != nil { t.Fatal(err) @@ -2108,7 +2109,7 @@ func TestAddDuplicateCreditAfterConfirm(t *testing.T) { Time: time.Now(), } cb := newCoinBase(1e8) - cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + cbRec, err := NewTxRecordFromMsgTx("btc", cb, b100.Time) if err != nil { t.Fatal(err) } @@ -2144,7 +2145,7 @@ func TestAddDuplicateCreditAfterConfirm(t *testing.T) { Time: time.Now(), } spendTx := spendOutput(&cbRec.Hash, 0, 5e7, 4e7) - spendTxRec, err := NewTxRecordFromMsgTx(spendTx, b101.Time) + spendTxRec, err := NewTxRecordFromMsgTx("btc", spendTx, b101.Time) if err != nil { t.Fatal(err) } @@ -2234,7 +2235,7 @@ func TestInsertMempoolTxAndConfirm(t *testing.T) { // Create a transaction which we'll insert into the store as // unconfirmed. tx := newCoinBase(1e8) - txRec, err := NewTxRecordFromMsgTx(tx, time.Now()) + txRec, err := NewTxRecordFromMsgTx("btc", tx, time.Now()) if err != nil { t.Fatal(err) } @@ -2508,7 +2509,7 @@ func insertConfirmedCredit(t *testing.T, store *Store, db walletdb.DB, t.Helper() commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { - rec, err := NewTxRecordFromMsgTx(tx, time.Now()) + rec, err := NewTxRecordFromMsgTx("btc", tx, time.Now()) if err != nil { t.Fatal(err) } @@ -2749,7 +2750,7 @@ func TestOutputLocks(t *testing.T) { txHash := confirmedTx.TxHash() spendTx := spendOutput(&txHash, 0, 500) spendRec, err := NewTxRecordFromMsgTx( - spendTx, time.Now(), + "btc", spendTx, time.Now(), ) if err != nil { t.Fatal(err) diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go index f62577f..7856614 100644 --- a/wtxmgr/unconfirmed.go +++ b/wtxmgr/unconfirmed.go @@ -6,6 +6,7 @@ package wtxmgr import ( + "github.com/bisoncraft/utxowallet/bisonwire" "github.com/bisoncraft/utxowallet/walletdb" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -29,7 +30,7 @@ func (s *Store) insertMemPoolTx(ns walletdb.ReadWriteBucket, rec *TxRecord) erro // transaction _and_ block confirmation, we'll iterate through the // transaction's outputs to determine if we've already seen them to // prevent from adding this transaction to the unconfirmed bucket. - for i := range rec.MsgTx.TxOut { + for i := range rec.Tx.TxOut { k := canonicalOutPoint(&rec.Hash, uint32(i)) if existsRawUnspent(ns, k) != nil { return nil @@ -46,7 +47,7 @@ func (s *Store) insertMemPoolTx(ns walletdb.ReadWriteBucket, rec *TxRecord) erro return err } - for _, input := range rec.MsgTx.TxIn { + for _, input := range rec.Tx.TxIn { prevOut := &input.PreviousOutPoint k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) err = putRawUnminedInput(ns, k, rec.Hash[:]) @@ -66,7 +67,7 @@ func (s *Store) insertMemPoolTx(ns walletdb.ReadWriteBucket, rec *TxRecord) erro // transaction). Each conflicting transaction and all transactions which spend // it are recursively removed. func (s *Store) removeDoubleSpends(ns walletdb.ReadWriteBucket, rec *TxRecord) error { - for _, input := range rec.MsgTx.TxIn { + for _, input := range rec.Tx.TxIn { prevOut := &input.PreviousOutPoint prevOutKey := canonicalOutPoint(&prevOut.Hash, prevOut.Index) @@ -89,7 +90,9 @@ func (s *Store) removeDoubleSpends(ns walletdb.ReadWriteBucket, rec *TxRecord) e continue } - var doubleSpend TxRecord + doubleSpend := TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } doubleSpend.Hash = doubleSpendHash err := readRawTxRecord( &doubleSpend.Hash, doubleSpendVal, &doubleSpend, @@ -118,7 +121,7 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error // For each potential credit for this record, each spender (if any) must // be recursively removed as well. Once the spenders are removed, the // credit is deleted. - for i := range rec.MsgTx.TxOut { + for i := range rec.Tx.TxOut { k := canonicalOutPoint(&rec.Hash, uint32(i)) spenderHashes := fetchUnminedInputSpendTxHashes(ns, k) for _, spenderHash := range spenderHashes { @@ -132,7 +135,9 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error continue } - var spender TxRecord + spender := TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } spender.Hash = spenderHash err := readRawTxRecord(&spender.Hash, spenderVal, &spender) if err != nil { @@ -153,7 +158,7 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error // If this tx spends any previous credits (either mined or unmined), set // each unspent. Mined transactions are only marked spent by having the // output in the unmined inputs bucket. - for _, input := range rec.MsgTx.TxIn { + for _, input := range rec.Tx.TxIn { prevOut := &input.PreviousOutPoint k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) err := deleteRawUnminedInput(ns, k, rec.Hash) @@ -176,7 +181,7 @@ func (s *Store) UnminedTxs(ns walletdb.ReadBucket) ([]*wire.MsgTx, error) { txSet := make(map[chainhash.Hash]*wire.MsgTx, len(recSet)) for txHash, txRec := range recSet { - txSet[txHash] = &txRec.MsgTx + txSet[txHash] = &txRec.Tx.MsgTx } return DependencySort(txSet), nil @@ -190,8 +195,9 @@ func (s *Store) unminedTxRecords(ns walletdb.ReadBucket) (map[chainhash.Hash]*Tx if err != nil { return err } - - rec := new(TxRecord) + rec := &TxRecord{ + Tx: bisonwire.Tx{Chain: s.chainParams.Chain}, + } err = readRawTxRecord(&txHash, v, rec) if err != nil { return err