Files
chains/graphvm/block.go
T
zeekayandHanzo Dev 02c90d5c88 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>
2026-07-14 15:39:27 -07:00

302 lines
8.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package graphvm
import (
"context"
"errors"
"time"
"github.com/luxfi/consensus/core/choices"
"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
// block at boot (LastAccepted -> GetBlock). This assertion guarantees *Block
// satisfies block.Block; it is what catches the Status() signature mismatch
// (the interface needs a concrete uint8, not the named choices.Status) at
// compile time so GetBlock can actually return a *Block.
var _ block.Block = (*Block)(nil)
var (
errInvalidBlock = errors.New("invalid block")
)
// Block represents a block in the Graph Chain
type Block struct {
vm *VM
id ids.ID
parentID ids.ID
height uint64
timestamp time.Time
// Graph-specific block data
schemaUpdates []*SchemaUpdate
queryResults []*QueryResult
indexUpdates []*IndexUpdate
chainSyncEvents []*ChainSyncEvent
status choices.Status
bytes []byte
}
// SchemaUpdate represents an update to a GraphQL schema
type SchemaUpdate struct {
SchemaID string `json:"schemaId"`
Operation string `json:"operation"` // create, update, delete
NewVersion string `json:"newVersion,omitempty"`
Schema string `json:"schema,omitempty"`
}
// QueryResult represents a query result to be committed
type QueryResult struct {
QueryID ids.ID `json:"queryId"`
ResultHash []byte `json:"resultHash"`
Status string `json:"status"`
}
// IndexUpdate represents an index update
type IndexUpdate struct {
IndexID string `json:"indexId"`
ChainID ids.ID `json:"chainId"`
Operation string `json:"operation"` // create, update, rebuild
Status string `json:"status"`
}
// ChainSyncEvent represents a chain synchronization event
type ChainSyncEvent struct {
ChainID ids.ID `json:"chainId"`
BlockHeight uint64 `json:"blockHeight"`
BlockHash ids.ID `json:"blockHash"`
Timestamp int64 `json:"timestamp"`
}
// ID implements the chain.Block interface
func (b *Block) ID() ids.ID {
return b.id
}
// Accept implements the chain.Block interface
func (b *Block) Accept(context.Context) error {
b.status = choices.Accepted
// Process schema updates
b.vm.schemaMu.Lock()
for _, update := range b.schemaUpdates {
switch update.Operation {
case "create", "update":
if schema, exists := b.vm.schemas[update.SchemaID]; exists {
schema.Version = update.NewVersion
schema.Schema = update.Schema
schema.UpdatedAt = b.timestamp.Unix()
} else {
b.vm.schemas[update.SchemaID] = &GraphSchema{
ID: update.SchemaID,
Version: update.NewVersion,
Schema: update.Schema,
CreatedAt: b.timestamp.Unix(),
UpdatedAt: b.timestamp.Unix(),
}
}
case "delete":
delete(b.vm.schemas, update.SchemaID)
}
}
b.vm.schemaMu.Unlock()
// Process query results
b.vm.queryMu.Lock()
for _, result := range b.queryResults {
if query, exists := b.vm.queries[result.QueryID]; exists {
query.Status = QueryCompleted
query.CompletedAt = b.timestamp.Unix()
}
}
b.vm.queryMu.Unlock()
// Process index updates
for _, indexUpdate := range b.indexUpdates {
if index, exists := b.vm.dataIndexes[indexUpdate.IndexID]; exists {
index.Status = indexUpdate.Status
}
}
// Process chain sync events
for _, syncEvent := range b.chainSyncEvents {
if source, exists := b.vm.chainSources[syncEvent.ChainID]; exists {
source.LastSync = syncEvent.Timestamp
source.BlockHeight = syncEvent.BlockHeight
}
}
// Update last accepted
b.vm.lastAcceptedID = b.id
b.vm.preferredID = b.id
return nil
}
// Reject implements the chain.Block interface
func (b *Block) Reject(context.Context) error {
b.status = choices.Rejected
return nil
}
// Status implements the block.Block interface. The interface requires a
// concrete uint8; choices.Status is `type Status uint8`, so a method returning
// the named type would NOT satisfy block.Block — which is why GetBlock could
// never have returned a *Block before this fix.
func (b *Block) Status() uint8 {
return uint8(b.status)
}
// Parent implements the chain.Block interface
func (b *Block) Parent() ids.ID {
return b.parentID
}
// ParentID returns the parent block ID
func (b *Block) ParentID() ids.ID {
return b.parentID
}
// Height implements the chain.Block interface
func (b *Block) Height() uint64 {
return b.height
}
// Timestamp implements the chain.Block interface
func (b *Block) Timestamp() time.Time {
return b.timestamp
}
// Verify implements the chain.Block interface
func (b *Block) Verify(ctx context.Context) error {
if b.height == 0 && b.parentID != ids.Empty {
return errInvalidBlock
}
for _, update := range b.schemaUpdates {
if update.Operation != "create" && update.Operation != "update" && update.Operation != "delete" {
return errors.New("invalid schema operation")
}
}
for _, result := range b.queryResults {
if _, exists := b.vm.queries[result.QueryID]; !exists {
return errors.New("result for unknown query")
}
}
b.status = choices.Processing
return nil
}
// Bytes implements the block.Block interface. It returns the deterministic
// canonical encoding set at construction; the block ID is the SHA-256 of
// exactly these bytes, so ParseBlock(b.Bytes()).ID() == b.ID().
func (b *Block) Bytes() []byte {
return b.bytes
}
// genesisTimestamp is the deterministic timestamp of the G-Chain genesis block.
// Genesis is the root of trust (accepted by definition), so a fixed,
// node-independent value is used — never time.Now(), which would make the
// genesis block ID diverge across validators and break consensus agreement.
var genesisTimestamp = time.Unix(0, 0).UTC()
// 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.
//
// 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
// from the genesis config bytes. The G-Chain is a read-only query/index chain —
// it never builds blocks past genesis — so this is its permanent last-accepted
// block, the one GetBlock(LastAccepted()) must return during Initialize.
func newGenesisBlock(vm *VM, genesisBytes []byte) (*Block, error) {
return newBlock(vm, ids.Empty, 0, genesisTimestamp, genesisBytes)
}
// 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 := marshalGBlock(parentID, height, timestamp.Unix(), payload)
return &Block{
vm: vm,
id: ids.ID(hash.ComputeHash256(raw)),
parentID: parentID,
height: height,
timestamp: timestamp,
status: choices.Accepted,
bytes: raw,
}, nil
}
// 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) {
parentID, height, timestamp, _, err := parseGBlock(raw)
if err != nil {
return nil, err
}
return &Block{
vm: vm,
id: ids.ID(hash.ComputeHash256(raw)),
parentID: parentID,
height: height,
timestamp: time.Unix(timestamp, 0).UTC(),
status: choices.Accepted,
bytes: raw,
}, nil
}