From 215edcfaa864121d5ceec8ba6d7733c6cf281124 Mon Sep 17 00:00:00 2001 From: David Burkett Date: Wed, 31 Aug 2022 15:32:47 -0400 Subject: [PATCH] Added "mwebheader" getdata type and CMerkleBlockWithMWEB response --- src/merkleblock.cpp | 11 + src/merkleblock.h | 49 +++ src/net_processing.cpp | 5 + src/protocol.cpp | 3 + src/protocol.h | 11 +- src/version.h | 5 +- test/functional/mweb_mining.py | 6 +- test/functional/mweb_p2p.py | 108 +++++ test/functional/test_framework/ltc_util.py | 81 +++- test/functional/test_framework/messages.py | 474 +++++++++++++++++++-- test/functional/test_framework/p2p.py | 15 + test/functional/test_runner.py | 1 + 12 files changed, 706 insertions(+), 63 deletions(-) create mode 100644 test/functional/mweb_p2p.py diff --git a/src/merkleblock.cpp b/src/merkleblock.cpp index b571d463c9..af073c766b 100644 --- a/src/merkleblock.cpp +++ b/src/merkleblock.cpp @@ -178,3 +178,14 @@ uint256 CPartialMerkleTree::ExtractMatches(std::vector &vMatch, std::ve return uint256(); return hashMerkleRoot; } + +CMerkleBlockWithMWEB::CMerkleBlockWithMWEB(const CBlock& block) +{ + hogex = block.GetHogEx(); + assert(hogex != nullptr); + + mweb_header = block.mweb_block.GetMWEBHeader(); + assert(mweb_header != nullptr); + + merkle = CMerkleBlock(block, std::set{hogex->GetHash()}); +} \ No newline at end of file diff --git a/src/merkleblock.h b/src/merkleblock.h index b2d2828784..ed1743c691 100644 --- a/src/merkleblock.h +++ b/src/merkleblock.h @@ -155,4 +155,53 @@ private: CMerkleBlock(const CBlock& block, CBloomFilter* filter, const std::set* txids); }; + +/** + * Can be requested by MWEB light clients to retrieve and verify MWEB headers. + * + * Serializes as merkleblock + tx (HogEx) + MWEB header: + * + * Merkle Block + * - uint32 version (4 bytes) + * - uint256 prev_block (32 bytes) + * - uint256 merkle_root (32 bytes) + * - uint32 timestamp (4 bytes) + * - uint32 bits (4 bytes) + * - uint32 nonce (4 bytes) + * - uint32 total_transactions (4 bytes) + * - varint number of hashes (1-3 bytes) + * - uint256[] hashes in depth-first order (<= 32*N bytes) + * - varint number of bytes of flag bits (1-3 bytes) + * - byte[] flag bits, packed per 8 in a byte, least significant bit first (<= 2*N-1 bits) + * + * HogEx Transaction + * - CTransaction hogex transaction (? bytes) + * + * MWEB Header + * - int32 height (4 bytes) + * - uint256 output_root (32 bytes) + * - uint256 kernel_root (32 bytes) + * - uint256 leafset_root (32 bytes) + * - uint256 kernel_offset (32 bytes) + * - uint256 stealth_offset (32 bytes) + * - uint64 output_mmr_size (8 bytes) + * - uint64 kernel_mmr_size (8 bytes) + * + * NOTE: The class assumes that the given CBlock contains a hogex transaction and MWEB block data. + * If it does not, an assertion will be hit. + */ +class CMerkleBlockWithMWEB +{ +private: + CMerkleBlock merkle; + CTransactionRef hogex; + mw::Header::CPtr mweb_header; + +public: + CMerkleBlockWithMWEB() { } + CMerkleBlockWithMWEB(const CBlock& block); + + SERIALIZE_METHODS(CMerkleBlockWithMWEB, obj) { READWRITE(obj.merkle, obj.hogex, obj.mweb_header); } +}; + #endif // BITCOIN_MERKLEBLOCK_H diff --git a/src/net_processing.cpp b/src/net_processing.cpp index fbba2fbecd..fdacef07b4 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1680,6 +1680,11 @@ void static ProcessGetBlockData(CNode& pfrom, const CChainParams& chainparams, c } else { connman.PushMessage(&pfrom, msgMaker.Make(nSendFlags, NetMsgType::BLOCK, *pblock)); } + } else if (inv.IsMsgMWEBHeader()) { + if (pblock->GetHogEx() != nullptr && !pblock->mweb_block.IsNull()) { + CMerkleBlockWithMWEB merkle_block_with_mweb(*pblock); + connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::MWEBHEADER, merkle_block_with_mweb)); + } } } diff --git a/src/protocol.cpp b/src/protocol.cpp index 44da30ea2e..40380e0b88 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -46,6 +46,7 @@ const char *CFHEADERS="cfheaders"; const char *GETCFCHECKPT="getcfcheckpt"; const char *CFCHECKPT="cfcheckpt"; const char *WTXIDRELAY="wtxidrelay"; +const char *MWEBHEADER="mwebheader"; } // namespace NetMsgType /** All known message types. Keep this in the same order as the list of @@ -86,6 +87,7 @@ const static std::string allNetMessageTypes[] = { NetMsgType::GETCFCHECKPT, NetMsgType::CFCHECKPT, NetMsgType::WTXIDRELAY, + NetMsgType::MWEBHEADER, }; const static std::vector allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes)); @@ -176,6 +178,7 @@ std::string CInv::GetCommand() const case MSG_BLOCK: return cmd.append(NetMsgType::BLOCK); case MSG_FILTERED_BLOCK: return cmd.append(NetMsgType::MERKLEBLOCK); case MSG_CMPCT_BLOCK: return cmd.append(NetMsgType::CMPCTBLOCK); + case MSG_MWEB_HEADER: return cmd.append(NetMsgType::MWEBHEADER); default: throw std::out_of_range(strprintf("CInv::GetCommand(): type=%d unknown type", type)); } diff --git a/src/protocol.h b/src/protocol.h index 7621ea6996..3715987735 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -260,6 +260,13 @@ extern const char* CFCHECKPT; * @since protocol version 70016 as described by BIP 339. */ extern const char* WTXIDRELAY; +/** + * Contains a CMerkleBlockWithMWEB. + * Sent in response to a getdata message which requested a + * block using the inventory type MSG_MWEB_HEADER. + * @since protocol version 70017 as described by LIP-0007 + */ +extern const char* MWEBHEADER; }; // namespace NetMsgType /* Get a vector of all valid message types (see above) */ @@ -427,6 +434,7 @@ enum GetDataMsg : uint32_t { // MSG_FILTERED_WITNESS_BLOCK = MSG_FILTERED_BLOCK | MSG_WITNESS_FLAG, MSG_MWEB_BLOCK = MSG_WITNESS_BLOCK | MSG_MWEB_FLAG, MSG_MWEB_TX = MSG_WITNESS_TX | MSG_MWEB_FLAG, + MSG_MWEB_HEADER = 8 | MSG_MWEB_FLAG, //!< Defined in LIP-0007 }; /** inv message data */ @@ -451,6 +459,7 @@ public: bool IsMsgCmpctBlk() const { return type == MSG_CMPCT_BLOCK; } bool IsMsgWitnessBlk() const { return type == MSG_WITNESS_BLOCK; } bool IsMsgMWEBBlk() const { return type == MSG_MWEB_BLOCK; } + bool IsMsgMWEBHeader() const { return type == MSG_MWEB_HEADER; } // Combined-message helper methods bool IsGenTxMsg() const @@ -459,7 +468,7 @@ public: } bool IsGenBlkMsg() const { - return type == MSG_BLOCK || type == MSG_FILTERED_BLOCK || type == MSG_CMPCT_BLOCK || type == MSG_WITNESS_BLOCK || type == MSG_MWEB_BLOCK; + return type == MSG_BLOCK || type == MSG_FILTERED_BLOCK || type == MSG_CMPCT_BLOCK || type == MSG_WITNESS_BLOCK || type == MSG_MWEB_BLOCK || type == MSG_MWEB_HEADER; } uint32_t type; diff --git a/src/version.h b/src/version.h index 019c3a3ae7..13b65403e3 100644 --- a/src/version.h +++ b/src/version.h @@ -9,7 +9,7 @@ * network protocol versioning */ -static const int PROTOCOL_VERSION = 70016; +static const int PROTOCOL_VERSION = 70017; //! initial proto version, to be increased after version/verack negotiation static const int INIT_PROTO_VERSION = 209; @@ -38,6 +38,9 @@ static const int INVALID_CB_NO_BAN_VERSION = 70015; //! "wtxidrelay" command for wtxid-based relay starts with this version static const int WTXID_RELAY_VERSION = 70016; +//! "mwebheader" command for light client MWEB support starts with this version +static const int MWEB_SYNC_VERSION = 70017; + // Make sure that none of the values above collide with // `SERIALIZE_TRANSACTION_NO_WITNESS` or `ADDRV2_FORMAT`. diff --git a/test/functional/mweb_mining.py b/test/functional/mweb_mining.py index 17f1e19045..27d214dead 100644 --- a/test/functional/mweb_mining.py +++ b/test/functional/mweb_mining.py @@ -8,7 +8,7 @@ from test_framework.blocktools import (create_coinbase, NORMAL_GBT_REQUEST_PARAM from test_framework.messages import (CBlock, MWEBBlock) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal -from test_framework.ltc_util import create_hogex, get_mweb_header_tip, setup_mweb_chain +from test_framework.ltc_util import create_hogex, get_mweb_header, setup_mweb_chain class MWEBMiningTest(BitcoinTestFramework): def set_test_params(self): @@ -28,7 +28,7 @@ class MWEBMiningTest(BitcoinTestFramework): next_height = int(gbt["height"]) # Build MWEB block - mweb_header = get_mweb_header_tip(node) + mweb_header = get_mweb_header(node) mweb_header.height = next_height mweb_header.rehash() mweb_block = MWEBBlock(mweb_header) @@ -46,7 +46,7 @@ class MWEBMiningTest(BitcoinTestFramework): block.nBits = int(gbt["bits"], 16) block.nNonce = 0 block.vtx = vtx - block.mweb_block = mweb_block.serialize().hex() + block.mweb_block = mweb_block block.hashMerkleRoot = block.calc_merkle_root() # Call getblocktemplate with the block proposal diff --git a/test/functional/mweb_p2p.py b/test/functional/mweb_p2p.py new file mode 100644 index 0000000000..773bef1e34 --- /dev/null +++ b/test/functional/mweb_p2p.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test LIP-0007 + +1. Test getdata 'mwebheader' *before* MWEB activation +2. Test getdata 'mwebheader' *after* MWEB activation +""" + +from test_framework.messages import ( + CBlockHeader, + CInv, + Hash, + hash256, + msg_getdata, + MSG_MWEB_HEADER, +) +from test_framework.p2p import P2PInterface, p2p_lock +from test_framework.script import MAX_SCRIPT_ELEMENT_SIZE +from test_framework.test_framework import BitcoinTestFramework +from test_framework.ltc_util import FIRST_MWEB_HEIGHT, get_hogex_tx, get_mweb_header +from test_framework.util import assert_equal + +# Can be used to mimic a light client requesting MWEB data from a full node +class MockLightClient(P2PInterface): + def __init__(self): + super().__init__() + self.merkle_blocks_with_mweb = {} + self.block_headers = {} + + def request_mweb_header(self, block_hash): + want = msg_getdata([CInv(MSG_MWEB_HEADER, int(block_hash, 16))]) + self.send_message(want) + + def on_mwebheader(self, message): + self.merkle_blocks_with_mweb[message.header_hash()] = message.merkleblockwithmweb + + def on_block(self, message): + message.block.calc_sha256() + self.block_headers[Hash(message.block.sha256)] = CBlockHeader(message.block) + + +class MWEBP2PTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.extra_args = [['-whitelist=noban@127.0.0.1']] # immediate tx relay + self.num_nodes = 1 + + def run_test(self): + node = self.nodes[0] + light_client = node.add_p2p_connection(MockLightClient()) + + self.log.info("Generate pre-MWEB blocks") + pre_mweb_block_hash = node.generate(FIRST_MWEB_HEIGHT - 1)[-1] + + self.log.info("Request mweb_header for pre-MWEB block '{}'".format(pre_mweb_block_hash)) + light_client.request_mweb_header(pre_mweb_block_hash) + + self.log.info("Activate MWEB") + node.sendtoaddress(node.getnewaddress(address_type='mweb'), 1) + post_mweb_block_hash = node.generate(1)[0] + + self.log.info("Request mweb_header for block '{}'".format(post_mweb_block_hash)) + light_client.request_mweb_header(post_mweb_block_hash) + + self.log.info("Waiting for mweb_header") + light_client.wait_for_mwebheader(post_mweb_block_hash, 5) + light_client.wait_for_block(int(post_mweb_block_hash, 16), 5) + + self.log.info("Assert results") + + # Before MWEB activation, no merkle block should be returned + assert pre_mweb_block_hash not in light_client.merkle_blocks_with_mweb + + # After MWEB activation, the requested merkle block should be returned + assert post_mweb_block_hash in light_client.merkle_blocks_with_mweb + merkle_block_with_mweb = light_client.merkle_blocks_with_mweb[post_mweb_block_hash] + + # Check block header is correct + assert Hash.from_hex(post_mweb_block_hash) in light_client.block_headers + block_header = light_client.block_headers[Hash.from_hex(post_mweb_block_hash)] + assert_equal(block_header, merkle_block_with_mweb.merkle.header) + + # Check MWEB header is correct + mweb_header = get_mweb_header(node, post_mweb_block_hash) + assert_equal(mweb_header, merkle_block_with_mweb.mweb_header) + + # Check HogEx transaction is correct + hogex_tx = get_hogex_tx(node, post_mweb_block_hash) + assert_equal(hogex_tx, merkle_block_with_mweb.hogex) + + # Check Merkle tree + merkle_tree = merkle_block_with_mweb.merkle.txn + assert_equal(3, merkle_tree.nTransactions) + assert_equal(2, len(merkle_tree.vHash)) + + left_hash = Hash(merkle_tree.vHash[0]) + right_hash = Hash(merkle_tree.vHash[1]) + assert_equal(Hash.from_hex(hogex_tx.hash), right_hash) + + right_branch_bytes = hash256(right_hash.serialize() + right_hash.serialize()) + merkle_root_bytes = hash256(left_hash.serialize() + right_branch_bytes) + assert_equal(Hash.from_byte_arr(merkle_root_bytes), Hash(block_header.hashMerkleRoot)) + +if __name__ == '__main__': + MWEBP2PTest().main() \ No newline at end of file diff --git a/test/functional/test_framework/ltc_util.py b/test/functional/test_framework/ltc_util.py index 9a01b7d9cd..95c46451bb 100644 --- a/test/functional/test_framework/ltc_util.py +++ b/test/functional/test_framework/ltc_util.py @@ -6,17 +6,22 @@ import os -from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, MWEBHeader +from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxOut, FromHex, MWEBHeader from test_framework.util import get_datadir_path, initialize_datadir, satoshi_round from test_framework.script_util import DUMMY_P2WPKH_SCRIPT, hogaddr_script from test_framework.test_node import TestNode +FIRST_MWEB_HEIGHT = 432 # Height of the first block to contain an MWEB if using default regtest params + + """Create a txout with a given amount and scriptPubKey Mines coins as needed. confirmed - txouts created will be confirmed in the blockchain; unconfirmed otherwise. + +Returns the 'COutPoint' of the UTXO """ def make_utxo(node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): fee = 1*COIN @@ -56,26 +61,59 @@ def make_utxo(node, amount, confirmed=True, scriptPubKey=DUMMY_P2WPKH_SCRIPT): return COutPoint(int(txid, 16), 0) -def setup_mweb_chain(node): + +"""Generates all pre-MWEB blocks, pegs 1 coin into the MWEB, +then mines the first MWEB block which includes that pegin. + +mining_node - The node to use to generate blocks +""" +def setup_mweb_chain(mining_node): # Create all pre-MWEB blocks - node.generate(431) + mining_node.generate(FIRST_MWEB_HEIGHT - 1) # Pegin some coins - node.sendtoaddress(node.getnewaddress(address_type='mweb'), 1) + mining_node.sendtoaddress(mining_node.getnewaddress(address_type='mweb'), 1) # Create some blocks - activate MWEB - node.generate(1) + mining_node.generate(1) -def get_hog_addr_txout(node): - best_block = node.getblock(node.getbestblockhash(), 2) - hogex_tx = best_block['tx'][-1] # TODO: Should validate that the tx is marked as a hogex tx - hog_addr = hogex_tx['vout'][0] +"""Retrieves the HogEx transaction for the block. - return CTxOut(int(hog_addr['value'] * COIN), hog_addr['scriptPubKey']) +node - The node to use to lookup the transaction. +block_hash - The block to retrieve the HogEx for. If not provided, the best block hash will be used. -def get_mweb_header_tip(node): - best_block = node.getblock(node.getbestblockhash(), 2) +Returns the HogEx as a 'CTransaction' +""" +def get_hogex_tx(node, block_hash = None): + block_hash = block_hash or node.getbestblockhash() + hogex_tx_id = node.getblock(block_hash)['tx'][-1] + hogex_tx = FromHex(CTransaction(), node.getrawtransaction(hogex_tx_id, False, block_hash)) # TODO: Should validate that the tx is marked as a hogex tx + hogex_tx.rehash() + return hogex_tx + + +"""Retrieves the HogAddr for a block. + +node - The node to use to lookup the HogAddr. +block_hash - The block to retrieve the HogAddr for. If not provided, the best block hash will be used. + +Returns the HogAddr as a 'CTxOut' +""" +def get_hog_addr_txout(node, block_hash = None): + return get_hogex_tx(node, block_hash).vout[0] + + +"""Retrieves the MWEB header for a block. + +node - The node to use to lookup the MWEB header. +block_hash - The block to retrieve the MWEB header for. If not provided, the best block hash will be used. + +Returns the MWEB header as an 'MWEBHeader' or None if the block doesn't contain MWEB data. +""" +def get_mweb_header(node, block_hash = None): + block_hash = block_hash or node.getbestblockhash() + best_block = node.getblock(block_hash, 2) if not 'mweb' in best_block: return None @@ -83,19 +121,26 @@ def get_mweb_header_tip(node): mweb_header.from_json(best_block['mweb']) return mweb_header -def create_hogex(node, mweb_hash): - best_block = node.getblock(node.getbestblockhash(), 2) + +"""Creates a HogEx transaction that commits to the provided MWEB hash. +# TODO: In the future, this should support passing in pegins and pegouts. - hogex_tx = best_block['tx'][-1] # TODO: Should validate that the tx is marked as a hogex tx - hog_addr = hogex_tx['vout'][0] +node - The node to use to lookup the latest block. +mweb_hash - The block to retrieve the MWEB header for. If not provided, the best block hash will be used. + +Returns the built HogEx transaction as a 'CTransaction' +""" +def create_hogex(node, mweb_hash): + hogex_tx = get_hogex_tx(node) tx = CTransaction() - tx.vin = [CTxIn(COutPoint(int(hogex_tx['txid'], 16), 0))] - tx.vout = [CTxOut(int(hog_addr['value'] * COIN), hogaddr_script(mweb_hash))] + tx.vin = [CTxIn(COutPoint(hogex_tx.sha256, 0))] + tx.vout = [CTxOut(hogex_tx.vout[0].nValue, hogaddr_script(mweb_hash.to_byte_arr()))] tx.hogex = True tx.rehash() return tx + """ Create a non-HD wallet from a temporary v15.1.0 node. Returns the path of the wallet.dat. diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index b9cf506869..8811427f80 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -34,7 +34,7 @@ from test_framework.siphash import siphash256 from test_framework.util import hex_str_to_bytes, assert_equal MIN_VERSION_SUPPORTED = 60001 -MY_VERSION = 70016 # past wtxid relay +MY_VERSION = 70017 # past wtxid relay MY_SUBVERSION = b"/python-p2p-tester:0.0.3/" MY_RELAY = 1 # from version 70001 onwards, fRelay should be appended to version messages (BIP37) @@ -71,6 +71,7 @@ MSG_TYPE_MASK = 0xffffffff >> 3 MSG_WITNESS_TX = MSG_TX | MSG_WITNESS_FLAG MSG_MWEB_BLOCK = MSG_BLOCK | MSG_WITNESS_FLAG | MSG_MWEB_FLAG MSG_MWEB_TX = MSG_WITNESS_TX | MSG_MWEB_FLAG +MSG_MWEB_HEADER = 8 | MSG_MWEB_FLAG FILTER_TYPE_BASIC = 0 @@ -142,6 +143,52 @@ def uint256_from_compact(c): return v +def deser_fixed_bytes(f, size): + r = [] + for i in range(size): + r.append(struct.unpack("B", f.read(1))[0]) + return r + +def ser_fixed_bytes(u, size): + rs = b"" + for i in range(size): + rs += struct.pack("B", u[i]) + #rs += struct.pack("B", u & 0xFF) + #u >>= 8 + return rs + + +def deser_pubkey(f): + r = 0 + for i in range(33): + t = struct.unpack("B", f.read(1))[0] + r += t << (i * 8) + return r + +def ser_pubkey(u): + rs = b"" + for _ in range(33): + rs += struct.pack("B", u & 0xFF) + u >>= 8 + return rs + + +def deser_signature(f): + r = 0 + for i in range(64): + t = struct.unpack("B", f.read(1))[0] + r += t << (i * 8) + return r + +def ser_signature(u): + rs = b"" + for _ in range(64): + rs += struct.pack("B", u & 0xFF) + u >>= 8 + return rs + + + # deser_function_name: Allow for an alternate deserialization function on the # entries in the vector. def deser_vector(f, c, deser_function_name=None): @@ -307,6 +354,7 @@ class CInv: MSG_FILTERED_BLOCK: "filtered Block", MSG_CMPCT_BLOCK: "CompactBlock", MSG_WTX: "WTX", + MSG_MWEB_HEADER: "MWEB Header" } def __init__(self, t=0, h=0): @@ -647,6 +695,8 @@ class CTransaction: return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) + def __eq__(self, other): + return isinstance(other, CTransaction) and repr(self) == repr(other) class CBlockHeader: __slots__ = ("hash", "hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", @@ -723,6 +773,9 @@ class CBlockHeader: % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, time.ctime(self.nTime), self.nBits, self.nNonce) + def __eq__(self, other): + return isinstance(other, CBlockHeader) and repr(self) == repr(other) + BLOCK_HEADER_SIZE = len(CBlockHeader().serialize()) assert_equal(BLOCK_HEADER_SIZE, 80) @@ -1871,8 +1924,55 @@ class msg_cfcheckpt: import blake3 as BLAKE3 +def hex_reverse(h): + return "".join(reversed([h[i:i+2] for i in range(0, len(h), 2)])) + +class Hash: + __slots__ = ("val") + + def __init__(self, val = 0): + self.val = val + + @classmethod + def from_hex(cls, hex_str): + return cls(int(hex_str, 16)) + + @classmethod + def from_rev_hex(cls, hex_str): + return cls(int(hex_reverse(hex_str), 16)) + + @classmethod + def from_byte_arr(cls, b): + return cls(deser_uint256(BytesIO(b))) + + def to_hex(self): + return self.serialize().hex() + + def to_byte_arr(self): + return hex_str_to_bytes(self.to_hex()) + + @classmethod + def deserialize(cls, f): + return cls(deser_uint256(f)) + + def serialize(self): + return ser_uint256(self.val) + + def __hash__(self): + return hash(self.val) + + def __str__(self): + return repr(self) + + def __repr__(self): + return "Hash(val=0x{:x})".format(self.val) + + def __eq__(self, other): + return isinstance(other, Hash) and self.val == other.val + + def blake3(s): - return BLAKE3.blake3(s).hexdigest() + return Hash.from_rev_hex(BLAKE3.blake3(s).hexdigest()) def ser_varint(n): r = b"" @@ -1904,7 +2004,7 @@ def ser_mweb_block(b): if b == None: return struct.pack("B", 0) else: - return struct.pack("B", 1) + hex_str_to_bytes(b) + return struct.pack("B", 1) + b.serialize() def deser_mweb_block(f): has_mweb = struct.unpack("B", f.read(1))[0] @@ -1919,14 +2019,260 @@ def ser_mweb_tx(t): if t == None: return struct.pack("B", 0) else: - return struct.pack("B", 1) + hex_str_to_bytes(t) + return struct.pack("B", 1) + t.serialize() def deser_mweb_tx(f): has_mweb = struct.unpack("B", f.read(1))[0] if has_mweb == 1: - return binascii.hexlify(f.read()) + mweb_tx = MWEBTransaction() + return mweb_tx.deserialize(f) else: return None + + +class MWEBInput: + __slots__ = ("features", "output_id", "commitment", "input_pubkey", + "output_pubkey", "extradata", "signature", "hash") + + def __init__(self): + self.features = 0 + self.output_id = Hash() + self.commitment = None + self.input_pubkey = None + self.output_pubkey = None + self.extradata = None + self.signature = None + self.hash = None + + def deserialize(self, f): + self.features = f.read(1) + self.output_id = Hash.deserialize(f) + self.commitment = deser_pubkey(f) + self.output_pubkey = deser_pubkey(f) + if self.features & 1: + self.input_pubkey = deser_pubkey(f) + self.extradata = None + if self.features & 2: + self.extradata = deser_fixed_bytes(f, deser_compact_size(f)) + self.signature = deser_signature(f) + self.rehash() + + def serialize(self): + r = b"" + r += struct.pack("B", self.features) + r += ser_uint256(self.output_id) + r += ser_pubkey(self.commitment) + r += ser_pubkey(self.output_pubkey) + if self.features & 1: + r += ser_pubkey(self.input_pubkey) + if self.features & 2: + r += ser_compact_size(self.extradata) + r += ser_fixed_bytes(self.extradata, len(self.extradata))#extradata + r += ser_signature(self.signature) + return r + + def rehash(self): + self.hash = blake3(self.serialize()) + return self.hash.to_hex() + +class MWEBOutputMessage: + __slots__ = ("features", "key_exchange_pubkey", "view_tag", "masked_value", + "masked_nonce", "extradata", "hash") + + def __init__(self): + self.features = 0 + self.key_exchange_pubkey = None + self.view_tag = 0 + self.masked_value = None + self.masked_nonce = None + self.extradata = None + self.hash = None + + def deserialize(self, f): + self.features = struct.unpack("B", f.read(1))[0] + if self.features & 1: + self.key_exchange_pubkey = deser_pubkey(f) + self.view_tag = struct.unpack("B", f.read(1))[0] + self.masked_value = struct.unpack("