diff --git a/config/config.go b/config/config.go index e7bd041..73b81a2 100644 --- a/config/config.go +++ b/config/config.go @@ -276,22 +276,29 @@ const ( ) // NetworkIDMainnet is the 16-byte network UUID for mainnet P2P handshake. -// From net_node.inl: bytes 0-9 are fixed, byte 10 = testnet flag (0), +// From net_node.inl: bytes 0-9 are fixed, byte 10 = P2P_NETWORK_ID_TESTNET_FLAG, // bytes 11-14 fixed, byte 15 = formation version (84 = 0x54). +// NOTE: In the C++ source, the #ifndef TESTNET branch (i.e. mainnet) sets +// P2P_NETWORK_ID_TESTNET_FLAG = 1 and the #else (testnet) sets it to 0. +// The naming is counter-intuitive but matches the compiled binaries. var NetworkIDMainnet = [16]byte{ 0x11, 0x10, 0x01, 0x11, 0x01, 0x01, 0x11, 0x01, - 0x10, 0x11, 0x00, 0x11, 0x01, 0x11, 0x21, 0x54, + 0x10, 0x11, 0x01, 0x11, 0x01, 0x11, 0x21, 0x54, } // NetworkIDTestnet is the 16-byte network UUID for testnet P2P handshake. -// Byte 10 = testnet flag (1), byte 15 = formation version (100 = 0x64). +// Byte 10 = 0x00 (P2P_NETWORK_ID_TESTNET_FLAG in testnet build), +// byte 15 = formation version (100 = 0x64). var NetworkIDTestnet = [16]byte{ 0x11, 0x10, 0x01, 0x11, 0x01, 0x01, 0x11, 0x01, - 0x10, 0x11, 0x01, 0x11, 0x01, 0x11, 0x21, 0x64, + 0x10, 0x11, 0x00, 0x11, 0x01, 0x11, 0x21, 0x64, } // ClientVersion is the version string sent in CORE_SYNC_DATA. -const ClientVersion = "Lethean/go-blockchain 0.1.0" +// The C++ daemon parses this as "major.minor.revision.build[commit]" +// and rejects connections where it cannot parse or the build number +// is below the minimum for the current hard-fork era. +const ClientVersion = "6.0.1.2[go-blockchain]" // --------------------------------------------------------------------------- diff --git a/config/config_test.go b/config/config_test.go index 68cbb4e..2b63120 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -168,20 +168,28 @@ func TestTransactionVersionConstants_Good(t *testing.T) { } func TestNetworkID_Good(t *testing.T) { - // Mainnet: byte 10 = 0 (not testnet), byte 15 = 84 (0x54) - if NetworkIDMainnet[10] != 0x00 { - t.Errorf("mainnet testnet flag: got %x, want 0x00", NetworkIDMainnet[10]) + // In the C++ source, #ifndef TESTNET (mainnet) sets + // P2P_NETWORK_ID_TESTNET_FLAG=1 and #else (testnet) sets it to 0. + // The naming is counter-intuitive but matches the compiled binaries. + + // Mainnet: byte 10 = 1 (flag from #ifndef branch), byte 15 = 84 (0x54) + if NetworkIDMainnet[10] != 0x01 { + t.Errorf("mainnet flag: got %x, want 0x01", NetworkIDMainnet[10]) } if NetworkIDMainnet[15] != 0x54 { t.Errorf("mainnet version: got %x, want 0x54", NetworkIDMainnet[15]) } - // Testnet: byte 10 = 1, byte 15 = 100 (0x64) - if NetworkIDTestnet[10] != 0x01 { - t.Errorf("testnet testnet flag: got %x, want 0x01", NetworkIDTestnet[10]) + // Testnet: byte 10 = 0 (flag from #else branch), byte 15 = 100 (0x64) + if NetworkIDTestnet[10] != 0x00 { + t.Errorf("testnet flag: got %x, want 0x00", NetworkIDTestnet[10]) } if NetworkIDTestnet[15] != 0x64 { t.Errorf("testnet version: got %x, want 0x64", NetworkIDTestnet[15]) } + // The two IDs must differ. + if NetworkIDMainnet == NetworkIDTestnet { + t.Error("mainnet and testnet IDs must differ") + } // ChainConfig should have them if Mainnet.NetworkID != NetworkIDMainnet { t.Error("Mainnet.NetworkID mismatch") diff --git a/p2p/handshake_test.go b/p2p/handshake_test.go index 86ed9ab..12c94f1 100644 --- a/p2p/handshake_test.go +++ b/p2p/handshake_test.go @@ -137,9 +137,10 @@ func TestNodeData_Good_NetworkIDBlob(t *testing.T) { if len(blob) != 16 { t.Fatalf("network_id blob: got %d bytes, want 16", len(blob)) } - // Byte 10 = testnet flag = 1 - if blob[10] != 0x01 { - t.Errorf("testnet flag: got %x, want 0x01", blob[10]) + // Byte 10 = P2P_NETWORK_ID_TESTNET_FLAG. In the C++ source, the + // #else (testnet) branch sets this to 0 (counter-intuitive naming). + if blob[10] != 0x00 { + t.Errorf("testnet flag: got %x, want 0x00", blob[10]) } // Byte 15 = version = 0x64 (100) if blob[15] != 0x64 { diff --git a/p2p/relay.go b/p2p/relay.go index f9eb787..ed436ca 100644 --- a/p2p/relay.go +++ b/p2p/relay.go @@ -86,12 +86,22 @@ type RequestGetObjects struct { } // Encode serialises the request. +// The C++ daemon uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB for both blocks +// and txs, so we pack all hashes into single concatenated blobs. func (r *RequestGetObjects) Encode() ([]byte, error) { + blocksBlob := make([]byte, 0, len(r.Blocks)*32) + for _, id := range r.Blocks { + blocksBlob = append(blocksBlob, id...) + } s := levin.Section{ - "blocks": levin.StringArrayVal(r.Blocks), + "blocks": levin.StringVal(blocksBlob), } if len(r.Txs) > 0 { - s["txs"] = levin.StringArrayVal(r.Txs) + txsBlob := make([]byte, 0, len(r.Txs)*32) + for _, id := range r.Txs { + txsBlob = append(txsBlob, id...) + } + s["txs"] = levin.StringVal(txsBlob) } return levin.EncodeStorage(s) } @@ -103,10 +113,12 @@ func (r *RequestGetObjects) Decode(data []byte) error { return err } if v, ok := s["blocks"]; ok { - r.Blocks, _ = v.AsStringArray() + blob, _ := v.AsString() + r.Blocks = splitHashes(blob, 32) } if v, ok := s["txs"]; ok { - r.Txs, _ = v.AsStringArray() + blob, _ := v.AsString() + r.Txs = splitHashes(blob, 32) } return nil } @@ -132,7 +144,12 @@ func (r *ResponseGetObjects) Encode() ([]byte, error) { "current_blockchain_height": levin.Uint64Val(r.CurrentHeight), } if len(r.MissedIDs) > 0 { - s["missed_ids"] = levin.StringArrayVal(r.MissedIDs) + // missed_ids uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB in C++. + blob := make([]byte, 0, len(r.MissedIDs)*32) + for _, id := range r.MissedIDs { + blob = append(blob, id...) + } + s["missed_ids"] = levin.StringVal(blob) } return levin.EncodeStorage(s) } @@ -159,7 +176,9 @@ func (r *ResponseGetObjects) Decode(data []byte) error { } } if v, ok := s["missed_ids"]; ok { - r.MissedIDs, _ = v.AsStringArray() + // missed_ids uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB in C++. + blob, _ := v.AsString() + r.MissedIDs = splitHashes(blob, 32) } return nil } @@ -170,21 +189,37 @@ type RequestChain struct { } // Encode serialises the request. +// The C++ daemon uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB for block_ids, +// so we pack all hashes into a single concatenated blob. func (r *RequestChain) Encode() ([]byte, error) { + blob := make([]byte, 0, len(r.BlockIDs)*32) + for _, id := range r.BlockIDs { + blob = append(blob, id...) + } s := levin.Section{ - "block_ids": levin.StringArrayVal(r.BlockIDs), + "block_ids": levin.StringVal(blob), } return levin.EncodeStorage(s) } +// BlockContextInfo holds a block hash and cumulative size from a chain +// entry response. Mirrors the C++ block_context_info struct. +type BlockContextInfo struct { + Hash []byte // 32-byte block hash (KV_SERIALIZE_VAL_POD_AS_BLOB) + CumulSize uint64 // Cumulative block size +} + // ResponseChainEntry is NOTIFY_RESPONSE_CHAIN_ENTRY (2007). type ResponseChainEntry struct { StartHeight uint64 TotalHeight uint64 - BlockIDs [][]byte + BlockIDs [][]byte // Convenience: just the hashes + Blocks []BlockContextInfo // Full entries with cumulative sizes } // Decode parses a chain entry response. +// m_block_ids is an object array of block_context_info, each with +// "h" (hash blob) and "cumul_size" (uint64). func (r *ResponseChainEntry) Decode(data []byte) error { s, err := levin.DecodeStorage(data) if err != nil { @@ -197,7 +232,28 @@ func (r *ResponseChainEntry) Decode(data []byte) error { r.TotalHeight, _ = v.AsUint64() } if v, ok := s["m_block_ids"]; ok { - r.BlockIDs, _ = v.AsStringArray() + sections, _ := v.AsSectionArray() + r.Blocks = make([]BlockContextInfo, len(sections)) + r.BlockIDs = make([][]byte, len(sections)) + for i, sec := range sections { + if hv, ok := sec["h"]; ok { + r.Blocks[i].Hash, _ = hv.AsString() + r.BlockIDs[i] = r.Blocks[i].Hash + } + if cv, ok := sec["cumul_size"]; ok { + r.Blocks[i].CumulSize, _ = cv.AsUint64() + } + } } return nil } + +// splitHashes divides a concatenated blob into fixed-size hash slices. +func splitHashes(blob []byte, size int) [][]byte { + n := len(blob) / size + out := make([][]byte, n) + for i := 0; i < n; i++ { + out[i] = blob[i*size : (i+1)*size] + } + return out +} diff --git a/p2p/relay_test.go b/p2p/relay_test.go index 8610ca2..1b43475 100644 --- a/p2p/relay_test.go +++ b/p2p/relay_test.go @@ -66,18 +66,19 @@ func TestRequestChain_Good_Roundtrip(t *testing.T) { t.Fatalf("encode: %v", err) } - // Decode back via storage + // Decode back via storage — block_ids is a single concatenated blob + // (KV_SERIALIZE_CONTAINER_POD_AS_BLOB in C++). s, err := levin.DecodeStorage(data) if err != nil { t.Fatalf("decode storage: %v", err) } if v, ok := s["block_ids"]; ok { - ids, err := v.AsStringArray() + blob, err := v.AsString() if err != nil { - t.Fatalf("AsStringArray: %v", err) + t.Fatalf("AsString: %v", err) } - if len(ids) != 1 || ids[0][0] != 0xFF { - t.Errorf("block_ids roundtrip failed") + if len(blob) != 32 || blob[0] != 0xFF { + t.Errorf("block_ids roundtrip failed: len=%d, first=%x", len(blob), blob[0]) } } else { t.Error("block_ids not found in decoded storage") @@ -87,10 +88,16 @@ func TestRequestChain_Good_Roundtrip(t *testing.T) { func TestResponseChainEntry_Good_Decode(t *testing.T) { hash := make([]byte, 32) hash[31] = 0xAB + // m_block_ids is an object array of block_context_info, + // each with "h" (hash blob) and "cumul_size" (uint64). + entry := levin.Section{ + "h": levin.StringVal(hash), + "cumul_size": levin.Uint64Val(1234), + } s := levin.Section{ "start_height": levin.Uint64Val(100), "total_height": levin.Uint64Val(6300), - "m_block_ids": levin.StringArrayVal([][]byte{hash}), + "m_block_ids": levin.ObjectArrayVal([]levin.Section{entry}), } data, err := levin.EncodeStorage(s) if err != nil { @@ -113,6 +120,12 @@ func TestResponseChainEntry_Good_Decode(t *testing.T) { if resp.BlockIDs[0][31] != 0xAB { t.Errorf("block_ids[0][31]: got %x, want AB", resp.BlockIDs[0][31]) } + if len(resp.Blocks) != 1 { + t.Fatalf("blocks: got %d, want 1", len(resp.Blocks)) + } + if resp.Blocks[0].CumulSize != 1234 { + t.Errorf("cumul_size: got %d, want 1234", resp.Blocks[0].CumulSize) + } } func TestRequestGetObjects_RoundTrip(t *testing.T) { @@ -144,9 +157,13 @@ func TestRequestGetObjects_RoundTrip(t *testing.T) { } func TestRequestGetObjects_WithTxs(t *testing.T) { + txHash := make([]byte, 32) + txHash[0] = 0xAA + txHash[1] = 0xBB + txHash[2] = 0xCC req := RequestGetObjects{ Blocks: [][]byte{make([]byte, 32)}, - Txs: [][]byte{{0xAA, 0xBB, 0xCC}}, + Txs: [][]byte{txHash}, } data, err := req.Encode() if err != nil { @@ -197,9 +214,10 @@ func TestResponseGetObjects_Decode(t *testing.T) { } missedHash := make([]byte, 32) missedHash[0] = 0xFF + // missed_ids uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB in C++. s := levin.Section{ "blocks": levin.ObjectArrayVal([]levin.Section{blockEntry1, blockEntry2}), - "missed_ids": levin.StringArrayVal([][]byte{missedHash}), + "missed_ids": levin.StringVal(missedHash), "current_blockchain_height": levin.Uint64Val(6300), } data, err := levin.EncodeStorage(s)