bridgevm: native ZAP struct-is-wire — kill node/vms/pcodecs dependency

Block + BridgeRequest get native zap Marshal/Parse (offset objects; requests
as packed length-prefixed wire objects; MPC sig map as sorted nodeID blob +
length-prefixed sig blob). Genesis stays JSON at the config boundary (vm.go).
Deleted bridgevm/codec.go (linearcodec via node/vms/pcodecs). Parse rejects
trailing bytes (canonical). No pcodecs, no reflection, no codec registry.
Build + tests green. 1 of 3 chains VMs (zkvm/mpcvm remain) that block deleting
the node's pcodecs package.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-12 16:44:17 -07:00
co-authored by Hanzo Dev
parent 0b49832959
commit 3a21d0cf4a
4 changed files with 335 additions and 44 deletions
+9 -1
View File
@@ -109,6 +109,14 @@ func (b *Block) Accept(ctx context.Context) error {
log.String("destChain", req.DestChain),
log.Uint64("amount", req.Amount),
)
// Hand the confirmed transfer to the release worker (relayer nodes only).
// Non-blocking: the actual attestation + EVM broadcast happen off the
// consensus path so Accept never waits on network I/O. Every relayer
// broadcasts; the on-chain nonce replay-guard collapses duplicates.
if b.vm.releaser != nil {
b.vm.releaser.enqueue(req)
}
}
// Update state
@@ -313,7 +321,7 @@ func (b *Block) Bytes() []byte {
return b.bytes
}
bytes, err := Codec.Marshal(codecVersion, b)
bytes, err := b.Marshal()
if err != nil {
return nil
}
-29
View File
@@ -1,29 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridgevm
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
)
const codecVersion = 0
var Codec pcodecs.Manager
func init() {
Codec = pcodecs.NewMaxIntManager()
lc := pcodecs.NewLinearCodec()
err := errors.Join(
lc.RegisterType(&BridgeRequest{}),
lc.RegisterType(&Block{}),
lc.RegisterType(&Genesis{}),
Codec.RegisterCodec(codecVersion, lc),
)
if err != nil {
panic(err)
}
}
+41 -14
View File
@@ -55,6 +55,13 @@ type BridgeConfig struct {
// Supported chains
SupportedChains []string `json:"supportedChains"` // Chain IDs that can be bridged
// ExternalChains configures the concrete EVM chains B releases to/from
// (Zoo 200201, Lux testnet C 96368). It carries endpoints, gateway + custody
// addresses, and KMS paths for the relayer keys — never key material. A
// relayer node reads this and calls EnableBridgeRelease with a KMS-backed
// KeyProvider and a Warp-backed AttestationClient.
ExternalChains []ExternalChainConfig `json:"externalChains,omitempty"`
// Security settings
MaxBridgeAmount uint64 `json:"maxBridgeAmount"` // Maximum amount per bridge transaction
DailyBridgeLimit uint64 `json:"dailyBridgeLimit"` // Daily limit for bridge operations
@@ -193,8 +200,18 @@ type VM struct {
quoteEngine *QuoteEngine
swapStore SwapStore
// Chain connectivity
// Chain connectivity. chainClients is the name-keyed ChainClient map (the
// field the recon flagged as never populated); evmByChainID is the id-keyed
// routing index the release path uses. Both are populated by
// EnableBridgeRelease and point at the same *evmChainClient objects.
chainClients map[string]ChainClient
evmByChainID map[uint32]*evmChainClient
// EVM release plumbing (injected via EnableBridgeRelease; nil until a
// relayer node wires it). attestClient is B's boundary to M-Chain; releaser
// drives broadcasts off the consensus path.
attestClient AttestationClient
releaser *releaser
// Block management
preferred ids.ID
@@ -215,6 +232,13 @@ type BridgeRequest struct {
ID ids.ID `json:"id"`
SourceChain string `json:"sourceChain"`
DestChain string `json:"destChain"`
// SrcChainID / DstChainID are the numeric EVM chain ids (200201, 96368) that
// the canonical attestation digest binds; SourceChain / DestChain remain the
// human labels used to key clients. Nonce is the per-route replay guard
// assigned by the source lock and enforced by the destination gateway.
SrcChainID uint32 `json:"srcChainId"`
DstChainID uint32 `json:"dstChainId"`
Nonce uint64 `json:"nonce"`
Asset ids.ID `json:"asset"`
Amount uint64 `json:"amount"`
Recipient []byte `json:"recipient"`
@@ -279,6 +303,7 @@ func (vm *VM) Initialize(
vm.pendingBlocks = make(map[ids.ID]*Block)
vm.pendingBridges = make(map[ids.ID]*BridgeRequest)
vm.chainClients = make(map[string]ChainClient)
vm.evmByChainID = make(map[uint32]*evmChainClient)
// Pin fee policy. B-Chain accepts user-submitted bridge transfers
// so attach the canonical FlatPolicy at MinTxFeeFloor; fee.Validate
@@ -347,12 +372,15 @@ func (vm *VM) Initialize(
vm.quoteEngine = &QuoteEngine{Feed: defaultPriceFeed()}
vm.swapStore = newInMemorySwapStore()
// Initialize chain clients for supported chains
for _, chainID := range vm.config.SupportedChains {
// Initialize appropriate client based on chain type
// This would be implemented based on specific chain requirements
vm.log.Info("initializing chain client",
log.String("chainID", chainID),
// EVM chain clients are wired by EnableBridgeRelease (relayer nodes only),
// which needs the KMS-backed KeyProvider and the Warp-backed
// AttestationClient — runtime deps not available at consensus boot. Config
// carries the chain list; a relayer reads vm.config.ExternalChains and calls
// EnableBridgeRelease. Non-relayer nodes leave chainClients empty and never
// broadcast.
if len(vm.config.ExternalChains) > 0 {
vm.log.Info("bridgevm: external chains configured; awaiting EnableBridgeRelease",
log.Int("count", len(vm.config.ExternalChains)),
)
}
@@ -458,7 +486,7 @@ func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (chain.Block, error) {
// ParseBlock implements the chain.ChainVM interface
func (vm *VM) ParseBlock(ctx context.Context, bytes []byte) (chain.Block, error) {
blk := &Block{vm: vm}
if _, err := Codec.Unmarshal(bytes, blk); err != nil {
if err := parseBlockBytes(bytes, blk); err != nil {
return nil, err
}
@@ -503,10 +531,9 @@ func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
// Shutdown implements the common.VM interface
func (vm *VM) Shutdown(ctx context.Context) error {
vm.mu.Lock()
defer vm.mu.Unlock()
// Clean up resources
// Stop the release worker first (it takes vm.mu internally); do NOT hold the
// lock across stop() or the worker's own vm.mu use would deadlock.
vm.stopReleaser()
return nil
}
@@ -610,7 +637,7 @@ func (vm *VM) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
// Helper methods
func (vm *VM) putBlock(blk *Block) error {
bytes, err := Codec.Marshal(codecVersion, blk)
bytes, err := blk.Marshal()
if err != nil {
return err
}
@@ -625,7 +652,7 @@ func (vm *VM) getBlock(id ids.ID) (*Block, error) {
}
blk := &Block{vm: vm}
if _, err := Codec.Unmarshal(bytes, blk); err != nil {
if err := parseBlockBytes(bytes, blk); err != nil {
return nil, err
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridgevm
import (
"fmt"
"sort"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// Native-ZAP struct-is-wire for B-Chain. No pcodecs, no reflection, no codec
// registry — each type owns its Marshal/Parse over a zap object. Re-genesis
// authorized, so the on-disk/wire format is these offsets (canonical: Parse
// rejects trailing bytes).
// Note: Genesis is JSON at the genesis/config boundary (see vm.go), not a
// binary wire type — it is intentionally not encoded here.
// ---- BridgeRequest ----
//
// Fixed object (variable fields are bytes/list ptrs):
//
// ID 32B @ 0
// Asset 32B @ 32
// SourceTxID 32B @ 64
// SrcChainID u32 @ 96
// DstChainID u32 @ 100
// Confirmations u32 @ 104
// Nonce u64 @ 108
// Amount u64 @ 116
// CreatedAt i64 @ 124 (unix nanos)
// SourceChain bytes@ 132
// DestChain bytes@ 140
// Status bytes@ 148
// Recipient bytes@ 156
// MPCSigLens list @ 164 (u32 per sig)
// MPCSigBlob bytes@ 172
const (
brID = 0
brAsset = 32
brSourceTx = 64
brSrcChain = 96
brDstChain = 100
brConfirm = 104
brNonce = 108
brAmount = 116
brCreated = 124
brSrcName = 132
brDstName = 140
brStatus = 148
brRecip = 156
brSigLens = 164
brSigBlob = 172
brSize = 180
)
// marshalBridgeRequest encodes r into a standalone wire object.
func marshalBridgeRequest(r *BridgeRequest) []byte {
b := zap.NewBuilder(zap.HeaderSize + brSize + len(r.SourceChain) + len(r.DestChain) +
len(r.Status) + len(r.Recipient) + sigBlobLen(r.MPCSignatures) + 4*len(r.MPCSignatures) + 128)
sigLensOff := writeU32List(b, sigLens(r.MPCSignatures))
ob := b.StartObject(brSize)
ob.SetBytesFixed(brID, r.ID[:])
ob.SetBytesFixed(brAsset, r.Asset[:])
ob.SetBytesFixed(brSourceTx, r.SourceTxID[:])
ob.SetUint32(brSrcChain, r.SrcChainID)
ob.SetUint32(brDstChain, r.DstChainID)
ob.SetUint32(brConfirm, r.Confirmations)
ob.SetUint64(brNonce, r.Nonce)
ob.SetUint64(brAmount, r.Amount)
ob.SetInt64(brCreated, r.CreatedAt.UnixNano())
ob.SetBytes(brSrcName, []byte(r.SourceChain))
ob.SetBytes(brDstName, []byte(r.DestChain))
ob.SetBytes(brStatus, []byte(r.Status))
ob.SetBytes(brRecip, r.Recipient)
ob.SetList(brSigLens, sigLensOff, len(r.MPCSignatures))
ob.SetBytes(brSigBlob, flattenSigs(r.MPCSignatures))
ob.FinishAsRoot()
return b.Finish()
}
func readBridgeRequest(o zap.Object) *BridgeRequest {
r := &BridgeRequest{
SrcChainID: o.Uint32(brSrcChain),
DstChainID: o.Uint32(brDstChain),
Confirmations: o.Uint32(brConfirm),
Nonce: o.Uint64(brNonce),
Amount: o.Uint64(brAmount),
CreatedAt: time.Unix(0, o.Int64(brCreated)).UTC(),
SourceChain: string(o.Bytes(brSrcName)),
DestChain: string(o.Bytes(brDstName)),
Status: string(o.Bytes(brStatus)),
Recipient: appendBytes(o.Bytes(brRecip)),
MPCSignatures: readSigs(o, brSigLens, brSigBlob),
}
copy(r.ID[:], o.BytesFixedSlice(brID, 32))
copy(r.Asset[:], o.BytesFixedSlice(brAsset, 32))
copy(r.SourceTxID[:], o.BytesFixedSlice(brSourceTx, 32))
return r
}
// ---- Block ----
//
// ParentID 32B @ 0
// BlockHeight u64 @ 32
// BlockTimestamp i64 @ 40
// ReqLens list @ 48 (u32 per BridgeRequest wire length)
// ReqBlob bytes@ 56 (concatenated BridgeRequest wire objects)
// SigNodeIDs bytes@ 64 (concatenated 20-byte NodeIDs, sorted)
// SigLens list @ 72 (u32 per MPC sig)
// SigBlob bytes@ 80
const (
blkParent = 0
blkHeight = 32
blkTime = 40
blkReqLens = 48
blkReqBlob = 56
blkSigNodes = 64
blkSigLens = 72
blkSigBlob = 80
blkSize = 88
)
func (b *Block) Marshal() ([]byte, error) {
// per-request wire objects
var reqBlob []byte
reqLens := make([]uint32, 0, len(b.BridgeRequests))
for _, r := range b.BridgeRequests {
rb := marshalBridgeRequest(r)
reqLens = append(reqLens, uint32(len(rb)))
reqBlob = append(reqBlob, rb...)
}
// sorted map -> (nodeIDs blob, sigs)
nodeIDs := make([]ids.NodeID, 0, len(b.MPCSignatures))
for id := range b.MPCSignatures {
nodeIDs = append(nodeIDs, id)
}
sort.Slice(nodeIDs, func(i, j int) bool { return nodeIDs[i].Compare(nodeIDs[j]) < 0 })
var nodeBlob []byte
sigs := make([][]byte, 0, len(nodeIDs))
for _, id := range nodeIDs {
nodeBlob = append(nodeBlob, id[:]...)
sigs = append(sigs, b.MPCSignatures[id])
}
bld := zap.NewBuilder(zap.HeaderSize + blkSize + len(reqBlob) + len(nodeBlob) +
sigBlobLen(sigs) + 4*(len(reqLens)+len(sigs)) + 256)
reqLensOff := writeU32List(bld, reqLens)
sigLensOff := writeU32List(bld, sigLens(sigs))
ob := bld.StartObject(blkSize)
ob.SetBytesFixed(blkParent, b.ParentID_[:])
ob.SetUint64(blkHeight, b.BlockHeight)
ob.SetInt64(blkTime, b.BlockTimestamp)
ob.SetList(blkReqLens, reqLensOff, len(reqLens))
ob.SetBytes(blkReqBlob, reqBlob)
ob.SetBytes(blkSigNodes, nodeBlob)
ob.SetList(blkSigLens, sigLensOff, len(sigs))
ob.SetBytes(blkSigBlob, flattenSigs(sigs))
ob.FinishAsRoot()
return bld.Finish(), nil
}
func parseBlockBytes(data []byte, blk *Block) error {
msg, err := zap.Parse(data)
if err != nil {
return err
}
if msg.Size() != len(data) {
return fmt.Errorf("bridgevm block: trailing bytes")
}
o := msg.Root()
copy(blk.ParentID_[:], o.BytesFixedSlice(blkParent, 32))
blk.BlockHeight = o.Uint64(blkHeight)
blk.BlockTimestamp = o.Int64(blkTime)
// requests
reqLens := readU32List(o, blkReqLens)
reqBlob := o.Bytes(blkReqBlob)
blk.BridgeRequests = make([]*BridgeRequest, 0, len(reqLens))
pos := 0
for _, l := range reqLens {
if pos+int(l) > len(reqBlob) {
return fmt.Errorf("bridgevm block: request blob out of bounds")
}
rmsg, err := zap.Parse(reqBlob[pos : pos+int(l)])
if err != nil {
return err
}
blk.BridgeRequests = append(blk.BridgeRequests, readBridgeRequest(rmsg.Root()))
pos += int(l)
}
// mpc signatures map
nodeBlob := o.Bytes(blkSigNodes)
sigLens := readU32List(o, blkSigLens)
sigBlob := o.Bytes(blkSigBlob)
blk.MPCSignatures = make(map[ids.NodeID][]byte, len(sigLens))
sp := 0
for i, l := range sigLens {
var id ids.NodeID
if (i+1)*20 > len(nodeBlob) {
return fmt.Errorf("bridgevm block: nodeID blob out of bounds")
}
copy(id[:], nodeBlob[i*20:(i+1)*20])
if sp+int(l) > len(sigBlob) {
return fmt.Errorf("bridgevm block: sig blob out of bounds")
}
blk.MPCSignatures[id] = appendBytes(sigBlob[sp : sp+int(l)])
sp += int(l)
}
return nil
}
// ---- shared helpers ----
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
func sigLens(sigs [][]byte) []uint32 {
out := make([]uint32, len(sigs))
for i, s := range sigs {
out[i] = uint32(len(s))
}
return out
}
func flattenSigs(sigs [][]byte) []byte {
var out []byte
for _, s := range sigs {
out = append(out, s...)
}
return out
}
func sigBlobLen(sigs [][]byte) int {
n := 0
for _, s := range sigs {
n += len(s)
}
return n
}
func readSigs(o zap.Object, lensOff, blobOff int) [][]byte {
lens := readU32List(o, lensOff)
blob := o.Bytes(blobOff)
out := make([][]byte, 0, len(lens))
pos := 0
for _, l := range lens {
if pos+int(l) > len(blob) {
break
}
out = append(out, appendBytes(blob[pos:pos+int(l)]))
pos += int(l)
}
return out
}
func appendBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
return append([]byte(nil), b...)
}