Files
node/vms/rpcchainvm/zap/client.go
T
zeekayandHanzo Dev 49e6958664 fix(proposervm): lock verifiedBlocks + Tree maps; fail loud+actionable on a behind height-index
Two pre-existing reliability bugs surfaced by the v1.32.1 roll (both present
identically in v1.31.5 — NOT caused by QCv2), plus a snapshot-inconsistency
diagnostic.

BUG 1 (HIGH) — concurrent-map crash in getPostForkBlock. The consensus engine
drives the proposervm from multiple handler goroutines concurrently (a
PullQuery/Put verifying a block WRITES verifiedBlocks via verifyAndRecordInnerBlk
while a Qbit handler READS it via GetBlock -> getPostForkBlock; the retry loop in
PullQuery even documents "allow concurrent Put to complete"). The verifiedBlocks
map was read/written with no lock -> the Go runtime aborts with "fatal error:
concurrent map read and map write" (exit 2) under heavy verify load. Fix: a
dedicated verifiedBlocksLock (RWMutex) behind three accessors (cachedVerifiedBlock
/ recordVerifiedBlock / forgetVerifiedBlock) at ALL seven sites; the sibling
Tree.nodes map (touched on the same Verify/Accept paths) gets its own RWMutex,
with Accept restructured to make its inner-VM Accept/Reject callouts OUTSIDE the
lock. Both are leaf locks, never nested, never held across a callout — the lock
graph is a strict DAG (vm.lock > verifiedBlocksLock; tree.lock disjoint), so
deadlock-free. innerBlkCache (lru.SizedCache) is already internally locked.
Regression: TestVerifiedBlocksConcurrentAccess + TestTreeConcurrentAccess hammer
readers vs writers under -race; both FAIL on the pre-fix code with the exact
production signatures ("concurrent map read and map write" / "concurrent map
writes") and pass after.

BUG 3 — a proposervm height index BELOW the inner VM tip (the devnet-C
"index 7 < inner 8" from a snapshot restored inconsistently across the proposervm
and EVM databases) had a cryptic fatal ("should never be lower"). It is now a
LOUD, ACTIONABLE fatal with a recovery runbook. It is deliberately NOT
"self-healed" by dropping the finality pointer: after DeleteLastAccepted,
proposervm.LastAccepted() falls back to the INNER-namespace id, whose ParentID is
contiguity-incompatible with the network's OUTER wrappers — that permanently
wedges bootstrap (first-block anchor), catch-up (parent==tip guard) and live
Verify (parent lookup) at the inner tip (blocks at height <= tip are skipped, so
the missing wrapper is never rebuilt): a SILENT wedge strictly worse than the
loud stop. The only correct remedy is operator action (restore a consistent
snapshot or full resync), which the error now states. The reconciliation decision
is decomplected into a pure classifyHeightRepair(pro, inner) -> heightRelation,
regression-locked by TestClassifyHeightRepair so a revert to a silent reset fails
the test; the fork height is read lazily (only the ahead/rollback path needs it),
so the match and behind paths gain no new failure mode.

RED-reviewed: CHANGE 1 (locks) cleared (deadlock-free DAG, fail-on-old proven);
the original bug-3 silent-reset was caught as a wedge and reworked to this
fail-loud form. Full vms/proposervm/... suite green under -race.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 20:45:01 -07:00

723 lines
20 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package zap provides ZAP transport implementation for VM RPC.
package zap
import (
"context"
"encoding/binary"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
zapwire "github.com/luxfi/api/zap"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/log"
rpcdbzap "github.com/luxfi/node/db/rpcdb"
"github.com/luxfi/version"
"github.com/luxfi/vm/chain"
)
var (
ErrNotConnected = errors.New("zap: not connected")
ErrInvalidResponse = errors.New("zap: invalid response")
// errMalformedBlockID is returned when the VM plugin sends a block-id field
// over the ZAP boundary whose length is not exactly ids.IDLen (32 bytes).
// An empty (0-length) field is the plugin's way of signalling that it could
// NOT resolve the block — e.g. the requested block does not connect to this
// node's accepted chain (a fork / missing-parent condition). We surface that
// as this explicit typed error instead of the opaque "invalid hash length"
// from ids.ToID, and we NEVER coerce it to ids.Empty (a 32-byte zero id is a
// legitimate value; a 0-length field is not).
errMalformedBlockID = errors.New("zap: plugin returned malformed block id (does not connect to accepted chain)")
)
// Compile-time check that Client implements chain.ChainVM
var _ chain.ChainVM = (*Client)(nil)
// Client implements chain.ChainVM over ZAP transport
type Client struct {
conn *zapwire.Conn
logger log.Logger
// lastAcceptedID caches the plugin's last-accepted block id. SEEDED at Initialize and
// REFRESHED on every successful block Accept (setLastAccepted) so LastAccepted() honors the
// block.ChainVM contract — return the ACTUAL last-accepted, not a frozen Initialize snapshot.
// Before this refresh the cache froze for the process life: a fire-and-forget Accept advanced
// the plugin (coreth on-disk) but never the cache, so GetAcceptedFrontier served a stale tip and
// any consumer reading VM.LastAccepted was misled. Guarded by lastAcceptedMu because Accept (the
// consensus accept goroutine) and LastAccepted (the network/bootstrap goroutines) race.
lastAcceptedMu sync.RWMutex
lastAcceptedID ids.ID
// dbServer is the ZAP-native rpcdb server spawned in Initialize that
// hosts init.DB for the plugin's outbound RemoteZapDB to dial. It is
// the ZAP equivalent of the gRPC `dbServerListener` pattern in
// vm/rpc/vm_client.go:201. Lifetime is bound to this Client.
dbServerMu sync.Mutex
dbServer *rpcdbzap.ZAPServer
dbListener net.Listener
dbServerCtx context.Context
dbServerCxl context.CancelFunc
}
// NewClient creates a new ZAP-based VM client
func NewClient(conn *zapwire.Conn, logger log.Logger) *Client {
return &Client{
conn: conn,
logger: logger,
}
}
// Dial connects to a ZAP VM server
func Dial(ctx context.Context, addr string, config *zapwire.Config) (*zapwire.Conn, error) {
return zapwire.Dial(ctx, addr, config)
}
// Initialize implements chain.ChainVM
func (c *Client) Initialize(ctx context.Context, init block.Init) error {
var networkID uint32
var chainID, nodeID, publicKey []byte
var xChainID, cChainID, utxoAssetID []byte
var chainDataDir string
if init.Runtime != nil {
rt := init.Runtime
networkID = rt.NetworkID
chainID = rt.ChainID[:]
nodeID = rt.NodeID[:]
publicKey = rt.PublicKey
xChainID = rt.XChainID[:]
cChainID = rt.CChainID[:]
utxoAssetID = rt.UTXOAssetID[:]
chainDataDir = rt.ChainDataDir
}
// Spawn the ZAP rpcdb server before calling Initialize so the VM can
// dial it back as part of its own bootstrap. Mirrors the gRPC
// pattern in vm/rpc/vm_client.go:201 but speaks ZAP. The listener
// lifetime is bound to this Client (closed by Shutdown).
dbServerAddr, err := c.startDBServer(init.DB)
if err != nil {
return fmt.Errorf("zap: start db server: %w", err)
}
req := &zapwire.InitializeRequest{
NetworkID: networkID,
ChainID: chainID,
NodeID: nodeID,
PublicKey: publicKey,
XChainID: xChainID,
CChainID: cChainID,
UTXOAssetID: utxoAssetID,
ChainDataDir: chainDataDir,
GenesisBytes: init.Genesis,
UpgradeBytes: init.Upgrade,
ConfigBytes: init.Config,
DBServerAddr: dbServerAddr,
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
respType, respData, err := c.conn.Call(ctx, zapwire.MsgInitialize, buf.Bytes())
if err != nil {
return fmt.Errorf("zap initialize: %w", err)
}
// Strip both MsgResponseFlag (always set on responses) and MsgErrorFlag
// (set on error responses, though the transport's readLoop already
// converts those into a non-nil err above) before comparing to the
// request message type. Conflating MsgResponseFlag with "error" — as
// earlier code did — corrupted every successful Initialize because the
// binary InitializeResponse payload was rendered as an error string,
// hiding the response and crashing chain creation with an unreadable
// "vm error: <random-looking bytes>" message.
if respType&^(zapwire.MsgResponseFlag|zapwire.MsgErrorFlag) != zapwire.MsgInitialize {
return ErrInvalidResponse
}
resp := &zapwire.InitializeResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return fmt.Errorf("zap decode initialize response: %w", err)
}
seedID, err := blockIDFromZAP("lastAcceptedID", resp.LastAcceptedID)
if err != nil {
return err
}
c.setLastAccepted(seedID)
c.logger.Info("VM initialized via ZAP",
"height", resp.Height,
"lastAcceptedID", seedID,
)
return nil
}
// setLastAccepted refreshes the cached last-accepted id under the write lock. Called at
// Initialize (seed) and on every successful zapBlock.Accept so the cache tracks the plugin's
// real accepted tip instead of freezing at the boot snapshot.
func (c *Client) setLastAccepted(id ids.ID) {
c.lastAcceptedMu.Lock()
c.lastAcceptedID = id
c.lastAcceptedMu.Unlock()
}
// Shutdown implements chain.ChainVM
func (c *Client) Shutdown(ctx context.Context) error {
_, _, err := c.conn.Call(ctx, zapwire.MsgShutdown, nil)
if err != nil {
c.logger.Warn("ZAP shutdown error", "error", err)
}
c.stopDBServer()
return c.conn.Close()
}
// startDBServer binds a fresh TCP listener and serves init.DB over the
// ZAP rpcdb wire protocol against it. Returns the bound addr to pass
// to the plugin via InitializeRequest.DBServerAddr. The listener and
// server are tracked on the Client so Shutdown can close them
// deterministically.
//
// Returns "" if init.DB is nil — some VMs (the platform VM bootstrap)
// init the DB elsewhere; in that case the plugin falls back to its
// LocalZapDB path. Production cevm always gets a non-nil DB.
func (c *Client) startDBServer(db database.Database) (string, error) {
if db == nil {
c.logger.Warn("zap: init.DB is nil — VM will fall back to local backend")
return "", nil
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", fmt.Errorf("zap db listen: %w", err)
}
s := rpcdbzap.NewZAPServer(db)
s.ListenOn(listener)
c.dbServerMu.Lock()
defer c.dbServerMu.Unlock()
c.dbServer = s
c.dbListener = listener
c.dbServerCtx, c.dbServerCxl = context.WithCancel(context.Background())
addr := listener.Addr().String()
go func(ctx context.Context) {
if err := s.Serve(ctx); err != nil && !errors.Is(err, context.Canceled) {
c.logger.Warn("zap db serve exited", "error", err)
}
}(c.dbServerCtx)
c.logger.Info("zap db server listening", "addr", addr)
return addr, nil
}
func (c *Client) stopDBServer() {
c.dbServerMu.Lock()
defer c.dbServerMu.Unlock()
// Cancel ctx FIRST so Serve's loop exits; then closing the listener
// makes any pending Accept() return an error promptly. Close()
// itself is a no-op on the zapwire.Server map (we deliberately
// don't touch it — see ZAPServer.Close comment).
if c.dbServerCxl != nil {
c.dbServerCxl()
c.dbServerCxl = nil
}
if c.dbServer != nil {
_ = c.dbServer.Close()
c.dbServer = nil
}
c.dbListener = nil
}
// SetState implements chain.ChainVM
func (c *Client) SetState(ctx context.Context, state uint32) error {
req := &zapwire.SetStateRequest{
State: zapwire.State(state),
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := c.conn.Call(ctx, zapwire.MsgSetState, buf.Bytes())
return err
}
// Version implements chain.ChainVM
func (c *Client) Version(ctx context.Context) (string, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgVersion, nil)
if err != nil {
return "", err
}
resp := &zapwire.VersionResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return "", err
}
return resp.Version, nil
}
// blockIDFromZAP converts a block-id field returned by the VM plugin over the
// ZAP boundary into an ids.ID, guarding the malformed/empty case. A well-formed
// plugin always returns exactly ids.IDLen bytes; any other length (notably the
// 0-length "could not resolve" signal) yields errMalformedBlockID rather than
// the opaque ids.ToID "invalid hash length" error, and never a coerced zero id.
func blockIDFromZAP(field string, b []byte) (ids.ID, error) {
if len(b) != ids.IDLen {
return ids.Empty, fmt.Errorf("%w: %s was %d bytes, want %d", errMalformedBlockID, field, len(b), ids.IDLen)
}
return ids.ToID(b)
}
// BuildBlock implements chain.ChainVM
func (c *Client) BuildBlock(ctx context.Context) (block.Block, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgBuildBlock, nil)
if err != nil {
return nil, err
}
resp := &zapwire.BlockResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return nil, err
}
if resp.Err != zapwire.ErrorUnspecified {
return nil, errorFromZAP(resp.Err)
}
id, err := blockIDFromZAP("id", resp.ID)
if err != nil {
return nil, err
}
parentID, err := blockIDFromZAP("parentID", resp.ParentID)
if err != nil {
return nil, err
}
return &zapBlock{
client: c,
id: id,
parentID: parentID,
bytes: resp.Bytes,
height: resp.Height,
timestamp: time.Unix(0, resp.Timestamp),
}, nil
}
// ParseBlock implements chain.ChainVM
func (c *Client) ParseBlock(ctx context.Context, blockBytes []byte) (block.Block, error) {
req := &zapwire.ParseBlockRequest{
Bytes: blockBytes,
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, respData, err := c.conn.Call(ctx, zapwire.MsgParseBlock, buf.Bytes())
if err != nil {
return nil, err
}
resp := &zapwire.BlockResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return nil, err
}
if resp.Err != zapwire.ErrorUnspecified {
return nil, errorFromZAP(resp.Err)
}
id, err := blockIDFromZAP("id", resp.ID)
if err != nil {
return nil, err
}
parentID, err := blockIDFromZAP("parentID", resp.ParentID)
if err != nil {
return nil, err
}
return &zapBlock{
client: c,
id: id,
parentID: parentID,
bytes: blockBytes, // Use original bytes
height: resp.Height,
timestamp: time.Unix(0, resp.Timestamp),
}, nil
}
// GetBlock implements chain.ChainVM
func (c *Client) GetBlock(ctx context.Context, blkID ids.ID) (block.Block, error) {
req := &zapwire.GetBlockRequest{
ID: blkID[:],
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, respData, err := c.conn.Call(ctx, zapwire.MsgGetBlock, buf.Bytes())
if err != nil {
return nil, err
}
resp := &zapwire.BlockResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return nil, err
}
if resp.Err != zapwire.ErrorUnspecified {
return nil, errorFromZAP(resp.Err)
}
parentID, err := blockIDFromZAP("parentID", resp.ParentID)
if err != nil {
return nil, err
}
return &zapBlock{
client: c,
id: blkID,
parentID: parentID,
bytes: resp.Bytes,
height: resp.Height,
timestamp: time.Unix(0, resp.Timestamp),
}, nil
}
// SetPreference implements chain.ChainVM
func (c *Client) SetPreference(ctx context.Context, blkID ids.ID) error {
req := &zapwire.SetPreferenceRequest{
ID: blkID[:],
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := c.conn.Call(ctx, zapwire.MsgSetPreference, buf.Bytes())
return err
}
// LastAccepted implements chain.ChainVM. Returns the cache refreshed on every Accept (NOT a
// frozen Initialize snapshot) under the read lock.
func (c *Client) LastAccepted(ctx context.Context) (ids.ID, error) {
c.lastAcceptedMu.RLock()
defer c.lastAcceptedMu.RUnlock()
return c.lastAcceptedID, nil
}
// NewHTTPHandler implements chain.ChainVM
func (c *Client) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgNewHTTPHandler, nil)
if err != nil {
return nil, err
}
resp := &zapwire.NewHTTPHandlerResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return nil, err
}
// If no handler address returned, the VM doesn't have an HTTP handler
if resp.ServerAddr == "" {
return nil, nil
}
// Return a reverse proxy handler to the VM's HTTP server
// For now, return nil as complex HTTP proxying is typically handled differently
c.logger.Debug("VM HTTP handler available", "addr", resp.ServerAddr)
return nil, nil
}
// CreateHandlers calls the VM's CreateHandlers and returns reverse proxy handlers
func (c *Client) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgCreateHandlers, nil)
if err != nil {
return nil, fmt.Errorf("zap CreateHandlers: %w", err)
}
resp := &zapwire.CreateHandlersResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return nil, fmt.Errorf("zap decode CreateHandlers response: %w", err)
}
if len(resp.Handlers) == 0 {
return nil, nil
}
handlers := make(map[string]http.Handler, len(resp.Handlers))
for _, h := range resp.Handlers {
if h.ServerAddr == "" {
continue
}
// Parse the server address and create a reverse proxy
targetURL, err := url.Parse("http://" + h.ServerAddr)
if err != nil {
c.logger.Warn("failed to parse handler address", "prefix", h.Prefix, "addr", h.ServerAddr, "error", err)
continue
}
proxy := httputil.NewSingleHostReverseProxy(targetURL)
handlers[h.Prefix] = proxy
c.logger.Debug("created handler proxy", "prefix", h.Prefix, "target", targetURL.String())
}
c.logger.Info("CreateHandlers returned handlers", "count", len(handlers))
return handlers, nil
}
// Connected implements chain.ChainVM
func (c *Client) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *version.Application) error {
req := &zapwire.ConnectedRequest{
NodeID: nodeID[:],
}
if nodeVersion != nil {
req.Name = nodeVersion.Name
req.Major = uint32(nodeVersion.Major)
req.Minor = uint32(nodeVersion.Minor)
req.Patch = uint32(nodeVersion.Patch)
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := c.conn.Call(ctx, zapwire.MsgConnected, buf.Bytes())
return err
}
// Disconnected implements chain.ChainVM
func (c *Client) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
req := &zapwire.DisconnectedRequest{
NodeID: nodeID[:],
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := c.conn.Call(ctx, zapwire.MsgDisconnected, buf.Bytes())
return err
}
// HealthCheck implements chain.ChainVM
func (c *Client) HealthCheck(ctx context.Context) (block.HealthCheckResult, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgHealth, nil)
if err != nil {
return block.HealthCheckResult{}, fmt.Errorf("health check failed: %w", err)
}
resp := &zapwire.HealthResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return block.HealthCheckResult{}, err
}
result := block.HealthCheckResult{
Healthy: true,
}
// Parse details if present. Wire shape (ZAP-native, k/v list):
//
// [u32 count]( [u32 keylen][key bytes] [u32 vallen][val bytes] )*
//
// Servers that still emit legacy JSON details are tolerated: parse
// failure is non-fatal and Details remains nil. The lux/vm server
// emits an empty Details by default; the day-1 contract is "Details
// is optional and opaque" so this is forward-compatible.
if details, ok := decodeHealthDetails(resp.Details); ok {
result.Details = details
}
return result, nil
}
// decodeHealthDetails parses a ZAP-encoded health-details k/v list. The
// envelope is described above HealthCheck; returns ok=false (silently)
// on any wire-shape mismatch so legacy JSON-emitting servers don't
// trigger spurious health failures.
func decodeHealthDetails(b []byte) (map[string]string, bool) {
if len(b) < 4 {
return nil, false
}
count := binary.LittleEndian.Uint32(b[:4])
out := make(map[string]string, count)
rest := b[4:]
for i := uint32(0); i < count; i++ {
if len(rest) < 4 {
return nil, false
}
klen := binary.LittleEndian.Uint32(rest[:4])
rest = rest[4:]
if uint32(len(rest)) < klen {
return nil, false
}
key := string(rest[:klen])
rest = rest[klen:]
if len(rest) < 4 {
return nil, false
}
vlen := binary.LittleEndian.Uint32(rest[:4])
rest = rest[4:]
if uint32(len(rest)) < vlen {
return nil, false
}
val := string(rest[:vlen])
rest = rest[vlen:]
out[key] = val
}
if len(rest) != 0 {
return nil, false
}
return out, true
}
// GetBlockIDAtHeight implements chain.ChainVM
func (c *Client) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
req := &zapwire.GetBlockIDAtHeightRequest{
Height: height,
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, respData, err := c.conn.Call(ctx, zapwire.MsgGetBlockIDAtHeight, buf.Bytes())
if err != nil {
return ids.Empty, err
}
resp := &zapwire.GetBlockIDAtHeightResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return ids.Empty, err
}
if resp.Err != zapwire.ErrorUnspecified {
return ids.Empty, errorFromZAP(resp.Err)
}
blkID, err := blockIDFromZAP("blkID", resp.BlkID)
if err != nil {
return ids.Empty, err
}
return blkID, nil
}
// WaitForEvent implements chain.ChainVM
func (c *Client) WaitForEvent(ctx context.Context) (block.Message, error) {
_, respData, err := c.conn.Call(ctx, zapwire.MsgWaitForEvent, nil)
if err != nil {
return block.Message{}, err
}
resp := &zapwire.WaitForEventResponse{}
if err := resp.Decode(zapwire.NewReader(respData)); err != nil {
return block.Message{}, err
}
return block.Message{
Type: block.MessageType(resp.Message),
}, nil
}
// Close closes the connection
func (c *Client) Close() error {
return c.conn.Close()
}
// zapBlock implements block.Block
type zapBlock struct {
client *Client
id ids.ID
parentID ids.ID
bytes []byte
height uint64
timestamp time.Time
status uint8
}
func (b *zapBlock) ID() ids.ID { return b.id }
func (b *zapBlock) Parent() ids.ID { return b.parentID }
func (b *zapBlock) ParentID() ids.ID { return b.parentID }
func (b *zapBlock) Bytes() []byte { return b.bytes }
func (b *zapBlock) Height() uint64 { return b.height }
func (b *zapBlock) Timestamp() time.Time { return b.timestamp }
func (b *zapBlock) Status() uint8 { return b.status }
func (b *zapBlock) Verify(ctx context.Context) error {
req := &zapwire.BlockVerifyRequest{
Bytes: b.bytes,
HasPChainHeight: false,
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := b.client.conn.Call(ctx, zapwire.MsgBlockVerify, buf.Bytes())
return err
}
func (b *zapBlock) Accept(ctx context.Context) error {
req := &zapwire.BlockAcceptRequest{
ID: b.id[:],
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
if _, _, err := b.client.conn.Call(ctx, zapwire.MsgBlockAccept, buf.Bytes()); err != nil {
return err
}
// Refresh the cache so LastAccepted() reflects this accept instead of freezing at the
// Initialize snapshot. Only on SUCCESS — a failed accept did not advance the plugin.
b.client.setLastAccepted(b.id)
return nil
}
func (b *zapBlock) Reject(ctx context.Context) error {
req := &zapwire.BlockRejectRequest{
ID: b.id[:],
}
buf := zapwire.GetBuffer()
defer zapwire.PutBuffer(buf)
req.Encode(buf)
_, _, err := b.client.conn.Call(ctx, zapwire.MsgBlockReject, buf.Bytes())
return err
}
func errorFromZAP(err zapwire.Error) error {
switch err {
case zapwire.ErrorUnspecified:
return nil
case zapwire.ErrorClosed:
return errors.New("vm closed")
case zapwire.ErrorNotFound:
return errors.New("not found")
case zapwire.ErrorStateSyncNotImplemented:
return errors.New("state sync not implemented")
default:
return fmt.Errorf("unknown error: %d", err)
}
}