The bare `db*` .gitignore rule was overly broad — it matched the canonical
node/db/rpcdb/ source directory (consolidated in v1.26.28). Previous PRs
needed `git add -f` to land files there. No code references in-repo ./db/
runtime path; runtime DB lives under ~/.lux/ per the canonical operator
convention.
LLM.md + CLAUDE.md updated to reflect actual current main tag.
Part A — swap Layer-B wire-types path:
github.com/luxfi/proto/rpcdb → github.com/luxfi/protocol/rpcdb (v0.0.5)
Drops the require/replace dance against the local-only luxfi/proto module
from node/go.mod. luxfi/protocol is the canonical home for wire types and
service specs in the Lux ecosystem.
Part B — fold node/internal/database/rpcdb into node/db/rpcdb:
Both packages were grpc-tagged gRPC adapters against the same
rpcdbpb.DatabaseClient/Server. The internal one (db_client.go/db_server.go)
predated the Layer A/B/C decomplect; the db/rpcdb one (grpc_server.go +
zap_server.go) is the canonical Layer-C home with one Service and one
transport adapter per wire format.
New grpc_client.go in db/rpcdb mirrors the deleted internal db_client.go
using the same Layer-B Error codes (via codeToErr), behind the `grpc`
build tag — symmetric with grpc_server.go.
Updated service/keystore/rpckeystore/client.go to import the canonical
db/rpcdb.NewGRPCClient instead of internal/database/rpcdb.NewClient.
internal/database/rpcdb/ deleted in its entirety. One canonical rpcdb
home; no dual-shim, no deprecation period.
Default-tag build is clean. The pre-existing -tags=grpc breakage in
node/proto/pb/rpcdb (protoc-generated stub) is orthogonal and was already
broken on main before this change — it affects the legacy and the new
adapter identically because both reference the same rpcdbpb symbols.
Picks up ConsensusInfo.Corona -> ConsensusInfo.Corona rename so the
service_test.go typecheck (TestGetNodeVersionConsensusRoundtrip) compiles.
Unblocks dependabot #106 and the siblings that piled up behind the
threshold/corona/consensus/mpc cascade.
Single LICENSING.md file referencing the canonical three-tier IP and
licensing strategy at github.com/luxfi/.github/blob/main/profile/README.md.
LICENSE file is unchanged; this only adds a navigational pointer.
Layer A — wire framing: github.com/luxfi/api/zap (unchanged)
Layer B — service spec: github.com/luxfi/proto/rpcdb (transport-agnostic
data carriers, was node/proto/zap/rpcdb)
Layer C — service impl + transports: node/db/rpcdb/
- service.go transport-neutral Service wrapping database.Database
- zap_server.go ZAP transport adapter (default; used by cevm)
- grpc_server.go gRPC transport adapter (-tags=grpc)
One Service. Many transport adapters. Adding a transport is a new
adapter file wrapping *Service; storage logic stays in service.go.
Removed (-1390 LOC of duplicate/dead code):
- node/proto/zap/rpcdb/rpcdb.go (moved to luxfi/proto/rpcdb)
- node/proto/rpcdb/rpcdb_zap.go (dead HandlerRegistry path, no callers)
- node/proto/rpcdb/rpcdb_grpc.go (duplicated gRPC server, replaced by
node/db/rpcdb/grpc_server.go)
- node/db/rpcdb/db.go (re-export shim, replaced by Service)
Consumers updated to import the canonical adapter at node/db/rpcdb:
- vms/rpcchainvm/zap/client.go
- vms/rpcchainvm/zap/client_dbserver_test.go
- vms/rpcchainvm/zap/cevm_e2e_test.go
go.mod: add `replace github.com/luxfi/proto => ../proto` for in-tree
development of the central wire-types module.
Tests: db/rpcdb (3/3) + rpcchainvm/zap (4/4 incl. cevm cross-process
e2e — KP_META written via ZAP db channel, no fallback to local zapdb).
Closes the "dbServerAddr empty under ZAP" gap from the cevm v0.48.0
premortem. The gRPC client (vm/rpc/vm_client.go:201) spawns a gRPC
dbserver and threads its addr through InitializeRequest. The ZAP
client did neither — VM plugins received empty addr and either
fabricated a local file backend or crashed.
Changes:
* `Client.Initialize` now calls `startDBServer(init.DB)` BEFORE
sending the wire request, mirroring the gRPC pattern.
* Spawned listener serves `vm/proto/rpcdb.NewZAPServer(init.DB)` —
a default-build (no `grpc` tag) ZAP-native rpcdb server.
* `InitializeRequest.DBServerAddr` is populated with the new
listener's `host:port`. Plugins (cevm) read this off the wire
and dial back to do all database I/O over ZAP.
* Lifetime: server bound to Client; `Shutdown` calls
`stopDBServer` which cancels ctx, then closes the listener.
* `Close()` deliberately does NOT call zapwire.Server.Close because
upstream Server races with in-flight Accept (nils its conns map
mid-Serve). Listener close + ctx cancel is sufficient.
Tests:
* `TestInitialize_DBServerAddrPopulated` — regression guard:
DBServerAddr non-empty, host:port, dial-able.
* `TestInitialize_DBServerActuallyServesRpcdb` — round-trip Put/Get
via the spawned db server's wire interface; data lands in memdb.
* `TestCEvm_DialsZAPdbServer` — TRUE cross-process E2E. Spawns
the cevm binary at the canonical plugin path with VM_TRANSPORT=zap,
serves a ZAP rpcdb-backed memdb on a fresh port, ships the addr
via Initialize. cevm dials it (logged at "[cevm] zapdb: connected
to luxd db listener at 127.0.0.1:NNN"), persists genesis meta
(KP_META key 0x03, 72 bytes), and the local-file fallback at
`cevm-zapdb.bin` is NOT created — proving RemoteZapDB was the
active backend.
- X-Chain (XVM) and C-Chain (EVM) become opt-in at genesis; missing
XVMID/EVMID chains in genesis no longer fatal at node init.
- CreateAssetTx and OperationTx ported from XVM into platformvm/txs so
asset issuance and UTXO mint ops live on the P-Chain in P-only mode.
- Cross-chain P->X->C flows replaced by direct P->subnet warp transfers.
- Signer visitor + backend visitor wired for the new tx types.
- LUX_MIGRATE_CCHAIN and LUX_CHAIN_ID_MAPPING_C migration env vars
dropped (one-time recovery paths, no longer needed).
luxfi/crypto v1.19.0 dropped the inline ipa/ dir and now requires
github.com/luxfi/crypto/ipa as a separate module. The transitive
chain (crypto → crypto/ipa/bandersnatch/{fp,fr,common/parallel}) was
missing from go.sum, breaking goreleaser's go build.
warp v1.18.6 ships the canonical pq.Mode integration —
EnvelopeV2 implements pq.PQEvidencer, LanesForMode(pq.Mode, ...)
replaces the old local profile-enum dispatch, and
pq.ErrClassicalAuthForbidden is the single sentinel across the
stack. Pull luxfi/pq v1.0.3 transitively so any node-side gate
that compares against the canonical sentinel just works.
No source changes needed at this layer; the bump is dep hygiene.
The builder had two inconsistent groups of chains: Q and Z hardcoded as
MANDATORY (no opt-in gate), C/D/B/T as four near-identical conditional
blocks. Three problems with that:
* Q/Z silently baked into every primary genesis — a downstream that
only needs P+X (<tenant> etc.) couldn't keep them out without an
env-knob hack.
* Adding a new primary-network chain meant copy-pasting the
if-block. Four chains = four near-identical conditionals.
* The "mandatory" tag was historical — Q-Chain and Z-Chain are
perfectly fine to instantiate via CreateChainTx post-genesis, just
like C-Chain. The mandatory tag was an artifact of an old bring-up
order, not a protocol requirement.
Now: P and X are mandatory (P implicit, X anchors fees + staking).
Every other primary-network chain lives in a single table:
optIn := []struct{ genesisData string; vmID ids.ID; name string }{
{config.CChainGenesis, constants.EVMID, "C-Chain"},
{config.DChainGenesis, constants.DexVMID, "D-Chain"},
{config.BChainGenesis, constants.BridgeVMID, "B-Chain"},
{config.TChainGenesis, constants.ThresholdVMID,"T-Chain"},
{config.QChainGenesis, constants.QuantumVMID, "Q-Chain"},
{config.ZChainGenesis, constants.ZKVMID, "Z-Chain"},
}
Adding a chain is one row. Removing one is one row. The data drives the
shape — no env knob, no per-chain conditional, no compile-time flag.
Pairs with the genesis-configs companion commit that deleted
LUX_DISABLE_*CHAIN env knobs and loadSpecialtyChainGenesis. Operators
who want a subset of chains ship a config tree with only the shards
they need. Runtime tracking remains separate via luxd's --track-chains.
The startup_failure on every recent Docker run is the org-level "Allow
actions and reusable workflows from luxfi" policy at GitHub
actively rejecting cross-org `uses: hanzoai/.github/.../docker-build.yml`
calls. Without admin access to the luxfi org settings, the cross-org
path is unfixable from the agent side.
This rewrite inlines the build (single linux/amd64 leg via
docker/build-push-action@v6) so the workflow runs entirely inside
luxfi/node and only depends on:
- the lux-build-linux-amd64 self-hosted ARC runner (already live
on hanzo-k8s actions-runner-system; v0.2.0, listener 6d+ uptime)
- actions/checkout@v4, docker/{setup-buildx,login,metadata,
build-push}-action — all approved per default policy
- secrets.GITHUB_TOKEN (auto-injected; no `secrets: inherit` needed)
- secrets.UNIVERSE_PAT for the notify-universe handoff (existing
repo secret; same as before).
Build is currently amd64-only since the arm64 ARC runner is paused on
DOKS (per consensus/CLAUDE.md memory). Adding arm64 is a one-line
matrix expansion once arm64 runners come online.
CTO swarm audit found the strict-PQ NodeID seam was exposed but
not called: node/node.go:132 still derived the validator's
NodeID via ids.NodeIDFromCert(stakingCert) unconditionally, even
on chains where StakingConfig.StakingMLDSAPub was populated.
The seam StakingConfig.DeriveNodeID(chainID) shipped in
dd87350ff5 dispatches per the strict-PQ pivot — when
StakingMLDSAPub is non-empty, NodeID derives from the ML-DSA-65
public key via SHAKE256-384("NODE_ID_V1" || chainID || 0x42 ||
pubKey)[:20] under ids.NodeIDSchemeMLDSA65. Classical-compat
chains fall through to ids.NodeIDFromCert via the same seam.
This commit routes node.go's boot path through the seam.
chainID is ids.Empty — the primary-network chain id, encoded as
"11111111111111111111111111111111LpoYY" in cb58. Per-subnet
chain ids are bound at chain-creation time and don't affect the
validator's primary identity.
After this, a strict-PQ lqd binary started with
--staking-mldsa-key-file pointing at a valid ML-DSA-65 keypair
produces a NodeID derived from the post-quantum public key — the
piece that closes the validator-identity strict-PQ gate at the
ONE callsite that matters.
Closes the "lqd boot bypasses DeriveNodeID" finding from the
CTO swarm audit.
Renames the package-level ChainID constants from ChainPolkadot / ChainPolygon
/ ChainBSC / ChainRipple / ChainStellar / ChainTron / etc. to namespaced
private identifiers (idPolkadot, idPolygon, ...) and exposes them through
a seeded registry instead. Single source of truth for which non-Lux chains
the adapter knows about.
New files:
- chain_ids.go: private id<Chain> constants + their public surface
- chains_registry.go: registry that maps idX → adapter implementations
- chains_seed.go: bootstrap-time population
- registry_full_test.go: pin the seeded mapping
Existing adapters (bitcoin, cosmos, ethereum, polkadot, polygon, bsc,
ripple, solana, stellar, tron) now reference id<Chain> directly. Public
chain-ID iteration is via registry.AllChainIDs() rather than a
copy-paste enum.
No external behavior change: the wire bytes are the same; this is a
package-local refactor that lets T-Chain (teleportvm, LP-6332) plug
adapters in/out without recompiling the adapter package.
Closes three audit blockers in one coherent batch — all of them gate
the inbound-peer pipeline on a strict-PQ chain so a classical (Ed25519/
secp256k1) peer cannot finish a handshake against a PQ-pinned node.
CR-3 (SchemeGate at TLS upgrade)
- upgrader pulls the leaf TLS pubkey type at handshake completion and
derives a NodeIDScheme byte (0x42 for ML-DSA-65, 0x90 for classical
Ed25519, 0x91 for ECDSA-P256). The chain's
ChainSecurityProfile.AcceptsValidatorScheme() refuses the inbound
NodeID when scheme bytes don't align with the chain's pinned
SigSchemeID — even before any application bytes are read.
- Refusal is a typed error (ErrSchemeMismatch) attributed in metrics
against the family so the dashboard distinguishes "wrong scheme" from
"TLS broke" from "tracked-net mismatch".
CR-5 (PQ-only handshake handoff)
- peer.Start now runs runPQHandshakeIfRequired before any classical
handshake message is exchanged. Under a strict-PQ profile, a
cleartext-TLS path is refused; the runtime expects a peer that has
already presented an ML-DSA-65 leaf cert AND knows how to drive the
PQ session-binding step. Test coverage in upgrader_strict_pq_test.go.
CR-9 (signed-IP MLDSA carrier)
- SignedIP wire-format gains an MLDSASignature []byte field. Encoded
append-only on the gossip wire (Reader.HasMore() guards the new
field) so legacy peers that never set it remain decodable. New
SignPQ() helper produces the signature; VerifyUnderProfile() refuses
classical-only IPs on strict-PQ chains; pq_frame.go gives
fuzz-friendly canonical encoding.
- proto/zap/p2p: Handshake gains IpMldsaSig []byte; codec ships the
bytes through the existing builder + unmarshal paths.
- message.OutboundMsgBuilder.Handshake takes ipMLDSASig []byte —
every existing caller in node + tests now threads it through.
Wiring
- n.Config.NetworkConfig.SecurityProfile is set from
n.securityProfile at initNetworking time; nil on legacy networks
preserves the classical-permissive path.
- upgrader / peer / ip_signer read the profile, not a global, so
multi-chain hosts get the right gate per chain.
Tests: network/peer/{ip_pq_test.go, ip_pq_wire_test.go,
upgrader_strict_pq_test.go} pin the gates; go test
./network/peer/ -count=1 -short passes (6.5s).
Module bumps:
- github.com/luxfi/geth v1.16.91 (MLDSATxType + per-chain PQ gate)
- Config bridge at node/config/config.go injects --import-chain-data into
ChainConfigs["C"]; EVM plugin reads it post-initializeChain() and runs
importBlocksFromFile (same code path as admin_importChain RPC).
- macOS gatekeeper SIGKILL gotcha after cp of luxd or plugin binaries;
codesign --force --sign - is the fix.
- Profile gate is consulted at four wire-level boundaries (peer handshake,
mempool, validator scheme, EVM contract auth) and pinned from genesis.
Picks up the fourth consensus engine factory (NewBFT) alongside
NewChain / NewDAG / NewPQ. Adapter wraps luxfi/bft v0.1.5 as a
consensus.Engine; orthogonal to profile selection and threshold
kernel choice.
Pulls in:
github.com/luxfi/pulsar v1.0.0 (Module-LWE — was pulsar-m)
github.com/luxfi/corona v0.2.0 (Ring-LWE — was the old pulsar)
The old luxfi/pulsar-m module is gone. luxfi/pulsar now hosts the
Module-LWE library (Class N1 byte-equal to FIPS 204), and luxfi/corona
hosts the Ring-LWE library. Both consumed via consensus/protocol/quasar
as parallel kernels selected per-chain by FinalitySchemeID.
Matches consensus v1.23.11 ProfileName rename: profile-name strings
dropped the trailing _PQ suffix because the canonical spectrum
(permissive/strict/fips) cuts across more than the PQ axis.
Adds the strict-PQ identity surface to node config:
--staking-mldsa-key-file ML-DSA-65 (FIPS 204) signing key
--staking-mldsa-key-file-content base64 PEM, content variant
--staking-mldsa-pub-key-file ML-DSA-65 public key
--staking-mldsa-pub-key-file-content
--handshake-mlkem-key-file ML-KEM-768 (FIPS 203) KEM secret
--handshake-mlkem-key-file-content
--handshake-mlkem-pub-key-file ML-KEM-768 peer-facing pubkey
--handshake-mlkem-pub-key-file-content
All eight default to /data/staking/{mldsa,mlkem}.{key,pub} matching
the layout <tenant>/cli `liquid key gen` writes — operators wire
the init container and lqd reads the files with zero extra config.
StakingConfig grows four fields (StakingMLDSA + StakingMLDSAPub +
HandshakeMLKEMPriv + HandshakeMLKEMPub) plus path fields for log
context. config.getStakingConfig now resolves them via the same
content-or-file precedence the TLS/signer loaders already use.
Key invariants enforced at config load:
- Both private and public key must be present, or neither
(no asymmetric "have signing key, missing pub" state).
- Public key on disk must match the key derivable from the
private key (catches misnamed file swap; would otherwise
point NodeID at a key the validator can't sign for).
- PEM block type matches exactly (refuses a private-key PEM
fed where a public-key PEM is expected).
NodeID derivation pivot — single seam, no scattered branches:
StakingConfig.IsStrictPQ() → len(StakingMLDSAPub) > 0
StakingConfig.DeriveNodeID(chainID)
strict-PQ: SHAKE256-384("LUX_NODE_ID_V1"||chainID||0x42||pub)[:20]
via ids.NodeIDSchemeMLDSA65.DeriveMLDSA
classical: ids.NodeIDFromCert(StakingTLSCert.Leaf)
StakingConfig.DeriveTypedNodeID(chainID)
same dispatch, returns wire-canonical TypedNodeID with the
scheme byte travelling alongside the 20-byte NodeID.
Callsites that currently call ids.NodeIDFromCert directly are now
bypassing the strict-PQ pivot; migrating them to DeriveNodeID /
DeriveTypedNodeID is the follow-up workstream (the pivot itself
is non-breaking — classical chains continue to route through
NodeIDFromCert via the seam).
Requires luxfi/ids v1.2.10+ for NodeIDSchemeMLDSA65 / DeriveMLDSA.
Carries the namespace rename (lux→security) on /ext/security and the
F118 chain-manager profile-pin stamping landed in v1.26.12. Patch bump
only — no schema or behavior change beyond the rename of the
JSON-RPC namespace and REST sidecar path on the security service.
compatibility.json: add v1.26.12 + v1.26.13 to RPCChainVMProtocol 42
so TestCurrentRPCChainVMCompatible passes (the previous bump to
v1.26.12 left the map at v1.23.25).
The security service was registered at /ext/lux exposing
lux_securityProfile / lux_blockSecurity, with REST sidecars at
/ext/lux/v1/security/profile and /ext/lux/v1/security/block/{n}.
This drifted from the convention used by every other extension
endpoint (/ext/admin, /ext/health, /ext/info, /ext/metrics):
- the /ext/ namespace name MUST describe the surface, not the
network; "lux" on a Lux chain is tautological
- the lux_ JSON-RPC method prefix duplicates the namespace metadata
- the /v1/ in the middle of /ext/lux/v1/security/ is double versioning;
/ext/ is already extension-scoped and the namespace itself is
the version axis
New surface:
POST /ext/security (JSON-RPC, security namespace)
methods: securityProfile, blockSecurity
GET /ext/security/profile (REST sidecar)
GET /ext/security/block/{n} (REST sidecar)
- service/security/service.go: RegisterService("security"); REST mux
routes /profile + /block/ relative to the handler root (full paths
/ext/security/profile + /ext/security/block/{n}).
- service/security/types.go: doc comments name the new surface.
- service/security/service_test.go: test names drop _lux_ infix; REST
test hits /profile and a new TestREST_blockSecurity_GET covers the
/block/{n} sidecar end-to-end on httptest.NewServer.
- node/node.go: APIServer.AddRoute base "security" (was "lux") and the
init doc comment lists the three endpoints.
All 10 tests pass (env GOWORK=off go test ./service/security/...).
Companion to coreth: c84950de4. The chain manager now forwards the
chain-wide ChainSecurityProfile resolved at node bootstrap (F102)
into the C-Chain (coreth) plugin Initialize payload by stamping a
profileID + profileHashHex pin into the JSON config bytes that
already cross the rpcchainvm boundary.
- chains/manager.go adds ManagerConfig.SecurityProfile and an
injectSecurityProfileConfig pass (mirror of injectAutominingConfig)
that runs only on EVMID and is a no-op when the profile is nil.
- node/node.go threads n.securityProfile into chains.ManagerConfig.
- version/constants.go patch-bumps to 1.26.12.
Four F118 tests in chains/security_profile_f118_test.go:
- StampsCChainOnly — pin appears in EVMID config, not in other VMs.
- NoOpWhenProfileNil — classical-compat path passes through.
- MergesExistingConfig — automining + skip-block-fee survive.
- RoundTripsAcrossPluginBoundary — the wire form decodes cleanly
into the coreth-side LuxSecurityProfilePin struct shape, with the
hash decoding to a full 48 bytes (SHA3-384 width).
go.mod bumps consensus v1.23.5 → v1.23.9 to pick up the F113
checkpoint+Merkle SHA3-384 widening.
Before: network/kem/scheme.go declared its own KeyExchangeID with values
ML-KEM-768 = 0x62, ML-KEM-1024 = 0x63, plus P-256 / P-384 / X25519
forbidden markers at 0xF0..0xF2. Disjoint from consensus/config's
0x01/0x02/0x90 canonical block.
After: kem.KeyExchangeID is a type alias of config.KeyExchangeID. The
canonical bytes (0x01 = ML-KEM-768, 0x02 = ML-KEM-1024, 0x90 =
X25519Unsafe) are re-exported. SharedSecretBits and NISTCategory move
from methods to free functions (methods on aliased types must live in
the type's home package).
Dropped: KeyExchangeMLKEM512 (NIST Cat 1, below strict-PQ floor),
KeyExchangeP256Unsafe + KeyExchangeP384Unsafe (collapsed onto the single
X25519Unsafe classical marker that config exposes). No production
caller referenced any of these.
Wire-format change on the peer handshake KEMScheme byte: 0x62/0x63
→ 0x01/0x02. Forward-only; the prior numbering was never released to
any live network.
New test: TestKEMSchemeIDs_AllUseCanonicalNumbering pins
kem.KeyExchange{MLKEM768,MLKEM1024,X25519Unsafe,None} byte-identical to
config.KeyExchange{MLKEM768,MLKEM1024,X25519Unsafe,Invalid}.
The TLS handshake pins CurvePreferences = [tls.X25519MLKEM768], the
IANA-registered hybrid (curve ID 0x11ec). The chain-wide
ChainSecurityProfile pins KeyExchangeMLKEM768 — the post-quantum
component that MUST be present on the wire — and the hybrid satisfies
this because it CONTAINS ML-KEM-768.
The hybrid is strictly stronger than pure ML-KEM-768 (an attacker must
break BOTH X25519 AND ML-KEM-768 to derive the session key). Real-world
TLS 1.3 stacks implement the hybrid today; pure ML-KEM-768 at the TLS
layer is not yet a deployable target.
ForbidClassicalKEM continues to refuse a pure-classical curve at the
application layer; it does NOT refuse a hybrid that includes ML-KEM-768.
Decision recorded in consensus/config commit 12d7000c.
Closes F103 (pure vs hybrid ML-KEM-768 ambiguity).
Expose the chain-wide ChainSecurityProfile to operators, dApps, and
auditors via two surfaces wired through a single boot site
(initSecurityAPI):
- JSON-RPC under namespace "lux" at /ext/lux:
lux_securityProfile → ProfileReply (full profile JSON)
lux_blockSecurity → BlockSecurityReply (chain-wide envelope)
- REST sidecar at /ext/lux/v1/security/{profile,block/{n}}
- Prometheus gauges under namespace "security" on /ext/metrics:
security_profile_post_quantum_end_to_end{profile_id, profile_name}
security_profile_nist_friendly{profile_id}
security_profile_lux_canonical{profile_id}
security_profile_unsafe_mode_enabled{profile_id}
security_mempool_classical_credentials_total{chain}
security_zchain_proof_lag_epochs
The reply shape is SCREAMING_SNAKE canonical names (renderName) so audit
tooling matches on stable identifiers; the underlying scheme bytes come
from consensus/config. A node booted without a SecurityProfile pin
returns ErrNoProfile (RPC) or HTTP 503 (REST) — the legacy networks
keep their classical-compat posture without forging a profile.
Closes the lux_securityProfile and profile-gauges F102 follow-ups.
The X-chain mempool now installs the chain-wide credential-admission
policy resolved from genesis at node bootstrap (F102 close-out). Wiring
path:
genesis → node.SecurityProfile() → xvm.Factory.SecurityProfile
→ VM.securityProfile → vm.Initialize → xmempool.SetAuthPolicy
X-chain credentials are wrapped in fxs.FxCredential; the mempool gate
unwraps them before consulting the auth policy so the same
EnforceCredentialPolicy implementation gates both P-chain and X-chain.
Integration test exercises the full path: build VM with strict-PQ
profile, call Initialize + Linearize, then issue a tx with a wrapped
classical secp256k1 credential via IssueTxFromRPCWithoutVerification.
Observe ErrLegacyCredentialUnderStrictPQ.
The platformvm mempool now installs the chain-wide credential-admission
policy resolved from genesis at node bootstrap (F102 close-out). Wiring
path:
genesis → node.SecurityProfile() → platformvm.config.Internal.SecurityProfile
→ vm.Initialize → pmempool.SetAuthPolicy
Strict-PQ chains refuse classical secp256k1 credentials at gossip time
via the existing auth.EnforceCredentialPolicy gate; legacy/classical-
compat networks keep admitting them unchanged (nil profile is a no-op
gate).
Integration test exercises the full end-to-end path: build VM with
SecurityProfile = LuxStrictPQ(), call Initialize, attempt to add a tx
with an unwrapped *secp256k1fx.Credential, observe
ErrLegacyCredentialUnderStrictPQ.
Closes the CPX deferred item:
"chain-builder code that constructs the mempool today does not call
SetAuthPolicy ... follow-up that depends on plumbing the
*config.ChainSecurityProfile from genesis into the chain builder"
The reusable hanzoai/.github/docker-build.yml requires cross-org callers
to override runner labels — hanzoai org scale-sets aren't visible from
luxfi org. All v1.26.x CI runs have failed with startup_failure since
the upstream workflow added this requirement (~2026-05).
Per the reusable workflow's docstring:
Cross-org callers (luxfi/*, zooai/*, <tenant>/*) MUST override
these with their own org's ARC scale-set labels.
Switch to lux-build-linux-{amd64,arm64} labels which exist on the
luxfi ARC scale-set.
Adds Node.SecurityProfile() getter and Node.initSecurityProfile() boot
step. New() now calls initSecurityProfile() right after
initBootstrappers() and before tracer/metrics/networking/chain init —
every downstream consumer (signer factory, peer handshake gate,
mempool, validator registry, bridge oracle) sees the resolved
*consensus/config.ChainSecurityProfile (or its absence) via the
getter at construction time.
The pin lives in genesis.Config.SecurityProfile (luxfi/genesis@v1.9.6,
which pins luxfi/consensus@v1.23.5). Resolution:
1. genesiscfg.GetConfig(networkID) — load JSON-form genesis
2. cfg.SecurityProfile.Resolve() — refuse mismatched pin
3. stamp result onto n.securityProfile
4. emit startup banner with hash, post-quantum, NIST-friendly,
classical-SNARK, and BLS-fallback posture
A forked binary that swaps in a different canonical profile content
fails Resolve() because the live ComputeHash diverges from the
genesis-pinned hex — closes the F102 attack chain end-to-end.
Adds 5 regression tests for the load + reject paths under StrictPQ
and FIPS profiles, plus nil-pin / wrong-hash / unknown-ID rejection.
Bumps luxfi/consensus v1.23.4 → v1.23.5, luxfi/genesis pseudo →
v1.9.6, luxfi/crypto v1.18.3 → v1.18.4.