mirror of
https://github.com/luxfi/api.git
synced 2026-07-26 21:22:38 +00:00
fix(zap): length-tolerant InitializeResponse decode (evolvable VM wire)
InitializeResponse.Decode now reads the trailing Capabilities field only when >=8 bytes remain, so a newer node decoding a pre-v1.0.16 plugin's short payload yields Capabilities=0 instead of the unexpected-EOF that broke every EVM Initialize in the v1.36.11 skew (Capabilities appended at protocol 42 without a bump). Appending a future trailing field can no longer EOF-break an older peer. New test locks the tolerance. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -144,7 +144,23 @@ func (m *InitializeResponse) Encode(buf *Buffer) {
|
||||
buf.WriteUint64(m.Capabilities)
|
||||
}
|
||||
|
||||
// Decode deserializes InitializeResponse from the reader
|
||||
// Decode deserializes InitializeResponse from the reader.
|
||||
//
|
||||
// LENGTH-TOLERANT trailing field. Capabilities is OPTIONAL on the wire: it was
|
||||
// APPENDED in api v1.0.16 (the Quasar-export handshake) WITHOUT bumping
|
||||
// version.RPCChainVMProtocol, so a peer built before it — a stale VM plugin —
|
||||
// sends a payload that ends right after Timestamp. Guarding the read on
|
||||
// Remaining() lets a newer decoder read that short payload as "Capabilities = 0"
|
||||
// (Nova-only, no optional add-ons) instead of failing with io.ErrUnexpectedEOF —
|
||||
// exactly the v1.36.11 skew that broke every EVM Initialize with
|
||||
// "zap decode initialize response: unexpected EOF". The mirror case (an OLDER
|
||||
// decoder reading a NEWER payload) already works: a length-prefixed ZAP frame
|
||||
// lets the old decoder stop after Timestamp and ignore the trailing bytes.
|
||||
//
|
||||
// EVOLUTION RULE: any FUTURE field is appended AFTER Capabilities and read the
|
||||
// same way — guard each read on Remaining(), and keep Encode writing fields in
|
||||
// this exact order. Never insert or reorder a field in the middle: that is a
|
||||
// breaking wire change and MUST bump version.RPCChainVMProtocol.
|
||||
func (m *InitializeResponse) Decode(r *Reader) error {
|
||||
var err error
|
||||
if m.LastAcceptedID, err = r.ReadBytes(); err != nil {
|
||||
@@ -162,8 +178,16 @@ func (m *InitializeResponse) Decode(r *Reader) error {
|
||||
if m.Timestamp, err = r.ReadInt64(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Capabilities, err = r.ReadUint64()
|
||||
return err
|
||||
// OPTIONAL trailing field: absent from pre-v1.0.16 peers. Remaining() < 8
|
||||
// (a uint64 is 8 bytes) ⇒ leave Capabilities at its zero value rather than
|
||||
// EOF. A count of exactly 8 (or more, from an even-newer peer with further
|
||||
// appended fields) ⇒ read it and ignore anything past it.
|
||||
if r.Remaining() >= 8 {
|
||||
if m.Capabilities, err = r.ReadUint64(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetStateRequest contains state change request.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package zap
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestInitializeResponseDecodeLengthTolerant proves the OPTIONAL trailing
|
||||
// Capabilities field can be absent on the wire without breaking Decode — the
|
||||
// forward/backward-compatible property that fixes the v1.36.11 "unexpected EOF"
|
||||
// skew, where a node built with the Capabilities field decoded an InitializeResponse
|
||||
// from a stale plugin that never encoded it.
|
||||
func TestInitializeResponseDecodeLengthTolerant(t *testing.T) {
|
||||
lastID := []byte("last-accepted-id-32-bytes-000000")
|
||||
parentID := []byte("parent-accepted-id-32-bytes-0000")
|
||||
const height = uint64(12345)
|
||||
blockBytes := []byte("block-bytes")
|
||||
const ts = int64(1_700_000_000_000_000_000)
|
||||
|
||||
// PRE-v1.0.16 payload: the five original fields only, exactly as a stale
|
||||
// plugin (built before Capabilities existed) encodes it.
|
||||
old := GetBuffer()
|
||||
old.WriteBytes(lastID)
|
||||
old.WriteBytes(parentID)
|
||||
old.WriteUint64(height)
|
||||
old.WriteBytes(blockBytes)
|
||||
old.WriteInt64(ts)
|
||||
oldPayload := append([]byte(nil), old.Bytes()...)
|
||||
PutBuffer(old)
|
||||
|
||||
var got InitializeResponse
|
||||
if err := got.Decode(NewReader(oldPayload)); err != nil {
|
||||
t.Fatalf("decode of pre-Capabilities payload must not error, got: %v", err)
|
||||
}
|
||||
if got.Capabilities != 0 {
|
||||
t.Fatalf("absent Capabilities must default to 0, got %d", got.Capabilities)
|
||||
}
|
||||
if string(got.LastAcceptedID) != string(lastID) ||
|
||||
string(got.LastAcceptedParentID) != string(parentID) ||
|
||||
got.Height != height ||
|
||||
string(got.Bytes) != string(blockBytes) ||
|
||||
got.Timestamp != ts {
|
||||
t.Fatalf("non-optional fields corrupted by tolerant decode: %+v", got)
|
||||
}
|
||||
|
||||
// FULL round-trip: Capabilities present survives Encode -> Decode.
|
||||
want := InitializeResponse{
|
||||
LastAcceptedID: lastID,
|
||||
LastAcceptedParentID: parentID,
|
||||
Height: height,
|
||||
Bytes: blockBytes,
|
||||
Timestamp: ts,
|
||||
Capabilities: CapQuasarExport,
|
||||
}
|
||||
buf := GetBuffer()
|
||||
want.Encode(buf)
|
||||
full := append([]byte(nil), buf.Bytes()...)
|
||||
PutBuffer(buf)
|
||||
|
||||
var rt InitializeResponse
|
||||
if err := rt.Decode(NewReader(full)); err != nil {
|
||||
t.Fatalf("round-trip decode: %v", err)
|
||||
}
|
||||
if rt.Capabilities != CapQuasarExport {
|
||||
t.Fatalf("Capabilities round-trip: got %d want %d", rt.Capabilities, CapQuasarExport)
|
||||
}
|
||||
|
||||
// FORWARD-compat: an even-newer peer appends further trailing bytes; Decode
|
||||
// reads Capabilities and ignores the rest without erroring.
|
||||
fwd := append(append([]byte(nil), full...), 0xDE, 0xAD, 0xBE, 0xEF)
|
||||
var fr InitializeResponse
|
||||
if err := fr.Decode(NewReader(fwd)); err != nil {
|
||||
t.Fatalf("forward-compat decode must ignore trailing bytes, got: %v", err)
|
||||
}
|
||||
if fr.Capabilities != CapQuasarExport {
|
||||
t.Fatalf("forward-compat Capabilities: got %d want %d", fr.Capabilities, CapQuasarExport)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user