From 971409030b2076ed3c19cff2c1c31159fe705689 Mon Sep 17 00:00:00 2001 From: iteye Date: Mon, 6 Jul 2026 15:49:06 +0800 Subject: [PATCH] Init wire Messages --- qkc/cluster/wire/messages.go | 897 +++++++++++++++++++++++ qkc/cluster/wire/messages_test.go | 646 ++++++++++++++++ qkc/cluster/wire/protocol.go | 236 ++++++ qkc/cluster/wire/rawbytes_placeholder.go | 57 ++ qkc/cluster/wire/types.go | 90 +++ 5 files changed, 1926 insertions(+) create mode 100644 qkc/cluster/wire/messages.go create mode 100644 qkc/cluster/wire/messages_test.go create mode 100644 qkc/cluster/wire/protocol.go create mode 100644 qkc/cluster/wire/rawbytes_placeholder.go create mode 100644 qkc/cluster/wire/types.go diff --git a/qkc/cluster/wire/messages.go b/qkc/cluster/wire/messages.go new file mode 100644 index 0000000000..a58f8b06b4 --- /dev/null +++ b/qkc/cluster/wire/messages.go @@ -0,0 +1,897 @@ +// Copyright 2026-2027, QuarkChain. + +// Package wire: serializable message structs for every cluster RPC opcode. +// +// Each struct mirrors a pyquarkchain Serializable from +// quarkchain/cluster/rpc.py (ClusterOp messages) or +// quarkchain/cluster/p2p_commands.py (CommandOp messages). +// +// Field layout MUST stay byte-compatible with the Python wire format. Every +// field name matches the Python FIELDS order and the wire encoding is enforced +// by qkc/serialize/ struct tags (see typecache.go): +// +// bytesizeofslicelen:"4" 4-byte big-endian length prefix for slices +// (Python PrependedSizeBytesSerializer(4) and +// PrependedSizeListSerializer(4, T)) +// ser:"nil" nullable pointer — 1-byte presence marker +// (Python Optional(T)) +// ser:"-" ignored field (not serialised) +// +// Primitive type mapping (Python → Go): +// +// uint8 → uint8 1 byte +// uint16 → uint16 2 bytes big-endian +// uint32 → uint32 4 bytes big-endian +// uint64 → uint64 8 bytes big-endian +// uint128 → [16]byte 16 bytes big-endian +// uint256 → *big.Int 1-byte length prefix + big-endian bytes +// biguint → *big.Int same as uint256 +// hash256 → [32]byte 32 bytes +// Branch → uint32 4 bytes +// Address → [20]byte 20 bytes +// signature65 → [65]byte 65 bytes +// boolean → bool 1 byte (0x00 / 0x01) +// +// ============================================================================= +// Placeholder: RawBytes +// ============================================================================= +// +// pyquarkchain defines many complex Serializable types (RootBlock, +// MinorBlockHeader, TypedTransaction, CrossShardTransactionList, +// TokenBalanceMap, TransactionReceipt, Log, MinorBlock, RootBlockHeader) +// that are NOT yet ported to Go. They all have a Python FIELDS list, so +// the Go wire length for any given field is well-defined — it just depends +// on the future Go type's encoding. +// +// To keep this PR self-contained and the wire layout pinned down NOW, each +// not-yet-ported type is referenced as *RawBytes. RawBytes is a transparent +// Serializable that round-trips arbitrary bytes, matching the wire length +// of the future Go struct once the fields are filled in. When the real Go +// type lands, only the field type needs to change — the wire encoding stays +// byte-identical. +// +// SAFETY: RawBytes.Deserialize consumes ALL remaining bytes in the buffer. +// It is only safe when the *RawBytes field is the LAST field of its parent +// struct. Fields that are not last are annotated with a WARNING and cannot be +// correctly deserialized until the real Go type is ported. +// +// ============================================================================= +// Layout +// ============================================================================= +// +// Grouped to match the wire opcode sections in opcode.go: +// +// §1 Cluster initialisation (PING, CONNECT_TO_SLAVES, MINE, GEN_TX) +// §2 Virtual connection mgmt (CREATE/DESTROY_CLUSTER_PEER) +// §3 Block updates (ADD_ROOT_BLOCK, ADD_MINOR_BLOCK, +// SYNC_MINOR_BLOCK_LIST, CHECK_MINOR_BLOCK, +// GET_UNCONFIRMED_HEADERS, ADD_MINOR_BLOCK_HEADER) +// §4 Block queries (GET_ECO_INFO_LIST, GET_NEXT_BLOCK_TO_MINE, +// GET_MINOR_BLOCK, GET_TRANSACTION, EXECUTE_TX, +// GET_TX_RECEIPT, GET_TX_LIST_BY_ADDRESS, +// GET_ALL_TX, GET_LOG, ESTIMATE_GAS, GET_STORAGE, +// GET_CODE, GAS_PRICE, GET_WORK, SUBMIT_WORK) +// §5 Account / staking (GET_ACCOUNT_DATA, GET_ROOT_CHAIN_STAKES, +// GET_TOTAL_BALANCE) +// §6 Cross-shard (Slave↔Slave) (ADD_XSHARD_TX_LIST, BATCH_ADD_XSHARD_TX_LIST) +// §7 P2P commands (HELLO, NEW_MINOR_BLOCK_HEADER_LIST, +// NEW_TRANSACTION_LIST, NEW_BLOCK_MINOR, +// PING_PONG, NEW_ROOT_BLOCK) +// §8 P2P queries (GET_ROOT_BLOCK_*, GET_MINOR_BLOCK_*) +// +// Every struct is defined in one place to keep the opcode-to-struct mapping +// in protocol.go complete and avoid scattering definitions across PRs. +package wire + +import ( + "math/big" +) + +// ============================================================================= +// Wire-level address / branch / hash constants +// ============================================================================= +// +// These match quarkchain/core.py:Constant and Python Branch/Address types. + +// AddressLength is the byte length of an Address (20 bytes). +const AddressLength = 20 + +// HashLength is the byte length of a hash256. +const HashLength = 32 + +// SignatureLength is the byte length of a signature65. +const SignatureLength = 65 + +// UInt128Length is the byte length of a uint128. +const UInt128Length = 16 + +// ============================================================================= +// §1 Cluster initialisation +// ============================================================================= + +// PingRequest (ClusterOp.PING, 0x81) — sent by master to initialise a slave. +// +// FIELDS = [ +// ("id", PrependedSizeBytesSerializer(4)), +// ("full_shard_id_list", PrependedSizeListSerializer(4, uint32)), +// ("root_tip", Optional(RootBlock)), +// ] +type PingRequest struct { + ID []byte `bytesizeofslicelen:"4"` + FullShardIDList []uint32 `bytesizeofslicelen:"4"` + RootTip *RawBytes `ser:"nil"` // TODO: Replace with *RootBlock once core.RootBlock is ported +} + +// PongResponse (ClusterOp.PONG, 0x82) — slave's reply to PING. +// +// FIELDS = [ +// ("id", PrependedSizeBytesSerializer(4)), +// ("full_shard_id_list", PrependedSizeListSerializer(4, uint32)), +// ] +type PongResponse struct { + ID []byte `bytesizeofslicelen:"4"` + FullShardIDList []uint32 `bytesizeofslicelen:"4"` +} + +// SlaveInfo (used by ConnectToSlavesRequest) — describes a remote slave. +// +// FIELDS = [ +// ("id", PrependedSizeBytesSerializer(4)), +// ("host", PrependedSizeBytesSerializer(4)), +// ("port", uint16), +// ("full_shard_id_list", PrependedSizeListSerializer(4, uint32)), +// ] +type SlaveInfo struct { + ID []byte `bytesizeofslicelen:"4"` + Host []byte `bytesizeofslicelen:"4"` + Port uint16 + FullShardIDList []uint32 `bytesizeofslicelen:"4"` +} + +// ConnectToSlavesRequest (ClusterOp.CONNECT_TO_SLAVES_REQUEST, 0x83). +// +// FIELDS = [("slave_info_list", PrependedSizeListSerializer(4, SlaveInfo))] +type ConnectToSlavesRequest struct { + SlaveInfoList []SlaveInfo `bytesizeofslicelen:"4"` +} + +// ConnectToSlavesResponse (ClusterOp.CONNECT_TO_SLAVES_RESPONSE, 0x84). +// +// result_list has the same size as slave_info_list; empty result = success, +// otherwise the bytes are a serialised error message. +// +// FIELDS = [ +// ("result_list", PrependedSizeListSerializer(4, PrependedSizeBytesSerializer(4))) +// ] +type ConnectToSlavesResponse struct { + ResultList []PrependedSizeBytes4 `bytesizeofslicelen:"4"` +} + +// ArtificialTxConfig — used by MineRequest / GetNextBlockToMineRequest / +// AddMinorBlockHeaderResponse. +// +// FIELDS = [("target_root_block_time", uint32), ("target_minor_block_time", uint32)] +type ArtificialTxConfig struct { + TargetRootBlockTime uint32 + TargetMinorBlockTime uint32 +} + +// MineRequest (ClusterOp.MINE_REQUEST, 0xA7) — start/stop mining on slaves. +// +// FIELDS = [("artificial_tx_config", ArtificialTxConfig), ("mining", boolean)] +type MineRequest struct { + ArtificialTxConfig ArtificialTxConfig + Mining bool +} + +// MineResponse (ClusterOp.MINE_RESPONSE, 0xA8). +type MineResponse struct { + ErrorCode uint32 +} + +// GenTxRequest (ClusterOp.GEN_TX_REQUEST, 0xA9) — generate transactions. +// +// FIELDS = [ +// ("num_tx_per_shard", uint32), +// ("x_shard_percent", uint32), # [0, 100] +// ("tx", TypedTransaction), +// ] +type GenTxRequest struct { + NumTxPerShard uint32 + XShardPercent uint32 + Tx *RawBytes // TODO: Replace with *TypedTransaction once core.TypedTransaction is ported +} + +// GenTxResponse (ClusterOp.GEN_TX_RESPONSE, 0xAA). +type GenTxResponse struct { + ErrorCode uint32 +} + +// ============================================================================= +// §2 Virtual connection management (mode 3 — Peer→Master→Slave) +// ============================================================================= + +// CreateClusterPeerConnectionRequest (ClusterOp.CREATE_CLUSTER_PEER_CONNECTION_REQUEST, 0x99). +type CreateClusterPeerConnectionRequest struct { + ClusterPeerID uint64 +} + +// CreateClusterPeerConnectionResponse (ClusterOp.CREATE_CLUSTER_PEER_CONNECTION_RESPONSE, 0x9A). +type CreateClusterPeerConnectionResponse struct { + ErrorCode uint32 +} + +// DestroyClusterPeerConnectionCommand (ClusterOp.DESTROY_CLUSTER_PEER_CONNECTION_COMMAND, 0x9B) +// — fire-and-forget, no response. +type DestroyClusterPeerConnectionCommand struct { + ClusterPeerID uint64 +} + +// ============================================================================= +// §3 Block updates +// ============================================================================= + +// AddRootBlockRequest (ClusterOp.ADD_ROOT_BLOCK_REQUEST, 0x85). +// +// FIELDS = [("root_block", RootBlock), ("expect_switch", boolean)] +type AddRootBlockRequest struct { + // TODO: Replace with *RootBlock once core.RootBlock is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + RootBlock *RawBytes + ExpectSwitch bool +} + +// AddRootBlockResponse (ClusterOp.ADD_ROOT_BLOCK_RESPONSE, 0x86). +type AddRootBlockResponse struct { + ErrorCode uint32 + Switched bool +} + +// EcoInfo — used by GetEcoInfoListResponse. +// +// FIELDS = [ +// ("branch", Branch), +// ("height", uint64), +// ("coinbase_amount", uint256), +// ("difficulty", biguint), +// ("unconfirmed_headers_coinbase_amount", uint256), +// ] +type EcoInfo struct { + Branch uint32 + Height uint64 + CoinbaseAmount *big.Int // uint256 + Difficulty *big.Int // biguint + UnconfirmedHeadersCoinbaseAmount *big.Int // uint256 +} + +// GetEcoInfoListRequest (ClusterOp.GET_ECO_INFO_LIST_REQUEST, 0x87) — empty body. +type GetEcoInfoListRequest struct{} + +// GetEcoInfoListResponse (ClusterOp.GET_ECO_INFO_LIST_RESPONSE, 0x88). +type GetEcoInfoListResponse struct { + ErrorCode uint32 + EcoInfoList []EcoInfo `bytesizeofslicelen:"4"` +} + +// GetNextBlockToMineRequest (ClusterOp.GET_NEXT_BLOCK_TO_MINE_REQUEST, 0x89). +// +// FIELDS = [ +// ("branch", Branch), +// ("address", Address), +// ("artificial_tx_config", ArtificialTxConfig), +// ] +type GetNextBlockToMineRequest struct { + Branch uint32 + Address [AddressLength]byte + ArtificialTxConfig ArtificialTxConfig +} + +// GetNextBlockToMineResponse (ClusterOp.GET_NEXT_BLOCK_TO_MINE_RESPONSE, 0x8A). +type GetNextBlockToMineResponse struct { + ErrorCode uint32 + Block *RawBytes // TODO: Replace with *MinorBlock once core.MinorBlock is ported +} + +// AddMinorBlockRequest (ClusterOp.ADD_MINOR_BLOCK_REQUEST, 0x97) — JRPC-mined blocks. +// +// FIELDS = [("minor_block_data", PrependedSizeBytesSerializer(4))] +type AddMinorBlockRequest struct { + MinorBlockData []byte `bytesizeofslicelen:"4"` +} + +// AddMinorBlockResponse (ClusterOp.ADD_MINOR_BLOCK_RESPONSE, 0x98). +type AddMinorBlockResponse struct { + ErrorCode uint32 +} + +// CheckMinorBlockRequest (ClusterOp.CHECK_MINOR_BLOCK_REQUEST, 0xBD). +type CheckMinorBlockRequest struct { + MinorBlockHeader *RawBytes // TODO: Replace with *MinorBlockHeader once core.MinorBlockHeader is ported +} + +// CheckMinorBlockResponse (ClusterOp.CHECK_MINOR_BLOCK_RESPONSE, 0xBE). +type CheckMinorBlockResponse struct { + ErrorCode uint32 +} + +// HeadersInfo — used by GetUnconfirmedHeadersResponse. +type HeadersInfo struct { + Branch uint32 + HeaderList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*MinorBlockHeader once core.MinorBlockHeader is ported +} + +// GetUnconfirmedHeadersRequest (ClusterOp.GET_UNCONFIRMED_HEADERS_REQUEST, 0x8B) — empty body. +type GetUnconfirmedHeadersRequest struct{} + +// GetUnconfirmedHeadersResponse (ClusterOp.GET_UNCONFIRMED_HEADERS_RESPONSE, 0x8C). +type GetUnconfirmedHeadersResponse struct { + ErrorCode uint32 + HeadersInfoList []HeadersInfo `bytesizeofslicelen:"4"` +} + +// AccountBranchData — used by GetAccountDataResponse. +// +// FIELDS = [ +// ("branch", Branch), +// ("transaction_count", uint256), +// ("token_balances", TokenBalanceMap), +// ("is_contract", boolean), +// ("posw_mineable_blocks", uint16), +// ("mined_blocks", uint16), +// ] +type AccountBranchData struct { + Branch uint32 + TransactionCount *big.Int // uint256 + // TODO: Replace with *TokenBalanceMap once core.TokenBalanceMap is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + TokenBalances *RawBytes + IsContract bool + PoswMineableBlocks uint16 + MinedBlocks uint16 +} + +// GetAccountDataRequest (ClusterOp.GET_ACCOUNT_DATA_REQUEST, 0x8D). +type GetAccountDataRequest struct { + Address [AddressLength]byte + BlockHeight *uint64 `ser:"nil"` // Optional uint64 +} + +// GetAccountDataResponse (ClusterOp.GET_ACCOUNT_DATA_RESPONSE, 0x8E). +type GetAccountDataResponse struct { + ErrorCode uint32 + AccountBranchDataList []AccountBranchData `bytesizeofslicelen:"4"` +} + +// AddTransactionRequest (ClusterOp.ADD_TRANSACTION_REQUEST, 0x8F). +type AddTransactionRequest struct { + Tx *RawBytes // TODO: Replace with *TypedTransaction once core.TypedTransaction is ported +} + +// AddTransactionResponse (ClusterOp.ADD_TRANSACTION_RESPONSE, 0x90). +type AddTransactionResponse struct { + ErrorCode uint32 +} + +// ShardStats — used by AddMinorBlockHeaderRequest / SyncMinorBlockListResponse. +// +// FIELDS = [ +// ("branch", Branch), +// ("height", uint64), +// ("difficulty", biguint), +// ("coinbase_address", Address), +// ("timestamp", uint64), +// ("tx_count60s", uint32), +// ("pending_tx_count", uint32), +// ("total_tx_count", uint32), +// ("block_count60s", uint32), +// ("stale_block_count60s", uint32), +// ("last_block_time", uint32), +// ] +type ShardStats struct { + Branch uint32 + Height uint64 + Difficulty *big.Int // biguint + CoinbaseAddress [AddressLength]byte + Timestamp uint64 + TxCount60s uint32 + PendingTxCount uint32 + TotalTxCount uint32 + BlockCount60s uint32 + StaleBlockCount60s uint32 + LastBlockTime uint32 +} + +// AddMinorBlockHeaderRequest (ClusterOp.ADD_MINOR_BLOCK_HEADER_REQUEST, 0x91) — slave→master. +// +// FIELDS = [ +// ("minor_block_header", MinorBlockHeader), +// ("tx_count", uint32), +// ("x_shard_tx_count", uint32), +// ("coinbase_amount_map", TokenBalanceMap), +// ("shard_stats", ShardStats), +// ] +type AddMinorBlockHeaderRequest struct { + // TODO: Replace with *MinorBlockHeader once core.MinorBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + MinorBlockHeader *RawBytes + TxCount uint32 + XShardTxCount uint32 + // TODO: Replace with *TokenBalanceMap once core.TokenBalanceMap is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + CoinbaseAmountMap *RawBytes + ShardStats ShardStats +} + +// AddMinorBlockHeaderResponse (ClusterOp.ADD_MINOR_BLOCK_HEADER_RESPONSE, 0x92). +type AddMinorBlockHeaderResponse struct { + ErrorCode uint32 + ArtificialTxConfig ArtificialTxConfig +} + +// AddMinorBlockHeaderListRequest (ClusterOp.ADD_MINOR_BLOCK_HEADER_LIST_REQUEST, 0xBB) — slave→master. +type AddMinorBlockHeaderListRequest struct { + MinorBlockHeaderList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*MinorBlockHeader once core.MinorBlockHeader is ported + CoinbaseAmountMapList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*TokenBalanceMap once core.TokenBalanceMap is ported +} + +// AddMinorBlockHeaderListResponse (ClusterOp.ADD_MINOR_BLOCK_HEADER_LIST_RESPONSE, 0xBC). +type AddMinorBlockHeaderListResponse struct { + ErrorCode uint32 +} + +// SyncMinorBlockListRequest (ClusterOp.SYNC_MINOR_BLOCK_LIST_REQUEST, 0x95). +type SyncMinorBlockListRequest struct { + MinorBlockHashList [][HashLength]byte `bytesizeofslicelen:"4"` + Branch uint32 + ClusterPeerID uint64 +} + +// SyncMinorBlockListResponse (ClusterOp.SYNC_MINOR_BLOCK_LIST_RESPONSE, 0x96). +// +// block_coinbase_map: PrependedSizeMapSerializer(4, hash256, TokenBalanceMap) +// +// FIELDS = [ +// ("error_code", uint32), +// ("block_coinbase_map", PrependedSizeMapSerializer(4, hash256, TokenBalanceMap)), +// ("shard_stats", Optional(ShardStats)), +// ] +type SyncMinorBlockListResponse struct { + ErrorCode uint32 + // TODO: Replace with real block_coinbase_map once core.TokenBalanceMap is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + BlockCoinbaseMap *RawBytes + ShardStats *ShardStats `ser:"nil"` +} + +// ============================================================================= +// §4 Block queries +// ============================================================================= + +// MinorBlockExtraInfo — used by GetMinorBlockResponse. +type MinorBlockExtraInfo struct { + EffectiveDifficulty *big.Int // biguint + PoswMineableBlocks uint16 + PoswMinedBlocks uint16 +} + +// GetMinorBlockRequest (ClusterOp.GET_MINOR_BLOCK_REQUEST, 0x9D). +type GetMinorBlockRequest struct { + Branch uint32 + MinorBlockHash [HashLength]byte + Height uint64 + NeedExtraInfo bool +} + +// GetMinorBlockResponse (ClusterOp.GET_MINOR_BLOCK_RESPONSE, 0x9E). +type GetMinorBlockResponse struct { + ErrorCode uint32 + // TODO: Replace with *MinorBlock once core.MinorBlock is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + MinorBlock *RawBytes + ExtraInfo *MinorBlockExtraInfo `ser:"nil"` +} + +// GetTransactionRequest (ClusterOp.GET_TRANSACTION_REQUEST, 0x9F). +type GetTransactionRequest struct { + TxHash [HashLength]byte + Branch uint32 +} + +// GetTransactionResponse (ClusterOp.GET_TRANSACTION_RESPONSE, 0xA0). +type GetTransactionResponse struct { + ErrorCode uint32 + // TODO: Replace with *MinorBlock once core.MinorBlock is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + MinorBlock *RawBytes + Index uint32 +} + +// ExecuteTransactionRequest (ClusterOp.EXECUTE_TRANSACTION_REQUEST, 0xA3). +type ExecuteTransactionRequest struct { + // TODO: Replace with *TypedTransaction once core.TypedTransaction is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + Tx *RawBytes + FromAddress [AddressLength]byte + BlockHeight *uint64 `ser:"nil"` +} + +// ExecuteTransactionResponse (ClusterOp.EXECUTE_TRANSACTION_RESPONSE, 0xA4). +type ExecuteTransactionResponse struct { + ErrorCode uint32 + Result []byte `bytesizeofslicelen:"4"` +} + +// GetTransactionReceiptRequest (ClusterOp.GET_TRANSACTION_RECEIPT_REQUEST, 0xA5). +type GetTransactionReceiptRequest struct { + TxHash [HashLength]byte + Branch uint32 +} + +// GetTransactionReceiptResponse (ClusterOp.GET_TRANSACTION_RECEIPT_RESPONSE, 0xA6). +type GetTransactionReceiptResponse struct { + ErrorCode uint32 + // TODO: Replace with *MinorBlock once core.MinorBlock is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + MinorBlock *RawBytes + Index uint32 + // TODO: Replace with *TransactionReceipt once core.TransactionReceipt is ported. + Receipt *RawBytes +} + +// TransactionDetail — used by GetTransactionListByAddressResponse and +// GetAllTransactionsResponse. +type TransactionDetail struct { + TxHash [HashLength]byte + Nonce uint64 + FromAddress [AddressLength]byte + ToAddress *[AddressLength]byte `ser:"nil"` // Optional Address + Value *big.Int // uint256 + BlockHeight uint64 + Timestamp uint64 + Success bool + GasTokenID uint64 + TransferTokenID uint64 + IsFromRootChain bool +} + +// GetTransactionListByAddressRequest (ClusterOp.GET_TRANSACTION_LIST_BY_ADDRESS_REQUEST, 0xAB). +type GetTransactionListByAddressRequest struct { + Address [AddressLength]byte + TransferTokenID *uint64 `ser:"nil"` + Start []byte `bytesizeofslicelen:"4"` + Limit uint32 +} + +// GetTransactionListByAddressResponse (ClusterOp.GET_TRANSACTION_LIST_BY_ADDRESS_RESPONSE, 0xAC). +type GetTransactionListByAddressResponse struct { + ErrorCode uint32 + TxList []TransactionDetail `bytesizeofslicelen:"4"` + Next []byte `bytesizeofslicelen:"4"` +} + +// GetAllTransactionsRequest (ClusterOp.GET_ALL_TRANSACTIONS_REQUEST, 0xBF). +type GetAllTransactionsRequest struct { + Branch uint32 + Start []byte `bytesizeofslicelen:"4"` + Limit uint32 +} + +// GetAllTransactionsResponse (ClusterOp.GET_ALL_TRANSACTIONS_RESPONSE, 0xC0). +type GetAllTransactionsResponse struct { + ErrorCode uint32 + TxList []TransactionDetail `bytesizeofslicelen:"4"` + Next []byte `bytesizeofslicelen:"4"` +} + +// GetLogRequest (ClusterOp.GET_LOG_REQUEST, 0xAD). +// +// FIELDS = [ +// ("branch", Branch), +// ("addresses", PrependedSizeListSerializer(4, Address)), +// ("topics", PrependedSizeListSerializer(4, PrependedSizeListSerializer(4, hash256))), +// ("start_block", uint64), +// ("end_block", uint64), +// ] +type GetLogRequest struct { + Branch uint32 + Addresses [][AddressLength]byte `bytesizeofslicelen:"4"` + Topics []PrependedSizeHashList4 `bytesizeofslicelen:"4"` + StartBlock uint64 + EndBlock uint64 +} + +// GetLogResponse (ClusterOp.GET_LOG_RESPONSE, 0xAE). +type GetLogResponse struct { + ErrorCode uint32 + Logs []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*Log once core.Log is ported +} + +// EstimateGasRequest (ClusterOp.ESTIMATE_GAS_REQUEST, 0xAF). +type EstimateGasRequest struct { + // TODO: Replace with *TypedTransaction once core.TypedTransaction is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + Tx *RawBytes + FromAddress [AddressLength]byte +} + +// EstimateGasResponse (ClusterOp.ESTIMATE_GAS_RESPONSE, 0xB0). +type EstimateGasResponse struct { + ErrorCode uint32 + Result uint32 +} + +// GetStorageRequest (ClusterOp.GET_STORAGE_REQUEST, 0xB1). +type GetStorageRequest struct { + Address [AddressLength]byte + Key *big.Int // uint256 + BlockHeight *uint64 `ser:"nil"` +} + +// GetStorageResponse (ClusterOp.GET_STORAGE_RESPONSE, 0xB2). +type GetStorageResponse struct { + ErrorCode uint32 + Result [HashLength]byte +} + +// GetCodeRequest (ClusterOp.GET_CODE_REQUEST, 0xB3). +type GetCodeRequest struct { + Address [AddressLength]byte + BlockHeight *uint64 `ser:"nil"` +} + +// GetCodeResponse (ClusterOp.GET_CODE_RESPONSE, 0xB4). +type GetCodeResponse struct { + ErrorCode uint32 + Result []byte `bytesizeofslicelen:"4"` +} + +// GasPriceRequest (ClusterOp.GAS_PRICE_REQUEST, 0xB5). +type GasPriceRequest struct { + Branch uint32 + TokenID uint64 +} + +// GasPriceResponse (ClusterOp.GAS_PRICE_RESPONSE, 0xB6). +type GasPriceResponse struct { + ErrorCode uint32 + Result uint64 +} + +// GetWorkRequest (ClusterOp.GET_WORK_REQUEST, 0xB7). +type GetWorkRequest struct { + Branch uint32 + CoinbaseAddr *[AddressLength]byte `ser:"nil"` // Optional Address +} + +// GetWorkResponse (ClusterOp.GET_WORK_RESPONSE, 0xB8). +type GetWorkResponse struct { + ErrorCode uint32 + HeaderHash [HashLength]byte + Height uint64 + Difficulty *big.Int // biguint +} + +// SubmitWorkRequest (ClusterOp.SUBMIT_WORK_REQUEST, 0xB9). +type SubmitWorkRequest struct { + Branch uint32 + HeaderHash [HashLength]byte + Nonce uint64 + Mixhash [HashLength]byte + Signature *[SignatureLength]byte `ser:"nil"` // Optional signature65 +} + +// SubmitWorkResponse (ClusterOp.SUBMIT_WORK_RESPONSE, 0xBA). +type SubmitWorkResponse struct { + ErrorCode uint32 + Success bool +} + +// ============================================================================= +// §5 Account / staking +// ============================================================================= + +// GetRootChainStakesRequest (ClusterOp.GET_ROOT_CHAIN_STAKES_REQUEST, 0xC1). +type GetRootChainStakesRequest struct { + Address [AddressLength]byte + MinorBlockHash [HashLength]byte +} + +// GetRootChainStakesResponse (ClusterOp.GET_ROOT_CHAIN_STAKES_RESPONSE, 0xC2). +type GetRootChainStakesResponse struct { + ErrorCode uint32 + Stakes *big.Int // biguint + Signer [AddressLength]byte +} + +// GetTotalBalanceRequest (ClusterOp.GET_TOTAL_BALANCE_REQUEST, 0xC3). +type GetTotalBalanceRequest struct { + Branch uint32 + Start *[HashLength]byte `ser:"nil"` // Optional hash256 + TokenID uint64 + Limit uint32 + MinorBlockHash [HashLength]byte + RootBlockHash *[HashLength]byte `ser:"nil"` // Optional hash256 +} + +// GetTotalBalanceResponse (ClusterOp.GET_TOTAL_BALANCE_RESPONSE, 0xC4). +type GetTotalBalanceResponse struct { + ErrorCode uint32 + TotalBalance *big.Int // biguint + Next []byte `bytesizeofslicelen:"4"` +} + +// ============================================================================= +// §6 Cross-shard (Slave↔Slave, direct TCP, no metadata) +// ============================================================================= + +// AddXshardTxListRequest (ClusterOp.ADD_XSHARD_TX_LIST_REQUEST, 0x93). +type AddXshardTxListRequest struct { + Branch uint32 + MinorBlockHash [HashLength]byte + TxList *RawBytes // TODO: Replace with *CrossShardTransactionList once core.CrossShardTransactionList is ported +} + +// AddXshardTxListResponse (ClusterOp.ADD_XSHARD_TX_LIST_RESPONSE, 0x94). +type AddXshardTxListResponse struct { + ErrorCode uint32 +} + +// BatchAddXshardTxListRequest (ClusterOp.BATCH_ADD_XSHARD_TX_LIST_REQUEST, 0xA1). +type BatchAddXshardTxListRequest struct { + AddXshardTxListRequestList []AddXshardTxListRequest `bytesizeofslicelen:"4"` +} + +// BatchAddXshardTxListResponse (ClusterOp.BATCH_ADD_XSHARD_TX_LIST_RESPONSE, 0xA2). +type BatchAddXshardTxListResponse struct { + ErrorCode uint32 +} + +// ============================================================================= +// §7 P2P commands (CommandOp, cluster_peer_id != 0) +// ============================================================================= + +// HelloCommand (CommandOp.HELLO, 0x00) — initial inter-cluster handshake. +// +// FIELDS = [ +// ("version", uint32), +// ("network_id", uint32), +// ("peer_id", hash256), +// ("peer_ip", uint128), +// ("peer_port", uint16), +// ("chain_mask_list", PrependedSizeListSerializer(4, uint32)), +// ("root_block_header", RootBlockHeader), +// ("genesis_root_block_hash", hash256), +// ] +type HelloCommand struct { + Version uint32 + NetworkID uint32 + PeerID [HashLength]byte + PeerIP [UInt128Length]byte + PeerPort uint16 + ChainMaskList []uint32 `bytesizeofslicelen:"4"` + // TODO: Replace with *RootBlockHeader once core.RootBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + RootBlockHeader *RawBytes + GenesisRootBlockHash [HashLength]byte +} + +// NewMinorBlockHeaderListCommand (CommandOp.NEW_MINOR_BLOCK_HEADER_LIST, 0x01). +type NewMinorBlockHeaderListCommand struct { + // TODO: Replace with *RootBlockHeader once core.RootBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + RootBlockHeader *RawBytes + MinorBlockHeaderList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*MinorBlockHeader once core.MinorBlockHeader is ported +} + +// NewTransactionListCommand (CommandOp.NEW_TRANSACTION_LIST, 0x02). +type NewTransactionListCommand struct { + TransactionList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*TypedTransaction once core.TypedTransaction is ported +} + +// NewBlockMinorCommand (CommandOp.NEW_BLOCK_MINOR, 0x0D). +type NewBlockMinorCommand struct { + Block *RawBytes // TODO: Replace with *MinorBlock once core.MinorBlock is ported +} + +// PingPongCommand (CommandOp.PING 0x0E, PONG 0x0F). +type PingPongCommand struct { + Message [HashLength]byte +} + +// NewRootBlockCommand (CommandOp.NEW_ROOT_BLOCK, 0x12). +type NewRootBlockCommand struct { + Block *RawBytes // TODO: Replace with *RootBlock once core.RootBlock is ported +} + +// ============================================================================= +// §8 P2P queries (CommandOp) +// ============================================================================= + +// PeerInfo — used by GetPeerListResponse. +type PeerInfo struct { + IP [UInt128Length]byte + Port uint16 +} + +// GetPeerListRequest (CommandOp.GET_PEER_LIST_REQUEST, 0x03). +type GetPeerListRequest struct { + MaxPeers uint32 +} + +// GetPeerListResponse (CommandOp.GET_PEER_LIST_RESPONSE, 0x04). +type GetPeerListResponse struct { + PeerInfoList []PeerInfo `bytesizeofslicelen:"4"` +} + +// GetRootBlockHeaderListRequest (CommandOp.GET_ROOT_BLOCK_HEADER_LIST_REQUEST, 0x05). +type GetRootBlockHeaderListRequest struct { + BlockHash [HashLength]byte + Limit uint32 + Direction Direction +} + +// GetRootBlockHeaderListResponse (CommandOp.GET_ROOT_BLOCK_HEADER_LIST_RESPONSE, 0x06). +type GetRootBlockHeaderListResponse struct { + // TODO: Replace with *RootBlockHeader once core.RootBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + RootTip *RawBytes + BlockHeaderList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*RootBlockHeader once core.RootBlockHeader is ported +} + +// GetRootBlockHeaderListWithSkipRequest (CommandOp.GET_ROOT_BLOCK_HEADER_LIST_WITH_SKIP_REQUEST, 0x10). +type GetRootBlockHeaderListWithSkipRequest struct { + Type uint8 + Data [HashLength]byte + Limit uint32 + Skip uint32 + Direction Direction +} + +// GetRootBlockListRequest (CommandOp.GET_ROOT_BLOCK_LIST_REQUEST, 0x07). +type GetRootBlockListRequest struct { + RootBlockHashList [][HashLength]byte `bytesizeofslicelen:"4"` +} + +// GetRootBlockListResponse (CommandOp.GET_ROOT_BLOCK_LIST_RESPONSE, 0x08). +type GetRootBlockListResponse struct { + RootBlockList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*RootBlock once core.RootBlock is ported +} + +// GetMinorBlockListRequest (CommandOp.GET_MINOR_BLOCK_LIST_REQUEST, 0x09). +type GetMinorBlockListRequest struct { + MinorBlockHashList [][HashLength]byte `bytesizeofslicelen:"4"` +} + +// GetMinorBlockListResponse (CommandOp.GET_MINOR_BLOCK_LIST_RESPONSE, 0x0A). +type GetMinorBlockListResponse struct { + MinorBlockList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*MinorBlock once core.MinorBlock is ported +} + +// GetMinorBlockHeaderListRequest (CommandOp.GET_MINOR_BLOCK_HEADER_LIST_REQUEST, 0x0B). +type GetMinorBlockHeaderListRequest struct { + BlockHash [HashLength]byte + Branch uint32 + Limit uint32 + Direction Direction +} + +// GetMinorBlockHeaderListResponse (CommandOp.GET_MINOR_BLOCK_HEADER_LIST_RESPONSE, 0x0C). +type GetMinorBlockHeaderListResponse struct { + // TODO: Replace with *RootBlockHeader once core.RootBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + RootTip *RawBytes + // TODO: Replace with *MinorBlockHeader once core.MinorBlockHeader is ported. + // WARNING: not the last field; RawBytes.Deserialize consumes remaining bytes. + ShardTip *RawBytes + BlockHeaderList []*RawBytes `bytesizeofslicelen:"4"` // TODO: Replace with []*MinorBlockHeader once core.MinorBlockHeader is ported +} + +// GetMinorBlockHeaderListWithSkipRequest (CommandOp.GET_MINOR_BLOCK_HEADER_LIST_WITH_SKIP_REQUEST, 0x13). +type GetMinorBlockHeaderListWithSkipRequest struct { + Type uint8 + Data [HashLength]byte + Branch uint32 + Limit uint32 + Skip uint32 + Direction Direction +} diff --git a/qkc/cluster/wire/messages_test.go b/qkc/cluster/wire/messages_test.go new file mode 100644 index 0000000000..9e85c7c0e2 --- /dev/null +++ b/qkc/cluster/wire/messages_test.go @@ -0,0 +1,646 @@ +// Copyright 2026-2027, QuarkChain. + +package wire + +import ( + "bytes" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/qkc/serialize" +) + +// ============================================================================= +// §1 Custom wire types +// ============================================================================= + +func TestPrependedSizeBytes4_RoundTrip(t *testing.T) { + cases := [][]byte{ + nil, + {}, + {0x00}, + {0xAA, 0xBB}, + bytes.Repeat([]byte{0xFF}, 100), + } + for i, data := range cases { + t.Run("", func(t *testing.T) { + p := PrependedSizeBytes4(data) + var buf []byte + if err := p.Serialize(&buf); err != nil { + t.Fatalf("case %d: Serialize: %v", i, err) + } + + bb := serialize.NewByteBuffer(buf) + var got PrependedSizeBytes4 + if err := got.Deserialize(bb); err != nil { + t.Fatalf("case %d: Deserialize: %v", i, err) + } + + if !bytes.Equal(got, data) { + t.Errorf("case %d: mismatch\n got %x\n want %x", i, got, data) + } + }) + } +} + +func TestPrependedSizeBytes4_WireFormat(t *testing.T) { + wantHex := "00000002aabb" + + p := PrependedSizeBytes4{0xAA, 0xBB} + var buf []byte + if err := p.Serialize(&buf); err != nil { + t.Fatalf("Serialize: %v", err) + } + + gotHex := hexEncode(buf) + if gotHex != wantHex { + t.Errorf("wire format mismatch:\n got %s\n want %s", gotHex, wantHex) + } + + bb := serialize.NewByteBuffer(buf) + var got PrependedSizeBytes4 + if err := got.Deserialize(bb); err != nil { + t.Fatalf("Deserialize: %v", err) + } + if !bytes.Equal(got, p) { + t.Errorf("round-trip mismatch") + } +} + +func TestPrependedSizeBytes4_Deserialize_InvalidLength(t *testing.T) { + buf := []byte{0xFF, 0xFF, 0xFF, 0xFF} + bb := serialize.NewByteBuffer(buf) + + var p PrependedSizeBytes4 + err := p.Deserialize(bb) + if err == nil { + t.Error("expected error for length exceeding remaining buffer") + } +} + +func TestPrependedSizeHashList4_RoundTrip(t *testing.T) { + cases := [][][HashLength]byte{ + nil, + {}, + {makeHash(1)}, + {makeHash(1), makeHash(2)}, + } + for i, hashes := range cases { + t.Run("", func(t *testing.T) { + p := PrependedSizeHashList4(hashes) + var buf []byte + if err := p.Serialize(&buf); err != nil { + t.Fatalf("case %d: Serialize: %v", i, err) + } + + bb := serialize.NewByteBuffer(buf) + var got PrependedSizeHashList4 + if err := got.Deserialize(bb); err != nil { + t.Fatalf("case %d: Deserialize: %v", i, err) + } + + if len(got) != len(hashes) { + t.Fatalf("case %d: length mismatch: got %d, want %d", i, len(got), len(hashes)) + } + for j := range got { + if got[j] != hashes[j] { + t.Errorf("case %d: hash[%d] mismatch", i, j) + } + } + }) + } +} + +func TestPrependedSizeHashList4_WireFormat(t *testing.T) { + p := PrependedSizeHashList4{makeHash(0x11), makeHash(0x22)} + var buf []byte + if err := p.Serialize(&buf); err != nil { + t.Fatalf("Serialize: %v", err) + } + + if len(buf) != 4+2*HashLength { + t.Errorf("wire length mismatch: got %d, want %d", len(buf), 4+2*HashLength) + } + + // count prefix is already checked by Deserialize; this is supplementary. + bb := serialize.NewByteBuffer(buf) + var got PrependedSizeHashList4 + if err := got.Deserialize(bb); err != nil { + t.Fatalf("Deserialize: %v", err) + } + if len(got) != 2 { + t.Errorf("round-trip length mismatch") + } +} + +func TestPrependedSizeHashList4_Deserialize_InvalidLength(t *testing.T) { + buf := []byte{0xFF, 0xFF, 0xFF, 0xFF} + bb := serialize.NewByteBuffer(buf) + + var p PrependedSizeHashList4 + err := p.Deserialize(bb) + if err == nil { + t.Error("expected error for count exceeding buffer capacity") + } +} + +// ============================================================================= +// §2 Message struct round-trips +// ============================================================================= +// +// Only structs with *RawBytes as the LAST field (or no RawBytes at all) can +// safely round-trip. Structs with non-last RawBytes are only verified via the +// factory completeness test (§7) — they serialize correctly but cannot be +// deserialized until the real Go type replaces RawBytes. + +func TestMessageRoundTrip(t *testing.T) { + toAddr := makeAddress(0x00010001, 2) + + tests := []struct { + name string + msg any + }{ + // --- no RawBytes --- + {"PingRequest_without_root_tip", PingRequest{ + ID: []byte("slave1"), + FullShardIDList: []uint32{0x00010001, 0x00020002}, + RootTip: nil, + }}, + {"PongResponse", PongResponse{ + ID: []byte("master"), + FullShardIDList: []uint32{0x00010001}, + }}, + {"SlaveInfo", SlaveInfo{ + ID: []byte("s1"), + Host: []byte("127.0.0.1"), + Port: 38391, + FullShardIDList: []uint32{0x00010001, 0x00010002}, + }}, + {"ConnectToSlavesRequest", ConnectToSlavesRequest{ + SlaveInfoList: []SlaveInfo{ + {ID: []byte("s1"), Host: []byte("10.0.0.1"), Port: 38391, FullShardIDList: []uint32{0x00010001}}, + {ID: []byte("s2"), Host: []byte("10.0.0.2"), Port: 38392, FullShardIDList: []uint32{0x00020001}}, + }, + }}, + {"ConnectToSlavesResponse", ConnectToSlavesResponse{ + ResultList: []PrependedSizeBytes4{{0xAA, 0xBB}, {0xCC, 0xDD, 0xEE}}, + }}, + {"ArtificialTxConfig", ArtificialTxConfig{60, 10}}, + {"MineRequest", MineRequest{ + ArtificialTxConfig: ArtificialTxConfig{TargetRootBlockTime: 60, TargetMinorBlockTime: 10}, + Mining: true, + }}, + {"EcoInfo", EcoInfo{ + Branch: 0x00010001, + Height: 12345, + CoinbaseAmount: big.NewInt(1000), + Difficulty: big.NewInt(1000000), + UnconfirmedHeadersCoinbaseAmount: big.NewInt(500), + }}, + {"GetEcoInfoListRequest", GetEcoInfoListRequest{}}, + {"GetEcoInfoListResponse", GetEcoInfoListResponse{ + ErrorCode: 0, + EcoInfoList: []EcoInfo{ + {Branch: 0x00010001, Height: 1, CoinbaseAmount: big.NewInt(1), Difficulty: big.NewInt(1), UnconfirmedHeadersCoinbaseAmount: big.NewInt(1)}, + }, + }}, + {"GetNextBlockToMineRequest", GetNextBlockToMineRequest{ + Branch: 0x00010001, + Address: makeAddress(0x00010001, 1), + ArtificialTxConfig: ArtificialTxConfig{TargetRootBlockTime: 60, TargetMinorBlockTime: 10}, + }}, + {"GetAccountDataRequest", GetAccountDataRequest{ + Address: makeAddress(0x00010001, 1), + BlockHeight: nil, + }}, + {"GetLogRequest", GetLogRequest{ + Branch: 0x00010001, + Addresses: [][AddressLength]byte{makeAddress(0x00010001, 1)}, + Topics: []PrependedSizeHashList4{ + {makeHash(0xAA)}, + {makeHash(0xBB), makeHash(0xCC)}, + }, + StartBlock: 100, + EndBlock: 200, + }}, + {"PingPongCommand", PingPongCommand{makeHash(42)}}, + {"PeerInfo", PeerInfo{IP: makeUint128(0x0102030405060708), Port: 38391}}, + {"TransactionDetail", TransactionDetail{ + TxHash: makeHash(1), + Nonce: 10, + FromAddress: makeAddress(0x00010001, 1), + ToAddress: &toAddr, + Value: big.NewInt(100), + BlockHeight: 50, + Timestamp: 1600000000, + Success: true, + GasTokenID: 1, + TransferTokenID: 1, + IsFromRootChain: false, + }}, + + // --- RawBytes as last field (safe to round-trip) --- + {"GenTxRequest", GenTxRequest{ + NumTxPerShard: 10, + XShardPercent: 30, + Tx: &RawBytes{0x01, 0x02, 0x03}, + }}, + {"GetNextBlockToMineResponse", GetNextBlockToMineResponse{ + ErrorCode: 0, + Block: &RawBytes{0xAA, 0xBB}, + }}, + {"AddTransactionRequest", AddTransactionRequest{ + Tx: &RawBytes{0x01, 0x02}, + }}, + {"CheckMinorBlockRequest", CheckMinorBlockRequest{ + MinorBlockHeader: &RawBytes{0x01, 0x02}, + }}, + {"GetLogResponse", GetLogResponse{ + ErrorCode: 0, + Logs: []*RawBytes{{0x01, 0x02}}, // single element only — multi-element []*RawBytes cannot round-trip + }}, + {"BatchAddXshardTxListRequest", BatchAddXshardTxListRequest{ + AddXshardTxListRequestList: []AddXshardTxListRequest{ + {Branch: 0x00010001, MinorBlockHash: makeHash(1), TxList: &RawBytes{0x01, 0x02}}, + }, + }}, + {"AddXshardTxListRequest", AddXshardTxListRequest{ + Branch: 0x00010001, + MinorBlockHash: makeHash(1), + TxList: &RawBytes{0x01, 0x02}, + }}, + {"NewTransactionListCommand", NewTransactionListCommand{ + TransactionList: []*RawBytes{{0x01, 0x02}}, // single element only + }}, + {"NewBlockMinorCommand", NewBlockMinorCommand{ + Block: &RawBytes{0x01, 0x02}, + }}, + {"NewRootBlockCommand", NewRootBlockCommand{ + Block: &RawBytes{0x01, 0x02}, + }}, + {"GetRootBlockListResponse", GetRootBlockListResponse{ + RootBlockList: []*RawBytes{{0x01, 0x02}}, // single element only + }}, + {"GetMinorBlockListResponse", GetMinorBlockListResponse{ + MinorBlockList: []*RawBytes{{0x01, 0x02}}, // single element only + }}, + {"GetUnconfirmedHeadersResponse", GetUnconfirmedHeadersResponse{ + ErrorCode: 0, + HeadersInfoList: []HeadersInfo{ + {Branch: 0x00010001, HeaderList: []*RawBytes{{0x01, 0x02}}}, // single element only + }, + }}, + {"PingRequest_with_root_tip", PingRequest{ + ID: []byte("test"), + FullShardIDList: []uint32{1, 2}, + RootTip: &RawBytes{0xAA, 0xBB, 0xCC}, + }}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var buf []byte + if err := serialize.Serialize(&buf, tc.msg); err != nil { + t.Fatalf("Serialize: %v", err) + } + + bb := serialize.NewByteBuffer(buf) + got := reflect.New(reflect.TypeOf(tc.msg)).Interface() + if err := serialize.Deserialize(bb, got); err != nil { + t.Fatalf("Deserialize: %v", err) + } + + gotVal := reflect.ValueOf(got).Elem().Interface() + + // Re-serialize and compare bytes. This sidesteps reflect.DeepEqual + // pointer-identity problems with *RawBytes while still verifying + // that the wire format is preserved through round-trip. + var buf2 []byte + if err := serialize.Serialize(&buf2, gotVal); err != nil { + t.Fatalf("Re-serialize: %v", err) + } + if !bytes.Equal(buf, buf2) { + t.Errorf("round-trip mismatch\n got %x\n want %x", buf2, buf) + } + }) + } +} + +// ============================================================================= +// §3 ser:"nil" behavior +// ============================================================================= + +func TestOptionalMarker_PresentAndAbsent(t *testing.T) { + absent := PingRequest{ID: []byte("x"), RootTip: nil} + var buf []byte + if err := serialize.Serialize(&buf, &absent); err != nil { + t.Fatalf("Serialize absent: %v", err) + } + if buf[len(buf)-1] != 0x00 { + t.Errorf("absent optional should end with 0x00, got %x", buf[len(buf)-1]) + } + + present := PingRequest{ID: []byte("x"), RootTip: &RawBytes{0xAA}} + buf = nil + if err := serialize.Serialize(&buf, &present); err != nil { + t.Fatalf("Serialize present: %v", err) + } + if buf[len(buf)-2] != 0x01 || buf[len(buf)-1] != 0xAA { + t.Errorf("present optional should write marker 0x01 then data, got %x", buf[len(buf)-2:]) + } +} + +func TestNonOptionalRawBytes_NoMarker(t *testing.T) { + req := AddRootBlockRequest{RootBlock: &RawBytes{0xAA}, ExpectSwitch: true} + var buf []byte + if err := serialize.Serialize(&buf, &req); err != nil { + t.Fatalf("Serialize: %v", err) + } + if buf[0] != 0xAA { + t.Errorf("non-optional RawBytes should not have presence marker, got %x", buf[0]) + } +} + +// ============================================================================= +// §4 Wire compatibility (hand-computed vectors) +// ============================================================================= +// +// These test vectors are hand-computed from the Python FIELDS definitions to +// verify that Go serialization produces identical bytes. They are NOT produced +// by running pyquarkchain directly. + +func TestWireCompat_PingRequest(t *testing.T) { + wantHex := "0000000474657374000000020000000100000002" + "00" + + ping := PingRequest{ + ID: []byte("test"), + FullShardIDList: []uint32{1, 2}, + RootTip: nil, + } + var buf []byte + if err := serialize.Serialize(&buf, &ping); err != nil { + t.Fatalf("Serialize: %v", err) + } + gotHex := hexEncode(buf) + if gotHex != wantHex { + t.Errorf("wire mismatch:\n got %s\n want %s", gotHex, wantHex) + } +} + +func TestWireCompat_PongResponse(t *testing.T) { + wantHex := "000000026f6b0000000100000003" + + pong := PongResponse{ + ID: []byte("ok"), + FullShardIDList: []uint32{3}, + } + var buf []byte + if err := serialize.Serialize(&buf, &pong); err != nil { + t.Fatalf("Serialize: %v", err) + } + gotHex := hexEncode(buf) + if gotHex != wantHex { + t.Errorf("wire mismatch:\n got %s\n want %s", gotHex, wantHex) + } +} + +func TestWireCompat_SlaveInfo(t *testing.T) { + wantHex := "000000027331" + + "000000096c6f63616c686f7374" + + "95f7" + + "0000000100010001" + + slave := SlaveInfo{ + ID: []byte("s1"), + Host: []byte("localhost"), + Port: 38391, + FullShardIDList: []uint32{0x00010001}, + } + var buf []byte + if err := serialize.Serialize(&buf, &slave); err != nil { + t.Fatalf("Serialize: %v", err) + } + gotHex := hexEncode(buf) + if gotHex != wantHex { + t.Errorf("wire mismatch:\n got %s\n want %s", gotHex, wantHex) + } +} + +// ============================================================================= +// §5 Field order verification +// ============================================================================= + +func TestFieldOrder(t *testing.T) { + cases := []struct { + name string + typ reflect.Type + expected []string + }{ + {"PingRequest", reflect.TypeFor[PingRequest](), []string{"ID", "FullShardIDList", "RootTip"}}, + {"PongResponse", reflect.TypeFor[PongResponse](), []string{"ID", "FullShardIDList"}}, + {"SlaveInfo", reflect.TypeFor[SlaveInfo](), []string{"ID", "Host", "Port", "FullShardIDList"}}, + {"EcoInfo", reflect.TypeFor[EcoInfo](), []string{"Branch", "Height", "CoinbaseAmount", "Difficulty", "UnconfirmedHeadersCoinbaseAmount"}}, + {"ShardStats", reflect.TypeFor[ShardStats](), []string{"Branch", "Height", "Difficulty", "CoinbaseAddress", "Timestamp", + "TxCount60s", "PendingTxCount", "TotalTxCount", "BlockCount60s", "StaleBlockCount60s", "LastBlockTime"}}, + {"TransactionDetail", reflect.TypeFor[TransactionDetail](), []string{"TxHash", "Nonce", "FromAddress", "ToAddress", "Value", + "BlockHeight", "Timestamp", "Success", "GasTokenID", "TransferTokenID", "IsFromRootChain"}}, + {"HelloCommand", reflect.TypeFor[HelloCommand](), []string{"Version", "NetworkID", "PeerID", "PeerIP", "PeerPort", + "ChainMaskList", "RootBlockHeader", "GenesisRootBlockHash"}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.typ.NumField() != len(tc.expected) { + t.Fatalf("field count mismatch: got %d, want %d", tc.typ.NumField(), len(tc.expected)) + } + for i, want := range tc.expected { + if tc.typ.Field(i).Name != want { + t.Errorf("field %d: got %s, want %s", i, tc.typ.Field(i).Name, want) + } + } + }) + } +} + +// ============================================================================= +// §6 Factory completeness +// ============================================================================= + +func TestNewClusterMessage_Completeness(t *testing.T) { + ops := []ClusterOp{ + ClusterOpPing, + ClusterOpPong, + ClusterOpConnectToSlavesRequest, + ClusterOpConnectToSlavesResponse, + ClusterOpAddRootBlockRequest, + ClusterOpAddRootBlockResponse, + ClusterOpGetEcoInfoListRequest, + ClusterOpGetEcoInfoListResponse, + ClusterOpGetNextBlockToMineRequest, + ClusterOpGetNextBlockToMineResponse, + ClusterOpGetUnconfirmedHeadersRequest, + ClusterOpGetUnconfirmedHeadersResponse, + ClusterOpGetAccountDataRequest, + ClusterOpGetAccountDataResponse, + ClusterOpAddTransactionRequest, + ClusterOpAddTransactionResponse, + ClusterOpAddMinorBlockHeaderRequest, + ClusterOpAddMinorBlockHeaderResponse, + ClusterOpAddXshardTxListRequest, + ClusterOpAddXshardTxListResponse, + ClusterOpSyncMinorBlockListRequest, + ClusterOpSyncMinorBlockListResponse, + ClusterOpAddMinorBlockRequest, + ClusterOpAddMinorBlockResponse, + ClusterOpCreateClusterPeerConnectionRequest, + ClusterOpCreateClusterPeerConnectionResponse, + ClusterOpDestroyClusterPeerConnectionCommand, + ClusterOpGetMinorBlockRequest, + ClusterOpGetMinorBlockResponse, + ClusterOpGetTransactionRequest, + ClusterOpGetTransactionResponse, + ClusterOpBatchAddXshardTxListRequest, + ClusterOpBatchAddXshardTxListResponse, + ClusterOpExecuteTransactionRequest, + ClusterOpExecuteTransactionResponse, + ClusterOpGetTransactionReceiptRequest, + ClusterOpGetTransactionReceiptResponse, + ClusterOpMineRequest, + ClusterOpMineResponse, + ClusterOpGenTxRequest, + ClusterOpGenTxResponse, + ClusterOpGetTransactionListByAddressRequest, + ClusterOpGetTransactionListByAddressResponse, + ClusterOpGetLogRequest, + ClusterOpGetLogResponse, + ClusterOpEstimateGasRequest, + ClusterOpEstimateGasResponse, + ClusterOpGetStorageRequest, + ClusterOpGetStorageResponse, + ClusterOpGetCodeRequest, + ClusterOpGetCodeResponse, + ClusterOpGasPriceRequest, + ClusterOpGasPriceResponse, + ClusterOpGetWorkRequest, + ClusterOpGetWorkResponse, + ClusterOpSubmitWorkRequest, + ClusterOpSubmitWorkResponse, + ClusterOpAddMinorBlockHeaderListRequest, + ClusterOpAddMinorBlockHeaderListResponse, + ClusterOpCheckMinorBlockRequest, + ClusterOpCheckMinorBlockResponse, + ClusterOpGetAllTransactionsRequest, + ClusterOpGetAllTransactionsResponse, + ClusterOpGetRootChainStakesRequest, + ClusterOpGetRootChainStakesResponse, + ClusterOpGetTotalBalanceRequest, + ClusterOpGetTotalBalanceResponse, + } + for _, op := range ops { + t.Run("ClusterOp(0x"+string("0123456789ABCDEF"[byte(op)>>4])+string("0123456789ABCDEF"[byte(op)&0x0F])+")", func(t *testing.T) { + msg, err := NewClusterMessage(op) + if err != nil { + t.Fatalf("NewClusterMessage(0x%x): %v", op, err) + } + if msg == nil { + t.Fatalf("NewClusterMessage(0x%x) returned nil", op) + } + typ := reflect.TypeOf(msg) + if typ.Kind() != reflect.Pointer || typ.Elem().Kind() != reflect.Struct { + t.Errorf("expected *struct, got %T", msg) + } + }) + } +} + +func TestNewCommandMessage_Completeness(t *testing.T) { + ops := []CommandOp{ + CommandOpHello, + CommandOpNewMinorBlockHeaderList, + CommandOpNewTransactionList, + CommandOpGetPeerListRequest, + CommandOpGetPeerListResponse, + CommandOpGetRootBlockHeaderListRequest, + CommandOpGetRootBlockHeaderListResponse, + CommandOpGetRootBlockListRequest, + CommandOpGetRootBlockListResponse, + CommandOpGetMinorBlockListRequest, + CommandOpGetMinorBlockListResponse, + CommandOpGetMinorBlockHeaderListRequest, + CommandOpGetMinorBlockHeaderListResponse, + CommandOpNewBlockMinor, + CommandOpPing, + CommandOpPong, + CommandOpGetRootBlockHeaderListWithSkipRequest, + CommandOpGetRootBlockHeaderListWithSkipResponse, + CommandOpNewRootBlock, + CommandOpGetMinorBlockHeaderListWithSkipRequest, + CommandOpGetMinorBlockHeaderListWithSkipResponse, + } + for _, op := range ops { + t.Run("", func(t *testing.T) { + msg, err := NewCommandMessage(op) + if err != nil { + t.Fatalf("NewCommandMessage(0x%x): %v", op, err) + } + if msg == nil { + t.Fatalf("NewCommandMessage(0x%x) returned nil", op) + } + }) + } +} + +func TestNewClusterMessage_UnknownOpcode(t *testing.T) { + _, err := NewClusterMessage(ClusterOp(0xFF)) + if err == nil { + t.Error("expected error for unknown ClusterOp") + } +} + +func TestNewCommandMessage_UnknownOpcode(t *testing.T) { + _, err := NewCommandMessage(CommandOp(0xEE)) + if err == nil { + t.Error("expected error for unknown CommandOp") + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +func makeAddress(fullShardID uint32, recipient byte) [AddressLength]byte { + var addr [AddressLength]byte + addr[16] = byte(fullShardID >> 24) + addr[17] = byte(fullShardID >> 16) + addr[18] = byte(fullShardID >> 8) + addr[19] = byte(fullShardID) + addr[0] = recipient + return addr +} + +func makeHash(seed byte) [HashLength]byte { + var h [HashLength]byte + for i := range h { + h[i] = seed + byte(i) + } + return h +} + +func makeUint128(seed uint64) [UInt128Length]byte { + var u [UInt128Length]byte + for i := range 8 { + u[i] = byte(seed >> (56 - 8*i)) + } + return u +} + +func hexEncode(b []byte) string { + const hexChars = "0123456789abcdef" + s := make([]byte, len(b)*2) + for i, v := range b { + s[i*2] = hexChars[v>>4] + s[i*2+1] = hexChars[v&0x0f] + } + return string(s) +} diff --git a/qkc/cluster/wire/protocol.go b/qkc/cluster/wire/protocol.go new file mode 100644 index 0000000000..5936a10ea9 --- /dev/null +++ b/qkc/cluster/wire/protocol.go @@ -0,0 +1,236 @@ +// Copyright 2026-2027, QuarkChain. + +// Package wire: opcode → message factory and protocol-level enumeration +// types. This file maps each ClusterOp / CommandOp to the concrete message +// struct that decodes a frame payload of that opcode. +package wire + +import ( + "fmt" +) + +// Direction matches the P2P sync Direction enum in +// quarkchain/cluster/p2p_commands.py. +// +// DIRECTIONS = [GENESIS, TIP] +// class Direction(IntEnum): +// GENESIS = 0 +// TIP = 1 +type Direction uint8 + +const ( + DirectionGenesis Direction = 0 + DirectionTip Direction = 1 +) + +func (d Direction) String() string { + switch d { + case DirectionGenesis: + return "GENESIS" + case DirectionTip: + return "TIP" + default: + return fmt.Sprintf("Direction(%d)", uint8(d)) + } +} + +// NewClusterMessage returns a new empty message struct pointer corresponding +// to the given ClusterOp. Used by slave/master_conn.go to allocate the +// concrete type before deserialising a frame payload. +func NewClusterMessage(op ClusterOp) (interface{}, error) { + switch op { + case ClusterOpPing: + return &PingRequest{}, nil + case ClusterOpPong: + return &PongResponse{}, nil + case ClusterOpConnectToSlavesRequest: + return &ConnectToSlavesRequest{}, nil + case ClusterOpConnectToSlavesResponse: + return &ConnectToSlavesResponse{}, nil + case ClusterOpAddRootBlockRequest: + return &AddRootBlockRequest{}, nil + case ClusterOpAddRootBlockResponse: + return &AddRootBlockResponse{}, nil + case ClusterOpGetEcoInfoListRequest: + return &GetEcoInfoListRequest{}, nil + case ClusterOpGetEcoInfoListResponse: + return &GetEcoInfoListResponse{}, nil + case ClusterOpGetNextBlockToMineRequest: + return &GetNextBlockToMineRequest{}, nil + case ClusterOpGetNextBlockToMineResponse: + return &GetNextBlockToMineResponse{}, nil + case ClusterOpGetUnconfirmedHeadersRequest: + return &GetUnconfirmedHeadersRequest{}, nil + case ClusterOpGetUnconfirmedHeadersResponse: + return &GetUnconfirmedHeadersResponse{}, nil + case ClusterOpGetAccountDataRequest: + return &GetAccountDataRequest{}, nil + case ClusterOpGetAccountDataResponse: + return &GetAccountDataResponse{}, nil + case ClusterOpAddTransactionRequest: + return &AddTransactionRequest{}, nil + case ClusterOpAddTransactionResponse: + return &AddTransactionResponse{}, nil + case ClusterOpAddMinorBlockHeaderRequest: + return &AddMinorBlockHeaderRequest{}, nil + case ClusterOpAddMinorBlockHeaderResponse: + return &AddMinorBlockHeaderResponse{}, nil + case ClusterOpAddXshardTxListRequest: + return &AddXshardTxListRequest{}, nil + case ClusterOpAddXshardTxListResponse: + return &AddXshardTxListResponse{}, nil + case ClusterOpSyncMinorBlockListRequest: + return &SyncMinorBlockListRequest{}, nil + case ClusterOpSyncMinorBlockListResponse: + return &SyncMinorBlockListResponse{}, nil + case ClusterOpAddMinorBlockRequest: + return &AddMinorBlockRequest{}, nil + case ClusterOpAddMinorBlockResponse: + return &AddMinorBlockResponse{}, nil + case ClusterOpCreateClusterPeerConnectionRequest: + return &CreateClusterPeerConnectionRequest{}, nil + case ClusterOpCreateClusterPeerConnectionResponse: + return &CreateClusterPeerConnectionResponse{}, nil + case ClusterOpDestroyClusterPeerConnectionCommand: + return &DestroyClusterPeerConnectionCommand{}, nil + case ClusterOpGetMinorBlockRequest: + return &GetMinorBlockRequest{}, nil + case ClusterOpGetMinorBlockResponse: + return &GetMinorBlockResponse{}, nil + case ClusterOpGetTransactionRequest: + return &GetTransactionRequest{}, nil + case ClusterOpGetTransactionResponse: + return &GetTransactionResponse{}, nil + case ClusterOpBatchAddXshardTxListRequest: + return &BatchAddXshardTxListRequest{}, nil + case ClusterOpBatchAddXshardTxListResponse: + return &BatchAddXshardTxListResponse{}, nil + case ClusterOpExecuteTransactionRequest: + return &ExecuteTransactionRequest{}, nil + case ClusterOpExecuteTransactionResponse: + return &ExecuteTransactionResponse{}, nil + case ClusterOpGetTransactionReceiptRequest: + return &GetTransactionReceiptRequest{}, nil + case ClusterOpGetTransactionReceiptResponse: + return &GetTransactionReceiptResponse{}, nil + case ClusterOpMineRequest: + return &MineRequest{}, nil + case ClusterOpMineResponse: + return &MineResponse{}, nil + case ClusterOpGenTxRequest: + return &GenTxRequest{}, nil + case ClusterOpGenTxResponse: + return &GenTxResponse{}, nil + case ClusterOpGetTransactionListByAddressRequest: + return &GetTransactionListByAddressRequest{}, nil + case ClusterOpGetTransactionListByAddressResponse: + return &GetTransactionListByAddressResponse{}, nil + case ClusterOpGetLogRequest: + return &GetLogRequest{}, nil + case ClusterOpGetLogResponse: + return &GetLogResponse{}, nil + case ClusterOpEstimateGasRequest: + return &EstimateGasRequest{}, nil + case ClusterOpEstimateGasResponse: + return &EstimateGasResponse{}, nil + case ClusterOpGetStorageRequest: + return &GetStorageRequest{}, nil + case ClusterOpGetStorageResponse: + return &GetStorageResponse{}, nil + case ClusterOpGetCodeRequest: + return &GetCodeRequest{}, nil + case ClusterOpGetCodeResponse: + return &GetCodeResponse{}, nil + case ClusterOpGasPriceRequest: + return &GasPriceRequest{}, nil + case ClusterOpGasPriceResponse: + return &GasPriceResponse{}, nil + case ClusterOpGetWorkRequest: + return &GetWorkRequest{}, nil + case ClusterOpGetWorkResponse: + return &GetWorkResponse{}, nil + case ClusterOpSubmitWorkRequest: + return &SubmitWorkRequest{}, nil + case ClusterOpSubmitWorkResponse: + return &SubmitWorkResponse{}, nil + case ClusterOpAddMinorBlockHeaderListRequest: + return &AddMinorBlockHeaderListRequest{}, nil + case ClusterOpAddMinorBlockHeaderListResponse: + return &AddMinorBlockHeaderListResponse{}, nil + case ClusterOpCheckMinorBlockRequest: + return &CheckMinorBlockRequest{}, nil + case ClusterOpCheckMinorBlockResponse: + return &CheckMinorBlockResponse{}, nil + case ClusterOpGetAllTransactionsRequest: + return &GetAllTransactionsRequest{}, nil + case ClusterOpGetAllTransactionsResponse: + return &GetAllTransactionsResponse{}, nil + case ClusterOpGetRootChainStakesRequest: + return &GetRootChainStakesRequest{}, nil + case ClusterOpGetRootChainStakesResponse: + return &GetRootChainStakesResponse{}, nil + case ClusterOpGetTotalBalanceRequest: + return &GetTotalBalanceRequest{}, nil + case ClusterOpGetTotalBalanceResponse: + return &GetTotalBalanceResponse{}, nil + default: + return nil, fmt.Errorf("unknown ClusterOp: 0x%x", op) + } +} + +// NewCommandMessage returns a new empty message struct pointer corresponding +// to the given CommandOp. Note: both PING and PONG share PingPongCommand — +// this matches the Python implementation where the same class is registered +// for both opcodes (see p2p_commands.py: REGISTER_OP_TO_SERIALIZER). +func NewCommandMessage(op CommandOp) (interface{}, error) { + switch op { + case CommandOpHello: + return &HelloCommand{}, nil + case CommandOpNewMinorBlockHeaderList: + return &NewMinorBlockHeaderListCommand{}, nil + case CommandOpNewTransactionList: + return &NewTransactionListCommand{}, nil + case CommandOpGetPeerListRequest: + return &GetPeerListRequest{}, nil + case CommandOpGetPeerListResponse: + return &GetPeerListResponse{}, nil + case CommandOpGetRootBlockHeaderListRequest: + return &GetRootBlockHeaderListRequest{}, nil + case CommandOpGetRootBlockHeaderListResponse: + return &GetRootBlockHeaderListResponse{}, nil + case CommandOpGetRootBlockListRequest: + return &GetRootBlockListRequest{}, nil + case CommandOpGetRootBlockListResponse: + return &GetRootBlockListResponse{}, nil + case CommandOpGetMinorBlockListRequest: + return &GetMinorBlockListRequest{}, nil + case CommandOpGetMinorBlockListResponse: + return &GetMinorBlockListResponse{}, nil + case CommandOpGetMinorBlockHeaderListRequest: + return &GetMinorBlockHeaderListRequest{}, nil + case CommandOpGetMinorBlockHeaderListResponse: + return &GetMinorBlockHeaderListResponse{}, nil + case CommandOpNewBlockMinor: + return &NewBlockMinorCommand{}, nil + case CommandOpPing, CommandOpPong: + return &PingPongCommand{}, nil + case CommandOpGetRootBlockHeaderListWithSkipRequest: + return &GetRootBlockHeaderListWithSkipRequest{}, nil + case CommandOpGetRootBlockHeaderListWithSkipResponse: + // Shares GetRootBlockHeaderListResponse with CommandOpGetRootBlockHeaderListResponse (0x06). + // Python's REGISTER_OP_TO_SERIALIZER also maps both opcodes (0x06, 0x11) to the same class. + // Verified: p2p_commands.py line 357. + return &GetRootBlockHeaderListResponse{}, nil + case CommandOpNewRootBlock: + return &NewRootBlockCommand{}, nil + case CommandOpGetMinorBlockHeaderListWithSkipRequest: + return &GetMinorBlockHeaderListWithSkipRequest{}, nil + case CommandOpGetMinorBlockHeaderListWithSkipResponse: + // Shares GetMinorBlockHeaderListResponse with CommandOpGetMinorBlockHeaderListResponse (0x0C). + // Python's REGISTER_OP_TO_SERIALIZER also maps both opcodes (0x0C, 0x14) to the same class. + // Verified: p2p_commands.py line 360. + return &GetMinorBlockHeaderListResponse{}, nil + default: + return nil, fmt.Errorf("unknown CommandOp: 0x%x", op) + } +} diff --git a/qkc/cluster/wire/rawbytes_placeholder.go b/qkc/cluster/wire/rawbytes_placeholder.go new file mode 100644 index 0000000000..5d263d8773 --- /dev/null +++ b/qkc/cluster/wire/rawbytes_placeholder.go @@ -0,0 +1,57 @@ +// Copyright 2026-2027, QuarkChain. + +// ============================================================================= +// TEMPORARY PLACEHOLDER FILE — DELETE after real types merge +// ============================================================================= +// +// RawBytes is a placeholder used during pyquarkchain → Go migration. +// Delete this file once real types (RootBlock, MinorBlockHeader, etc.) are ported. +// Replace `*RawBytes` fields in messages.go with real typed pointers. +// +// DO NOT REVIEW AS PRODUCTION CODE. +package wire + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/qkc/serialize" +) + +// RawBytes is a transparent byte passthrough placeholder for unported complex types. +type RawBytes []byte + +func (r RawBytes) Serialize(w *[]byte) error { + const maxRawBytesSize = 100 * 1024 * 1024 // 100 MB + if len(r) > maxRawBytesSize { + return fmt.Errorf("RawBytes.Serialize: size %d exceeds max %d", len(r), maxRawBytesSize) + } + + *w = append(*w, r...) + return nil +} + +// Deserialize consumes all remaining bytes from the buffer. +// +// SAFETY: This is only safe when RawBytes is the LAST field in its parent +// struct. If RawBytes appears before other fields, consuming the remaining +// bytes will corrupt subsequent fields. Structs with non-last RawBytes fields +// are marked with a WARNING in messages.go and cannot be correctly deserialized +// until the real Go type is ported. +// +// TEMPORARY: Delete this placeholder once real types (RootBlock, MinorBlockHeader) are ported. +func (r *RawBytes) Deserialize(bb *serialize.ByteBuffer) error { + const maxRawBytesSize = 100 * 1024 * 1024 // 100 MB, matches Serialize + if bb.Remaining() > maxRawBytesSize { + return fmt.Errorf("RawBytes.Deserialize: size %d exceeds max %d", bb.Remaining(), maxRawBytesSize) + } + bytes, err := bb.ReadRemaining() + if err != nil { + return err + } + *r = RawBytes(bytes) + return nil +} + +// Compile-time check: RawBytes implements Serializable (required by serialize package). +// This ensures serialize.Serialize(&buf, &struct{Field *RawBytes}) works. +var _ serialize.Serializable = (*RawBytes)(nil) diff --git a/qkc/cluster/wire/types.go b/qkc/cluster/wire/types.go new file mode 100644 index 0000000000..6c4ff0ccb5 --- /dev/null +++ b/qkc/cluster/wire/types.go @@ -0,0 +1,90 @@ +// Copyright 2026-2027, QuarkChain. + +package wire + +import ( + "encoding/binary" + "fmt" + "math" + + "github.com/ethereum/go-ethereum/qkc/serialize" +) + +// ============================================================================= +// Custom wire types for Python format compatibility +// ============================================================================= +// +// Python uses 4-byte length prefix for nested slices in some wire messages. +// Go's serialize framework defaults to 1-byte prefix for nested elements. +// These custom types enforce 4-byte prefix to match Python wire format. + +// PrependedSizeBytes4 is []byte with 4-byte length prefix (matches Python PrependedSizeBytesSerializer(4)). +type PrependedSizeBytes4 []byte + +func (p PrependedSizeBytes4) Serialize(w *[]byte) error { + lenBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lenBuf, uint32(len(p))) + *w = append(*w, lenBuf...) + *w = append(*w, p...) + return nil +} + +func (p *PrependedSizeBytes4) Deserialize(bb *serialize.ByteBuffer) error { + length, err := bb.GetUInt32() + if err != nil { + return err + } + + if length > math.MaxInt32 || int(length) > bb.Remaining() { + return fmt.Errorf("PrependedSizeBytes4.Deserialize: length %d exceeds remaining %d", length, bb.Remaining()) + } + + bytes, err := bb.ReadBytes(int(length)) + if err != nil { + return err + } + + *p = PrependedSizeBytes4(bytes) + return nil +} + +var _ serialize.Serializable = (*PrependedSizeBytes4)(nil) + +// PrependedSizeHashList4 is [][HashLength]byte with 4-byte length prefix (matches Python PrependedSizeListSerializer(4, hash256)). +type PrependedSizeHashList4 [][HashLength]byte + +func (p PrependedSizeHashList4) Serialize(w *[]byte) error { + lenBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lenBuf, uint32(len(p))) + *w = append(*w, lenBuf...) + + for _, hash := range p { + *w = append(*w, hash[:]...) + } + return nil +} + +func (p *PrependedSizeHashList4) Deserialize(bb *serialize.ByteBuffer) error { + length, err := bb.GetUInt32() + if err != nil { + return err + } + + if length > math.MaxInt32/HashLength || int(length)*HashLength > bb.Remaining() { + return fmt.Errorf("PrependedSizeHashList4.Deserialize: length %d exceeds capacity", length) + } + + list := make([][HashLength]byte, length) + for i := 0; i < int(length); i++ { + hashBytes, err := bb.ReadBytes(HashLength) + if err != nil { + return err + } + list[i] = [HashLength]byte(hashBytes) + } + + *p = PrependedSizeHashList4(list) + return nil +} + +var _ serialize.Serializable = (*PrependedSizeHashList4)(nil)