Files
vm/rpc/atomic_client_zap.go
zeekayandHanzo Dev 469494721e rpc: give a plugin-hosted VM the cross-chain atomic capability
A VM linked into the node reads its per-chain atomic.SharedMemory handle
straight off runtime.Runtime. A VM hosted as a ZAP plugin — the C-Chain EVM
and the D-Chain dexvm both are — could not: handleInitialize rebuilds the
Runtime from InitializeRequest scalars, and neither an interface over a live
database (SharedMemory) nor the node's chain-alias lookup (BCLookup) is a
scalar. Both were silently dropped. Every plugin-hosted VM therefore ran
with SharedMemory nil and DChainID empty, so the DEX 0x9999 settlement seam
was dark on every chain and every swap reverted "requires the cross-chain
atomic capability".

chains/atomic/atomiczap carries the handle. The node listens and the plugin
dials, which is what lets the existing strictly request/response ZAP
transport carry it with no duplex framing and no gRPC — the same shape
already used for DBServerAddr. Both halves live in one package beside the
interface they transport, so the round trip is pinned by tests against a
REAL atomic.Memory over a REAL socket rather than by two mocks agreeing
with each other; splitting them across repos would have made it untestable
in either, since node imports vm.

Those tests found two things reasoning had not. Get does not return an
empty value for an absent key, it errors — so the wire propagates the error
rather than softening it, because softening would let a caller that checks
only err conclude an object exists. And Apply cannot take a database batch
across a process boundary, so it refuses one outright instead of dropping
it; the flush path does not need one, since its window is derived from a
monotone consensus seq and both Puts and Removes are idempotent, making
at-least-once delivery exactly-once in effect.

BCLookup is not proxied. The seam resolves exactly one alias, to a value
fixed for the life of the network, so the resolved id is carried instead —
strictly smaller than a lookup service, and it cannot fail mid-call.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 14:25:14 -07:00

107 lines
3.5 KiB
Go

//go:build !grpc
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"fmt"
"github.com/luxfi/database"
"github.com/luxfi/ids"
)
// atomic_client_zap.go holds the two pieces of the plugin's Runtime that the
// InitializeRequest cannot carry as scalars.
//
// The shared-memory handle itself lives in vm/chains/atomic/atomiczap, beside
// the interface it transports, so the client and the node's server are one
// package and their round trip is pinned by a test against a real atomic.Memory.
// What remains here is the chain-alias lookup, which does not need a transport
// at all.
// optionalID decodes a fixed-width id that is ALLOWED to be absent on the wire.
//
// ids.ToID rejects any length that is not exactly the ID size, which is what the
// required identifiers (chainID, cChainID, ...) want: a short field there means
// a corrupt handshake, and silently zero-padding it would yield a DIFFERENT
// chain. An optional identifier is different — an empty field is a legitimate
// "this network has no such chain" and must decode to ids.Empty rather than fail
// Initialize. Anything non-empty but mis-sized is still an error, so a truncated
// id can never be mistaken for a valid one.
func optionalID(name string, raw []byte) (ids.ID, error) {
if len(raw) == 0 {
return ids.Empty, nil
}
id, err := ids.ToID(raw)
if err != nil {
return ids.Empty, fmt.Errorf("initialize %s: %w", name, err)
}
return id, nil
}
// staticBCLookup resolves the handful of chain aliases a plugin actually needs,
// from ids carried on the Initialize wire.
//
// WHY NOT PROXY THE REAL LOOKUP. runtime.BCLookup is an interface onto the
// node's chain manager. Proxying it would put a synchronous node round trip on
// whatever path called it, and the DEX seam resolves exactly one alias — "D" —
// to a value that is fixed for the life of the network. Carrying the resolved id
// is strictly smaller and cannot fail at an inconvenient moment.
//
// An alias with no id resolves to database.ErrNotFound, the same error the
// node's lookup returns for an unknown alias, so callers that already read that
// as "this network has no such chain" are unchanged. In particular the DEX seam
// reads a missing "D" as "no dexvm deployed" and keeps the on-ramp closed.
type staticBCLookup struct {
byAlias map[string]ids.ID
}
func newStaticBCLookup(cChainID, xChainID, dChainID ids.ID) *staticBCLookup {
l := &staticBCLookup{byAlias: make(map[string]ids.ID, 8)}
set := func(id ids.ID, aliases ...string) {
if id == ids.Empty {
return
}
for _, a := range aliases {
l.byAlias[a] = id
}
}
set(cChainID, "C", "evm")
set(xChainID, "X", "xvm")
set(dChainID, "D", "dex", "dexvm")
return l
}
func (l *staticBCLookup) Lookup(alias string) (ids.ID, error) {
if id, ok := l.byAlias[alias]; ok {
return id, nil
}
return ids.Empty, database.ErrNotFound
}
func (l *staticBCLookup) PrimaryAlias(id ids.ID) (string, error) {
// The primary alias is the single-letter form — the first registered for
// each id above.
for _, alias := range []string{"C", "X", "D"} {
if got, ok := l.byAlias[alias]; ok && got == id {
return alias, nil
}
}
return "", database.ErrNotFound
}
func (l *staticBCLookup) Aliases(id ids.ID) ([]string, error) {
var out []string
for _, alias := range []string{"C", "evm", "X", "xvm", "D", "dex", "dexvm"} {
if got, ok := l.byAlias[alias]; ok && got == id {
out = append(out, alias)
}
}
if len(out) == 0 {
return nil, database.ErrNotFound
}
return out, nil
}