graphvm: migrate block wire json→native ZAP (G-Chain codec-free)

The G-Chain block Bytes()/ID path used json.Marshal(blockWire{...}); now a
fixed-offset ZAP object (ParentID 32B, Height u64, Timestamp i64, Payload bytes).
blockID = hash(canonical ZAP wire), deterministic across nodes/restarts, trailing
bytes rejected. Dropped encoding/json from block.go. Round-trip + determinism +
trailing-byte tests green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-14 15:39:27 -07:00
co-authored by Hanzo Dev
parent d47e04b4cb
commit 02c90d5c88
2 changed files with 127 additions and 22 deletions
+53 -22
View File
@@ -5,7 +5,6 @@ package graphvm
import (
"context"
"encoding/json"
"errors"
"time"
@@ -13,6 +12,7 @@ import (
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// The consensus engine requires every ChainVM to resolve a real last-accepted
@@ -210,14 +210,53 @@ func (b *Block) Bytes() []byte {
// genesis block ID diverge across validators and break consensus agreement.
var genesisTimestamp = time.Unix(0, 0).UTC()
// blockWire is the deterministic on-wire encoding of a G-Chain block. The block
// ID is hash.ComputeHash256 of these bytes, so marshal/parse round-trips a
// G-Chain block wire — native ZAP (fixed offsets), no reflection codec. The
// block ID is hash.ComputeHash256 of these bytes, so marshal/parse round-trips a
// byte-identical ID across nodes and restarts.
type blockWire struct {
ParentID ids.ID `json:"parentID"`
Height uint64 `json:"height"`
Timestamp int64 `json:"timestamp"`
Payload []byte `json:"payload,omitempty"`
//
// ParentID 32B @ 0
// Height u64 @ 32
// Timestamp i64 @ 40
// Payload bytes @ 48
const (
gblkParentID = 0
gblkHeight = 32
gblkTimestamp = 40
gblkPayload = 48
gblkSize = 56
)
var errGBlockTrailing = errors.New("graphvm block: trailing bytes after canonical wire")
// marshalGBlock encodes a block into its canonical ZAP wire bytes.
func marshalGBlock(parentID ids.ID, height uint64, timestamp int64, payload []byte) []byte {
b := zap.NewBuilder(zap.HeaderSize + gblkSize + len(payload) + 16)
ob := b.StartObject(gblkSize)
ob.SetBytesFixed(gblkParentID, parentID[:])
ob.SetUint64(gblkHeight, height)
ob.SetInt64(gblkTimestamp, timestamp)
ob.SetBytes(gblkPayload, payload)
ob.FinishAsRoot()
return b.Finish()
}
// parseGBlock decodes canonical ZAP block wire; rejects trailing bytes.
func parseGBlock(raw []byte) (parentID ids.ID, height uint64, timestamp int64, payload []byte, err error) {
msg, perr := zap.Parse(raw)
if perr != nil {
err = perr
return
}
if msg.Size() != len(raw) {
err = errGBlockTrailing
return
}
o := msg.Root()
parentID = ids.ID(o.BytesFixedSlice(gblkParentID, 32))
height = o.Uint64(gblkHeight)
timestamp = o.Int64(gblkTimestamp)
payload = o.Bytes(gblkPayload)
return
}
// newGenesisBlock builds the G-Chain genesis block (height 0) deterministically
@@ -231,15 +270,7 @@ func newGenesisBlock(vm *VM, genesisBytes []byte) (*Block, error) {
// newBlock constructs a block, computes its canonical bytes and content-
// addressed ID, and returns it ready to serve.
func newBlock(vm *VM, parentID ids.ID, height uint64, timestamp time.Time, payload []byte) (*Block, error) {
raw, err := json.Marshal(blockWire{
ParentID: parentID,
Height: height,
Timestamp: timestamp.Unix(),
Payload: payload,
})
if err != nil {
return nil, err
}
raw := marshalGBlock(parentID, height, timestamp.Unix(), payload)
return &Block{
vm: vm,
id: ids.ID(hash.ComputeHash256(raw)),
@@ -254,16 +285,16 @@ func newBlock(vm *VM, parentID ids.ID, height uint64, timestamp time.Time, paylo
// parseBlock decodes the canonical wire bytes produced by newBlock back into a
// Block whose ID is recomputed from those exact bytes.
func parseBlock(vm *VM, raw []byte) (*Block, error) {
var wire blockWire
if err := json.Unmarshal(raw, &wire); err != nil {
parentID, height, timestamp, _, err := parseGBlock(raw)
if err != nil {
return nil, err
}
return &Block{
vm: vm,
id: ids.ID(hash.ComputeHash256(raw)),
parentID: wire.ParentID,
height: wire.Height,
timestamp: time.Unix(wire.Timestamp, 0).UTC(),
parentID: parentID,
height: height,
timestamp: time.Unix(timestamp, 0).UTC(),
status: choices.Accepted,
bytes: raw,
}, nil
+74
View File
@@ -0,0 +1,74 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package graphvm
import (
"bytes"
"testing"
"github.com/luxfi/ids"
)
// TestGBlockWireRoundTrip proves the native-ZAP block wire round-trips every
// field byte-for-byte (parentID, height, timestamp, payload).
func TestGBlockWireRoundTrip(t *testing.T) {
parentID := ids.ID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
const height = uint64(0xDEADBEEF)
const ts = int64(1_700_000_000)
payload := []byte("graph-block-payload")
raw := marshalGBlock(parentID, height, ts, payload)
gotParent, gotHeight, gotTS, gotPayload, err := parseGBlock(raw)
if err != nil {
t.Fatalf("parseGBlock: %v", err)
}
if gotParent != parentID {
t.Errorf("parentID: got %x, want %x", gotParent, parentID)
}
if gotHeight != height {
t.Errorf("height: got %d, want %d", gotHeight, height)
}
if gotTS != ts {
t.Errorf("timestamp: got %d, want %d", gotTS, ts)
}
if !bytes.Equal(gotPayload, payload) {
t.Errorf("payload: got %q, want %q", gotPayload, payload)
}
// empty payload (genesis-like) round-trips too.
raw0 := marshalGBlock(ids.Empty, 0, 0, nil)
p0, h0, t0, pay0, err := parseGBlock(raw0)
if err != nil {
t.Fatalf("parseGBlock(empty): %v", err)
}
if p0 != ids.Empty || h0 != 0 || t0 != 0 || len(pay0) != 0 {
t.Errorf("empty block mismatch: %x %d %d %q", p0, h0, t0, pay0)
}
}
// TestGBlockWireDeterminism proves the same logical block always serializes to
// identical bytes (so blockID = hash(wire) is stable across nodes/restarts).
func TestGBlockWireDeterminism(t *testing.T) {
parentID := ids.ID{0xAB}
a := marshalGBlock(parentID, 42, 99, []byte("x"))
b := marshalGBlock(parentID, 42, 99, []byte("x"))
if !bytes.Equal(a, b) {
t.Fatalf("non-deterministic block wire:\n a=%x\n b=%x", a, b)
}
// a field change must change the bytes.
c := marshalGBlock(parentID, 43, 99, []byte("x"))
if bytes.Equal(a, c) {
t.Fatal("height change did not change wire bytes")
}
}
// TestGBlockWireTrailingRejected proves canonical framing: a trailing byte is
// refused.
func TestGBlockWireTrailingRejected(t *testing.T) {
raw := marshalGBlock(ids.ID{0x01}, 1, 1, []byte("y"))
tampered := append(append([]byte(nil), raw...), 0xFF)
if _, _, _, _, err := parseGBlock(tampered); err == nil {
t.Fatal("trailing byte accepted; want rejection")
}
}