diff --git a/wire/msgblock.go b/wire/msgblock.go index 9651057605..8737c82578 100644 --- a/wire/msgblock.go +++ b/wire/msgblock.go @@ -30,6 +30,10 @@ const MaxBlockPayload = 4000000 // possibly fit into a block. const maxTxPerBlock = (MaxBlockPayload / minTxPayload) + 1 +// mwebVer is the bit of the block header's version that indicates the +// presence of a MWEB. +const mwebVer = 0x20000000 // 1 << 29 + // TxLoc holds locator data for the offset and length of where a transaction is // located within a MsgBlock data buffer. type TxLoc struct { @@ -81,6 +85,7 @@ func (msg *MsgBlock) BtcDecode(r io.Reader, pver uint32, enc MessageEncoding) er return messageError("MsgBlock.BtcDecode", str) } + var hasHogEx bool msg.Transactions = make([]*MsgTx, 0, txCount) for i := uint64(0); i < txCount; i++ { tx := MsgTx{} @@ -89,6 +94,15 @@ func (msg *MsgBlock) BtcDecode(r io.Reader, pver uint32, enc MessageEncoding) er return err } msg.Transactions = append(msg.Transactions, &tx) + hasHogEx = tx.IsHogEx + } + + // The mwebVer mask indicates it may contain a MWEB after a HogEx. + // src/primitives/block.h: SERIALIZE_NO_MWEB + if msg.Header.Version&mwebVer != 0 && hasHogEx { + if err = parseMWEB(r); err != nil { + return err + } } return nil @@ -288,3 +302,55 @@ func NewMsgBlock(blockHeader *BlockHeader) *MsgBlock { Transactions: make([]*MsgTx, 0, defaultTransactionAlloc), } } + +/// MWEB + +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 +} diff --git a/wire/msgblock_test.go b/wire/msgblock_test.go index 887a95e213..b7c7140276 100644 --- a/wire/msgblock_test.go +++ b/wire/msgblock_test.go @@ -6,13 +6,43 @@ package wire import ( "bytes" + _ "embed" "io" "reflect" "testing" "time" - "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/davecgh/go-spew/spew" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" +) + +var ( + // Testnet4 block 1821752 is pre-MWEB activation, 3 txns. + // But version 20000000. + //go:embed testdata/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 testdata/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 testdata/testnet4Block2321749.dat + block2321749 []byte + + // Block 2319633 is version 20000000 with a MWEB, 2 txns, one coinbase and + // one integration. + // e9fe2c6496aedefa8bf6529bdc5c1f9fd4af565ca4c98cab73e3a1f616fb3502 + //go:embed testdata/testnet4Block2319633.dat + block2319633 []byte + + //go:embed testdata/testnet4Block2215586.dat + block2215586 []byte ) // TestBlock tests the MsgBlock API. @@ -482,6 +512,77 @@ func TestBlockSerializeSize(t *testing.T) { } } +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) { + var msg MsgBlock + r := bytes.NewReader(tt.blk) + err := msg.Deserialize(r) + if err != nil { + t.Fatal(err) + } + + blkHash := msg.BlockHash() + if blkHash.String() != tt.wantHash { + t.Errorf("Wanted block hash %v, got %v", tt.wantHash, blkHash) + } + + if len(msg.Transactions) != tt.wantNumTx { + t.Errorf("Wanted %d txns, found %d", tt.wantNumTx, len(msg.Transactions)) + } + + lastTxHash := msg.Transactions[len(msg.Transactions)-1].TxHash() + if lastTxHash.String() != tt.wantLastTx { + t.Errorf("Wanted last tx hash %v, got %v", tt.wantLastTx, lastTxHash) + } + }) + } +} + // blockOne is the first block in the mainnet block chain. var blockOne = MsgBlock{ Header: BlockHeader{ diff --git a/wire/testdata/testnet4Block1821752.dat b/wire/testdata/testnet4Block1821752.dat new file mode 100644 index 0000000000..26cd51567c Binary files /dev/null and b/wire/testdata/testnet4Block1821752.dat differ diff --git a/wire/testdata/testnet4Block2215584.dat b/wire/testdata/testnet4Block2215584.dat new file mode 100644 index 0000000000..02a5daaeb2 Binary files /dev/null and b/wire/testdata/testnet4Block2215584.dat differ diff --git a/wire/testdata/testnet4Block2215586.dat b/wire/testdata/testnet4Block2215586.dat new file mode 100644 index 0000000000..f23ae0ec7b Binary files /dev/null and b/wire/testdata/testnet4Block2215586.dat differ diff --git a/wire/testdata/testnet4Block2319633.dat b/wire/testdata/testnet4Block2319633.dat new file mode 100644 index 0000000000..193246f82b Binary files /dev/null and b/wire/testdata/testnet4Block2319633.dat differ diff --git a/wire/testdata/testnet4Block2321749.dat b/wire/testdata/testnet4Block2321749.dat new file mode 100644 index 0000000000..b34a41e803 Binary files /dev/null and b/wire/testdata/testnet4Block2321749.dat differ