fix(p2p): correct network IDs and serialisation for C++ daemon compat
Three bugs found by integration testing against the C++ testnet daemon: 1. NetworkIDMainnet/Testnet had byte 10 swapped — the C++ #ifndef TESTNET branch (mainnet) sets P2P_NETWORK_ID_TESTNET_FLAG=1, and the #else (testnet) sets it to 0. Counter-intuitive but matches compiled binaries. 2. ClientVersion format "Lethean/go-blockchain 0.1.0" was rejected by the daemon's parse_client_version which expects "major.minor.rev.build[commit]". Changed to "6.0.1.2[go-blockchain]". 3. RequestChain, RequestGetObjects, and ResponseGetObjects used StringArrayVal for hash fields, but the C++ daemon uses KV_SERIALIZE_CONTAINER_POD_AS_BLOB which packs all 32-byte hashes into a single concatenated blob. Also, ResponseChainEntry.m_block_ids is an object array of block_context_info (hash + cumulative size), not a simple hash list. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
d588f8f8d0
commit
fcb867d53b
5 changed files with 121 additions and 31 deletions
|
|
@ -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]"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
74
p2p/relay.go
74
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue