mirror of
https://github.com/luxfi/chains.git
synced 2026-07-27 03:39:41 +00:00
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:
+53
-22
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user