mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
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]>
107 lines
3.5 KiB
Go
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
|
|
}
|