Files
zeekay b0b3388568 The P-chain's last hand-written wire is a schema too
Its transactions were the named job and are done; what was left was
everything else the chain writes. Three more schemas, and the offsets are out
of the source entirely.

chains/schema/warp.zap — the warp envelope, the payloads it carries, and the
conversion preimage an L1's id is the hash of. Eleven structs across warp.rs
and warpmsg.rs, and the differential still reproduces the warp message Go
wrote, byte for byte.

chains/schema/state.zap — what a P node writes to its own disk. Separate from
the wire on purpose: a change to a wire schema is a fork, a change to this one
is a migration, and one file per consequence is how you can tell which you
just made.

The rest went into pchain.zap: the fx primitives the chain spends when they
travel alone rather than inside a transaction, the standalone owner key a
locked balance is tallied under, and a genesis blob.

TWO THINGS THE SCHEMAS HAD TO SAY OUT LOUD, and would have moved bytes
otherwise. The stored Staker reserves sixteen bytes past its last field — it
always has, and a schema that stopped at the public key would write a shorter
record than every record already on disk, so those bytes are named Reserved
for the same reason Out and In name their Pad. And the P-chain's standalone
owner key is NOT the fx owner shape: the same two fields in the other order,
because one is a key and one is a wire. Naming them apart is what keeps a
reader from finding a threshold where a locktime is.

TransferOutput and Utxo are now declared in two schemas — xchain.zap and
pchain.zap — because the X-chain makes them and the P-chain spends them. That
is two statements of one format and it is the wrong number. The right number
needs an import across schemas that zapgen does not have, and cannot get
cheaply: xchain.zap's own envelope holds list<ptr<TransferableOut>>, so the
reference cannot be moved away from the file that declares it. Until then
chains/rust/zap/tests/one_format.rs reads both schemas and fails the day the
two declarations drift — which is the day before a P node and an X node stop
agreeing about a UTXO. Its negative control was run: change one offset in one
file and it fails, naming the struct.

WHAT IS LEFT. Nothing. No Rust chain hand-writes a wire shape any more. The
only remaining calls to the builder outside a generated module are two tests
crafting deliberate buffers to feed a reader, which is where a hand-built
object belongs, and two generic list helpers that take a builder and know
nothing about any shape.

424 platformvm tests including the warp and chain differentials, the corpus
byte test and the hostile-bytes fuzz; `make wire` verified a fixed point over
all seven schemas.
2026-09-06 13:54:27 -07:00

75 lines
2.3 KiB
Plaintext

# SPDX-License-Identifier: BSD-3-Clause-Eco
# What the P-chain writes down.
#
# These are records in this node's own store, not messages between nodes: a
# peer never sees them, and Go's store writes its own layout. They are stated
# here for the same reason the wire is — the offsets ARE the format, and a
# record read at the wrong offset is a validator with the wrong weight, found
# at the next restart rather than at the write.
#
# Nothing here is consensus. A change to this file rewrites what a node reads
# back from its own disk, which is a migration; a change to a wire schema is a
# fork. They are separate files because they are separate consequences.
package state
type id32 = bytes_fixed[32]
type addr = bytes_fixed[20]
# An owner set, as it is stored: the addresses are one flat run rather than a
# list, because nothing indexes into them here.
struct Owners {
Locktime u64 @0
Threshold u32 @8
Addresses bytes @16
}
# One staker. The key is present or it is not — a validator registered before
# keys existed has none, and an all-zero key would be a key that verifies
# nothing, so a byte says which and the run says what.
struct Staker {
Weight u64 @0
Start u64 @8
End u64 @16
Reward u64 @24
Next u64 @32
Priority u8 @40
HasKey u8 @41
PublicKey bytes @48
# Sixteen bytes past the last field. The record has always reserved them
# and no field has ever covered them, so naming them is what keeps the
# emitted size the size already on disk — a schema that stopped at the
# public key would write a shorter record than every record already
# written.
Reserved bytes_fixed[16] @56
}
struct L1Validator {
Chain id32 @0
NodeID addr @32
Start u64 @56
Weight u64 @64
MinNonce u64 @72
EndFee u64 @80
PublicKey bytes @88
RemainingOwner bytes @96
DisableOwner bytes @104
}
struct Conversion {
ConversionID id32 @0
Chain id32 @32
Address bytes @64
}
# One pending change to a validator set: how the weight moved, whether the key
# changed, and the two keys it moved between.
struct Change {
Amount u64 @0
Decrease u8 @8
Renamed u8 @9
Validation id32 @16
KeyBefore bytes @48
KeyAfter bytes @56
}