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

125 lines
3.8 KiB
Plaintext

# SPDX-License-Identifier: BSD-3-Clause-Eco
# The warp wire — what one chain says to another, and the payloads it says.
#
# The offsets are the format. They are stated here once and every language's
# accessors come out of this file, so a warp message a Rust node signs is the
# message a Go node verifies.
#
# THREE LAYERS, EACH ITS OWN MESSAGE, and the nesting is what makes the
# signature mean something. A signed message carries the UNSIGNED message's
# bytes and a signature over exactly those bytes; the unsigned message carries
# a payload's bytes; the payload carries an addressed call's bytes. Each layer
# is self-delimiting, so a verifier finds the bytes a signature covers by
# reading a length rather than by re-encoding what it just read — and a
# re-encoding that came out different would be a different message under the
# same signature.
#
# The kind byte at 0 of a payload and of an addressed call says which struct
# to read it as, the same way a transaction's does.
package warp
type id32 = bytes_fixed[32]
type addr = bytes_fixed[20]
type key = bytes_fixed[48]
type sig = bytes_fixed[96]
# ---- the envelope ---------------------------------------------------------
# What a source chain signs: which network, which chain, and what it says.
struct Unsigned {
NetworkID u32 @0
Source id32 @4
Payload bytes @36
}
# The unsigned message's bytes and the signature over them.
struct Message {
Unsigned bytes @0
Signature bytes @8
}
# An aggregate signature and the bit set naming who is in it. The signers are
# a bit set rather than a list because the verifier already holds the ordered
# validator set: what it needs is which of them, not who they are.
struct BitSetSignature {
Kind u8 @0
Signature sig @1
Signers bytes @97
}
# ---- what an unsigned message carries -------------------------------------
# A payload that is a hash and nothing else: the chain asserts a value, and
# what it means is the reader's business.
struct Hash {
Kind u8 @0
Hash id32 @1
}
# A payload addressed to a contract on the destination chain.
struct AddressedCall {
Kind u8 @0
Source bytes @1
Payload bytes @9
}
# ---- what an addressed call carries ---------------------------------------
# A network became an L1, under this manager and this validator set.
struct Conversion {
Kind u8 @0
ID id32 @1
}
# One validator asking to be registered on an L1.
struct RegisterValidator {
Kind u8 @0
Chain id32 @1
BLSKey key @33
Expiry u64 @81
Weight u64 @89
NodeID bytes @97
RemoveThreshold u32 @105
RemoveAddrs list<addr> @109
DisableThreshold u32 @117
DisableAddrs list<addr> @121
}
# The L1 saying a registration landed, or did not.
struct Registration {
Kind u8 @0
ValidationID id32 @1
Registered u8 @33
}
# A validator's weight, set by the L1's manager. The nonce is what stops an
# older weight from being replayed over a newer one.
struct ValidatorWeight {
Kind u8 @0
ValidationID id32 @1
Nonce u64 @33
Weight u64 @41
}
# ---- what a conversion is over --------------------------------------------
# One genesis validator of a converted network, inline at a fixed stride. Its
# node id is a run in a message-wide blob, which is what NodeIDStart and
# NodeIDLen name.
struct ConversionValidator {
NodeIDStart u32 @0
NodeIDLen u32 @4
BLSKey key @8
Weight u64 @56
}
# The bytes a conversion id is the hash of.
struct ConversionData {
Chain id32 @0
ManagerID id32 @32
ManagerAdr bytes @64
Validators list<ConversionValidator> @72
NodeIDPool bytes @80
}