Each removed package was a SECOND way to do something that already has a
canonical first way. Zero importers workspace-wide; verified identical build
output before/after (pre-existing keyutil breakage unchanged).
- chains/rpc 1250 lines whose own header says 'This replaces lines
941-990' of chains/manager.go. The replacement never
happened; manager.go still registers handlers inline.
- node/config.go 265-line shadow of the canonical node/config/node/config.go.
- vms/mpcvm self-described 'thin backward-compatibility alias' wrappers
- vms/dexvm over github.com/luxfi/chains/*. No backwards compatibility,
only forwards perfection.
Simple made easy: one name, one home, one path.
Disconnect/updateUptimeLocked now return (mutated bool, err); VM.Disconnected commits only when the flush wrote uptime state. Before StartTracking (bootstrap churn) and for non-validator peers the flush is a no-op, so the prior unconditional Commit was an empty full-write+fsync on every such disconnect. Skipping it is correct: every writer under stateLock commits its own diff, so no orphaned write depends on the disconnect path.
RED round-2 verdict: SHIP-READY (H1 phantom verified, mutated-gate correct, H2 pre-existing/not-worsened, block-height orthogonal). Isolated from the uncommitted staking-KMS WIP, which is quarantined on feat/staking-kms-native pending its own Blue->Red.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The uptime event-delivery plumbing (node/chain_router.go + chains/manager.go
blockHandler) dropped two invariants avalanchego's handler upholds — the
consensus lock and the peer version — each a mainnet-fleet crash. The uptime
tracker itself (vms/platformvm/uptime_tracker.go) is RED-cleared and UNCHANGED.
CRITICAL #2 — P-chain state race (concurrent map writes -> fatal):
chainRouter dispatched Connected/Disconnected on the peer-lifecycle goroutine,
where VM.Disconnected -> tracker.Disconnect (state.SetUptime) + state.Commit
(state.write) ran concurrently with the block acceptor's state.CommitBatch
(state.write) on the engine accept goroutine — no shared lock, so ordinary peer
churn triggered Go "concurrent map writes" and crashed the P-chain node.
Fix: one VM-owned stateLock serializes every commit of shared platform state
that originates outside the engine's lock-free VM.Accept call-out —
- block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock
(supplied to executor.NewManager) around the whole acceptor visit;
- peer/lifecycle: VM.Disconnected and the onReady/onBootstrapStarted/Shutdown
Start/StopTracking uptime flushes hold vm.stateLock.
This is avalanchego's ctx.Lock invariant (accept serialized with
engine.Connected/Disconnected), scoped to the state the platform VM owns and
implemented at the VM: the Lux engine invokes VM.Accept as a lock-free call-out
(no ctx.Lock over accept exists) and a chain-agnostic blockHandler cannot reach a
per-VM accept lock, so routing "through the engine" is not feasible.
CRITICAL #1 — C-Chain nil-version panic on state sync:
blockHandler.Connected passed connector.Connected(ctx, nodeID, nil). proposervm
promotes Connected to coreth, whose state-sync peer tracker compares peer
versions; a nil version deref panics any C-Chain node running state sync (a fresh
join OR a validator rejoining after falling behind — the launch's core invariant).
Fix: plumb the REAL peer version through a versionedConnector capability —
chainRouter.Connected converts the node peer version (luxfi/node/version) to the
VM boundary type (luxfi/version = chain.VersionInfo) and delivers it via
blockHandler.ConnectedWithVersion -> connector.Connected. Audit: geth in-process
Connected is a no-op stub (nil-safe); xvm stores the pointer without deref
(nil-safe); the real coreth plugin derefs -> fixed by feeding the real version.
Tests (SDKROOT + CGO_ENABLED=1, -race):
- vms/platformvm/uptime_state_race_test.go: state.Commit (VM.Disconnected path)
concurrent with state.CommitBatch (acceptor path) under the shared lock — no
race/fatal; verified meaningful (an unlocked probe trips DATA RACE).
- chains/blockhandler_connected_version_test.go + node/chain_router_connected_version_test.go:
the real, converted version reaches the connector non-nil (dedup covered).
Uptime tracker 13/13 and the block/executor reward gate untouched and green;
go build ./... and go vet clean.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
All 5 mainnet validators read uptime=0.0000 / connected=null even for peers
connected 24h+, so prefersCommit (block/executor/options.go) always saw
0% < threshold and withheld staking rewards (~165M LUX gate).
Root causes (four, all fixed):
1. StartTracking never existed/called. The custom uptimeTracker was created
late (onReady) and never baselined validator records, so a long-running
validator's stored upDuration stayed 0 and CalculateUptimePercentFrom
returned 0/total = 0.
2. upDuration flushed only on Disconnect/Shutdown, so a continuously-connected
validator's persisted uptime never grew.
3. service.go left `connected` hard-nil ("IsConnected no longer exists").
4. ROOT: VM.Connected never fired. network.Connected -> chainRouter.Connected
only added to a connectedPeers set and logged; it never dispatched to chain
handlers, and blockHandler.Connected was a no-op. So tracker.Connect was
never called and the connected map was always empty — nothing to accrue.
Fix (faithful port of avalanchego snow/uptime.Manager semantics, luxfi pkgs):
- vms/platformvm/uptime_tracker.go: rewrite as a startedTracking-gated tracker.
StartTracking/StopTracking/StartedTracking/IsConnected added; CalculateUptime
folds the live connected session forward to now using the persisted
lastUpdated (reconstructed from the second-granular uptime.State encoding), so
a connected validator accrues uptime WITHOUT a Disconnect. Before tracking, a
validator is assumed online since its last update (avalanchego baseline);
after tracking, only genuine sessions accrue. Second-granular clock matches
storage. CalculateUptimePercentFrom is the clean upDuration/(now-from) form,
clamped to [0,1].
- vms/platformvm/vm.go: create+register the tracker at Initialize (so bootstrap
Connect events are captured), StartTracking(primary validators) at onReady,
StopTracking on re-bootstrap and Shutdown. Mirrors avalanchego's
onNormalOperationsStarted lifecycle.
- vms/platformvm/service.go: populate `connected` via tracker.IsConnected.
- node/chain_router.go: chainRouter.Connected/Disconnected now dispatch to every
registered chain handler (snapshot under lock, call outside).
- chains/manager.go: blockHandler forwards Connected/Disconnected to its chain's
VM (engineVM) exactly once (dedup set), so the P-chain uptime tracker — and
every VM's peer set — finally observes connectivity.
Consensus safety: prefersCommit is a per-node PREFERENCE feeding oracle-block
voting, not a deterministic value. Fixing the local uptime measurement (which
was uniformly 0) changes only which reward option each node prefers; consensus
still converges by preference voting. No staking/reward math changed.
Tests (vms/platformvm/uptime_tracker_test.go, -race green):
- TestUptimeTrackerLongRunningValidatorAccruesUptime: a 30-day validator reports
~100%. Uses only the shared Calculator API; run against the OLD tracker it
returns 0.0 (FAIL — the exact mainnet symptom), against the fix 1.0 (PASS).
- Continuously-connected-climbs, StartTracking-baselines, flush-on-disconnect,
never-connected-gets-zero, StopTracking-flushes, idempotent double-connect,
dedup, concurrency. block/executor (reward gate) suite green.
Follow-on (NOT in this commit): devnet 5-node uptime-climb verification ->
gated release -> owner-gated one-at-a-time mainnet roll. No image built, no
deploy, no live validator touched.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The proposervm block wire stored the timestamp as Unix SECONDS (timestamp.Unix()), so the
sub-second window/granularity work was silently truncated to whole seconds when written to the
block: buildChild computed the proposer slot from sub-second time, but verify read back
second-resolution and computed a DIFFERENT slot → errUnexpectedProposer verify-drops at any
window < 1s. Store timestamp.UnixMilli() and read time.UnixMilli() so sub-second timestamps
round-trip and build/verify agree on the slot. Measured at a 100ms window: errUnexpectedProposer
frequent→0, 108→149 TPS, 2.8s→2.2s cadence, 5/5 byte-identical. Forward-only wire change (the
outer proposervm timestamp; the inner EVM block timestamp is independent). Remaining sub-second
floor is now the EVM targetBlockRate, not the proposervm.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two coupled knobs for differentiated cadence — co-located D-Chain/DEX (fast) vs public
C-Chain (standard):
1. MinBlkDelay was hardcoded to the 1s DefaultMinBlockDelay in chains/manager.go, ignoring
its own --proposervm-min-block-delay flag. Now wired node.Config → ManagerConfig →
proposervm.Config (0 ⇒ 1s default). High-throughput nets set it low.
2. Block timestamps were Truncate(time.Second) at 4 sites, quantizing cadence to 1s
regardless of WindowDuration — so a sub-second window inflated slot numbers without finer
time resolution and could NOT produce blocks faster than 1/s. Truncation now tracks
proposer.TimestampGranularity() = min(1s, WindowDuration): mainnet (>=1s) keeps exact
1-second block times; sub-second windows get matching sub-second timestamps.
Measured: 1s window/delay → ~95-104 TPS, byte-identical 5/5 finality (unchanged from before,
no regression). NOTE: true sub-second cadence additionally requires a slot-handoff fix — at
100ms the slot recomputed in buildChild from a drifted 'now' can differ from the slot
timeToBuild scheduled, causing errUnexpectedProposer verify drops; that + multi-sender
saturation is the next step. These knobs are the necessary foundation.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two changes, both node-local:
1. CADENCE — proposer.WindowDuration (the proposer-slot spacing, a validator's min build
delay = slot index x WindowDuration) was a hardcoded 5s const tuned for mainnet-scale
validator sets, flooring small/local block cadence at 5s per slot. It is now a
startup-configurable var (default 5s, unchanged for mainnet) set via the new
--proposervm-window-duration flag → node.Config → ManagerConfig → proposervm.Config →
proposer.SetWindowDuration at VM init. Read by both the windower delay math and
TimeToSlot, so they stay consistent. Measured on a 5-node strict-PQ net: 5s→1s took
sustained C-Chain from 44 TPS / 14s-per-block to 104 TPS / 3.8s-per-block, still 5/5
byte-identical finality.
2. SECURITY (cryptographer MEDIUM-1) — postForkCommonComponents.Verify now refuses a block
carrying a CLASSICAL secp256k1 proposer identity when the chain is strict-PQ
(StakingMLDSASigner set), UNCONDITIONALLY (before the consensusState==Ready gate, so it
also holds during bootstrap/state-sync). Fills the documented-but-unwired 'proposer'
SchemeGate site: the downgrade defense is now an explicit fail-closed in-perimeter gate,
not merely emergent from 20-byte NodeID collision-resistance + upstream enforcement.
Adds SignedBlock.HasClassicalProposer(). Mirrors contract.RefuseUnderStrictPQ.
Pins consensus v1.36.7 (consume-on-error build loop — kills the non-leader BuildBlock spin).
Block + proposervm VM tests green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Under strict-PQ the canonical NodeID is ML-DSA-65-derived (config/node DeriveNodeID →
DeriveMLDSA), so the P-chain validator set and the proposervm windower are ML-DSA-keyed.
But proposervm signed post-fork blocks with the classical TLS leaf and derived the block
Proposer() via ids.NodeIDFromCert — a different value than DeriveMLDSA — so every signed
block (height ≥ 2) failed verifyPostDurangoBlockDelay with errUnexpectedProposer, was
dropped, and rebuilt in an unbounded storm (block 1 survived only because the unsigned
transition block skips the proposer comparison).
The block's offCert slot now carries a scheme-tagged proposer identity [scheme:1B|identity]:
0x90 classical (cert DER → NodeIDFromCert, ECDSA verify) or 0x42 strict-PQ (raw ML-DSA-65
pubkey → DeriveMLDSA(ids.Empty,pub), ML-DSA verify). ML-DSA signing/verification uses a
FIPS 204 §5.2 domain-separation context ('lux-proposervm-block-v1') so a proposer signature
can never be replayed as another ML-DSA message. proposervm.Config gains StakingMLDSASigner
/StakingMLDSAPub, plumbed from StakingConfig through ManagerConfig; exactly one scheme is
active per chain. K=1 nets are unaffected (transition + no-window blocks are unsigned).
Proven on a 5-node strict-PQ local net: sustained multi-block production, every block
built once, gossiped, and finalized byte-identical on all 5 (no automine). Pins consensus
v1.36.6 (engine logger + logged build-drops) which made this diagnosable.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Both executor backends re-declared a local SharedMemory interface with the
weak Apply(map[ids.ID]interface{}, ...interface{}) signature — but nothing
referenced it. The real atomic path uses Runtime.SharedMemory (= the narrow
atomic.SharedMemory: Apply(map[ids.ID]*atomic.Requests, ...database.Batch)).
Pure dead code; removed both. 28 platformvm packages green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The fx set is a CLOSED sum type (secp256k1fx | nftfx | propertyfx), fixed at
compile time — no hot-loading, no genesis-configured fx. So the runtime
'which fx owns this value' dispatch needs no reflection: it is a total match on
the variant tag. Replaced reflect.TypeOf(val) + map[reflect.Type]int with:
- fxKindOf(val): a Go type switch over the closed set of fx primitive types
(compiler-checked exhaustive; lowers to a jump on the interface type tag),
returning the value's wire.TypeKind — the same family tag the wire envelope
already carries.
- FxIndex: a dense [16]int array indexed by that TypeKind (one bounds-checked
load; -1 = unregistered), filled by the SAME fx.(type) switch NewCustomParser
already ran — no separate reflect registration.
getFx (semantic verifier + tx_init) is now fxKindOf → array index: zero reflect,
zero map-hash, compile-time-checked. Deleted registerFxTypes + all
map[reflect.Type]int fields/params (parser, block/parser, vm, backend). Node-only
(uses already-imported utxo fx types + wire.TypeKind); no dep cascade.
Full xvm suite green (11 pkgs — secp/nft/property verify dispatch exercised).
This removes the LAST reflection from the X-chain tx/verify path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Tail of the xvm codec kill (ddb3fbca93): the X-chain wallet builder + signer
now rebuild signed wire bytes as unsigned ‖ fx credential envelopes over the
native luxfi/utxo/wire form, and components/lux parses fx Inputs from their wire
envelope by concrete type. No linearcodec, no reflection on the wallet path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The non-codec main changes (Dockerfile EVM_VERSION v1.104.8, chains/manager
GetContext size-chunking for behind-validator resync + Quasar EXPORT frontier
bridge, warp/signature, rpcchainvm/zap client) that the branch lacked. The
codec-era main files (codec.go/parse.go/tx.go etc.) are intentionally
superseded by this branch's native-ZAP struct-is-wire — not reintroduced.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
RED-verified consensus-safety fix: zap.Parse truncates to the header size
field, so a block buffer with extra tail bytes wraps the SAME message but
ID=hash(bytes) differs — a block-hash malleability / fork vector. setID now
rejects msg.Size() != len(bytes) with ErrExtraSpace. (The P-chain TX envelope
already guards this at tx.go:66; only block Parse had the gap.)
Renamed codec_determinism_test.go -> block_determinism_test.go and converted
its assertions to native ZAP: New*Block + Parse(b) (no Codec), native golden
AbortBlock bytes (65B: zap header + kind/parent/height/time object), byte-
stability + BlockID-stability + trailing-bytes-rejected. Block + txs green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Route registration, client URL builders, and log lines all move to /v1/
(wallet primary api, chains/rpc handler_manager+chain_integration, xvm
client+wallet_client, xsvm api client, multi-network example, manager logs).
Zero /ext/ literals remain. One endpoint namespace, no transition alias.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
All 3 warp codec registries deleted (codec.go, message/codec.go,
payload/codec.go). Every dispatched type now carries an EXPLICIT 1-byte
discriminator at object offset 0 — the invariant is DATA, not registration
order:
warp wkind: 0x00 BitSetSignature, 0x01 CoronaSignature,
0x02 EncryptedWarpPayload, 0x03 HybridBLSCoronaSignature,
0x04 TeleportMessage, 0x05 TeleportTransferPayload,
0x06 TeleportAttestPayload (ids = old registration order)
message mkind: 0 ChainToL1Conversion, 1 RegisterL1Validator,
2 L1ValidatorRegistration, 3 L1ValidatorWeight
payload pkind: 0 Hash, 1 AddressedCall
CONSENSUS-CRITICAL invariant made structural: ChainToL1ConversionID =
sha256(ChainToL1ConversionData.Marshal()) — the ID calls the SAME encoder, so
ID/Marshal skew (the bug in the reverted agent attempt) is impossible. The
native hash preimage is pinned as a golden with field-offset assertions
(verified by the L1 staking contract => consensus surface).
Message = {unsigned bytes, wkind-tagged sig bytes} container object;
UnsignedMessage = {networkID u32, sourceChainID 32B, payload} @44B header.
wire_baseline_test.go rewritten: pins the native hex golden for all 7 wkinds
+ structural discriminator checks + signature dispatch round-trips. Old-codec
sentinels -> zap errors. Whole node builds; warp+platformvm+proposervm green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Converts the per-type P-chain tx tests to native New*Tx + accessor methods +
roundTrip/SyntacticVerify (agents, verified): add_validator, add_chain_validator,
create_blockchain, disable/increase/register/set_weight/remove_chain/
transfer_ownership L1, add_permissionless_delegator, transform_chain. Every
error-path sentinel preserved by driving the bad value THROUGH the constructor
(pure byte-writer). Mock-based cases (fxmock/luxmock/verifymock, impossible on
an immutable zap buffer) reproduced with REAL unspendable owners / unsorted
inputs → real sentinels (ErrOutputUnspendable, ErrInputIndicesNotSortedUnique).
Only the un-reproducible 'already verified' cached-flag case dropped per file.
NEW staker_dispatch_guard_test.go pins the interface-satisfaction invariant the
staker type-switches depend on: struct-is-wire made *AddValidatorTx satisfy both
ValidatorTx+DelegatorTx (safe — ValidatorTx checked first everywhere), but
*AddDelegatorTx must NOT satisfy ValidatorTx (verified: lacks
ValidationRewardsOwner/Shares) or delegators would mis-route. Guard fails loudly
if a future edit breaks either property.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The shared UTXO value tree (UTXO/TransferableOutput/TransferableInput/Asset/
UTXOID/BaseTx/Metadata + OutputOwners path) gets native struct-is-wire
Marshal/Unmarshal (new marshal.go), replacing every pcodecs.Manager. Codec
param dropped from utxo_state / atomic_utxos / transferables (Sort* sorts on
inner fx wire Bytes()); flow_checker pcodecs.Errs -> errors.Join.
Node consensus persistence uses luxfi/utxo (unchanged); components/lux is the
tx-builder/#58 surface — encoding-only change, type tree NOT collapsed (#58
respected). Both P and X share the encoding => internally consistent, re-genesis
safe. Whole node builds EXIT 0; 8 components/lux round-trips + P-chain txs/block
tests green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
CreateNetworkTx no longer carries genesis chains — the executor never created
them, so tx.Chains() was a latent stub (declared, verified, silently dropped).
Model A resolves it by decomplection, not by implementing a second chain path:
- CreateNetworkTx = ∅→Network birth (owner + security.Mode + own validator set).
- CreateChainTx = the ONE chain constructor. An L1 spawn is a BLOCK of
CreateNetworkTx + N CreateChainTx (block-atomic, not tx-atomic).
- Removed NetworkChain, its wire (writeNetworkChains/readNetworkChains/
ncStride/sliceIDs), the chain error set, MaxNetworkChains, the chains param.
- managerChainIdx (index into genesis chains) -> managerChainID (direct
ids.ID), now SYMMETRIC with ConvertNetworkTx's manager ref. ids.Empty =>
P-Chain-governed; a set chainID => Contract-governed staking-contract host.
Net: less code (write as little as possible), one-and-one-way chain creation,
no stub. platformvm build+vet PASS; SovereignL1/InheritedL2/HybridL2/Convert
round-trips PASS.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.
Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.
All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.
KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
LuxAddress in airdrop.AirdropClaim + bridgevmroot.SignerLeaf is the NATIVE
20-byte Lux address (ids.ShortID / [20]byte), held beside the EVM common.Address
it disambiguates. 'Lux' prefix is banned place-in-value naming; the canonical
pair is EVMAddr / UTXOAddr. Named by address KIND, not brand.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pull the network security model out of the tx wire into a small orthogonal
package (vms/platformvm/security): pure values, stdlib-only, no zap/tx/state
deps. One definition — security.Mode{RestakeParent, Admission, Threshold,
Manager} + Sovereign() + Valid() — composed by tx wire, executor, and state
(Rich Hickey: values not places; Rob Pike: no stutter, small orthogonal pkg).
Two orthogonal axes replace the flat Inherited/Sovereign byte:
- RestakeParent: lean on parent's validator set
- own set: Admission(NoOwnSet|Open|Gated) + Manager(PChain|Contract)
Their product spans every mode incl. HYBRID L2 (restake AND additive own set),
which the flat byte could not express. Invariant RestakeParent || own-set on
Mode.Valid(); Sovereign derived, never flagged.
CreateNetworkTx + ConvertNetworkTx carry security.Mode on the wire via shared
setSecurity/readSecurity. Round-trip green incl. new HybridL2 case + Convert
target-mode. Wallet builders pass the explicit restaked-L2 Mode.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Blocks are now the zap buffer: kind + parentID@1 + height@33 + time@41 +
tx-length-list@49 + tx-blob@57 (+ proposal-tx@65). commonZapBlock embedded base
mirrors spendingTx. Parse = zap.Parse + kind dispatch (no codec, no version).
Deleted block/codec.go + block/v0 + lift_v0. Round-trip green for abort/commit/
proposal/standard incl real signed txs. go build + go test ./block/ = green.
FINDING: block.GenesisCodec was double-duty — also serialized NON-block STATE
values (feeState, owners, L1Validator, metadata, chains). That's a THIRD codec
surface (state DB serialization) still to migrate for the full kill.
Create and Convert are orthogonal arrows: CreateNetworkTx = ∅→Network (birth,
sovereign-or-inherited); ConvertNetworkTx = Network→Network (promote an
existing network: inherited→sovereign, re-anchor parent — L2→L1, L3→L1, with
owner auth). Only CreateSovereignL1 stays folded (it was birth+sovereign, not
a distinct op). Both reuse the NetworkValidator component. Round-trip green
incl L2→L1 promote.
CreateNetworkTx{parent, owner, security, validators, chains, manager} creates a
network at ANY level in one tx — parent is the level axis (Primary⇒L1, L1⇒L2,
recurse; level=depth, never stored). security is a coproduct: SecuritySovereign
(own validators+manager) | SecurityInherited (restaked from parent). manager
lives on the Network so an inherited L2 holds local admin. NetworkValidator +
NetworkChain are shared value components (reused by CreateChainTx). Deleted
ConvertNetworkToL1Tx + CreateSovereignL1Tx (folded / migration artifacts).
Round-trip green: sovereign L1 (own validators+chains+manager) AND inherited L2
(parent recorded, no own validators, local manager) both survive. One and one
way to make a network at any depth.
zap.ListBuilder.AddBytes counts BYTES not elements; fixed-stride lists were
storing byte-inflated counts -> stride clamp rejected them. Fixed every write
helper to store the real element count (len(entries)). Round-trip test now
GREEN across the hardest cases: multisig+stakeable outputs/inputs, the nested
ConvertNetworkToL1Validator (NodeID+BLS PoP+2 owners), SovereignL1Chain
manifest, and signed unsigned‖creds. go build ./vms/platformvm/txs = exit 0.
The P-chain tx wire is now struct-IS-wire, codec-free, and verified correct.
All 21 real P-chain tx types are now the zap buffer — no codec, no marshal/
unmarshal, no Manager, no version, decompounded, on luxfi/zap. ConvertNetworkToL1Tx
+ CreateSovereignL1Tx hand-written with a shared ConvertNetworkToL1Validator
nested encoder (fixed-stride records + shared NodeID/addr pools) and a
SovereignL1Chain encoder. go build ./vms/platformvm/txs/ = exit 0.
Next: round-trip correctness test, then the external consumer flip.
- SlashValidatorTx ripped (unwired stub; Avalanche has no slashing; consensus
only detects, never enforces). Type+executor+tests+Visitor surface removed.
- CreateAssetTx + OperationTx ripped from P-CHAIN (X-chain/AVM types; LP-0130:
asset creation + fx ops are X-chain money-rail, not P staking-rail; no P
producer). xvm/avm untouched.
- Staker interfaces (StakerTx/ValidatorTx/DelegatorTx/ScheduledStaker) restored
on the 5 validator types via pure accessors + FxID-on-stake preserved.
- Remaining real complex types: ConvertNetworkToL1Tx + CreateSovereignL1Tx.
tx.go: Tx holds Unsigned (zap-backed) + Creds; Parse wraps signed bytes and
splits unsigned/creds at the self-delimiting boundary; Sign builds unsigned‖creds;
no codec.Manager param anywhere. Deleted codec.go (V0/V1/V2 reflection Manager)
and codec_activation.go. WIP: consumer sites that passed txs.Codec / called the
old Parse(codec, bytes) flip next.
Parse wraps the buffer zero-copy and dispatches on the 1-byte kind. Signed =
unsigned ‖ creds (both self-delimiting), so unsigned is a byte-prefix of
signed. writeCredsBuf/parseCredsBuf encode credentials natively (shared 65B
sig-blob array). No Marshal/Unmarshal/Manager. WIP: references the 24 pure
type constructors (5 hand-written + 18 in parallel conversion + 1 template).
Shared delta-field encoders every non-proposal tx composes on the spending
envelope: owner (fx.Owner), auth (*secp256k1fx.Input), inline Validator (44B)
+ Signer (145B: kind+BLS pubkey+PoP), id lists, extra spending lists. Built on
luxfi/zap. With spending.go, the full reusable foundation — type files are now
pure compositions.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + multisig/stakeable
Output/Input entries + shared owner-address/sig-index arrays, built on
luxfi/zap generic primitives — no codec, no zap_native package. writeSpending/
setEnvelope build inside New*Tx; readEnvelope reads lazily. Polymorphism
(TransferOutput/LockOut, TransferInput/LockIn) handled in explode/assemble.
Foundation every non-proposal tx composes.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
kind.go: 1-byte discriminator @ object offset 0 = the whole dispatch. No
codec, no version, no slot map. AdvanceTimeTx converted to the pure model:
holds *zap.Message, Time() is an offset read, NewAdvanceTimeTx builds once,
Bytes() returns the buffer. No marshal/unmarshal anywhere.
Template for the remaining 21 types. Package migrates atomically (codec.go +
all types + Parse/Sign together), so it compiles green again only when the
whole set + concentrated consumers (executor/builder/fee/api, ~63 New sites)
are converted. WIP.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
ZAP has no serialization step. The bridge copied fields between plain txs.*
structs and the buffer, which is exactly the codec ZAP deletes. Removed.
The right way: tx type IS the zap buffer (accessors, Parse=wrap, Bytes=buffer),
no codec / manager / compat / version. Wire primitives stay in luxfi/zap.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Shared spending envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + polymorphic
converters bridging the txs struct graph to proto/zap_native primitives with
zero reflection: TransferOutput/stakeable.LockOut, TransferInput/stakeable.LockIn,
secp256k1fx.Credential. BaseTx (TxKindBaseFull) bridged; creds travel in the
separate creds buffer of the unsigned‖creds envelope.
Round-trip GREEN: 2-of-3 multisig + owner-locktime output, stakeable.LockOut,
stakeable.LockIn input, memo, AND signed secp256k1 credentials all survive
Marshal->Unmarshal with exact field equality; unsigned stays a byte-prefix of
signed. Every embedding tx type now composes this envelope + its delta fields.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>