mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
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:
+9
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
Reference in New Issue
Block a user