Files
zeekay 518613b5f7 The P-chain reads and writes its wire out of a schema
Every offset the C++ P-chain had was stated by hand, in four places that had
to agree: txs.hpp named the envelope and each of nineteen transactions'
fields, txs_wire.hpp named the strides, txs_wire.cpp did the arithmetic, and
block.hpp said it again for blocks. Four more files — genesis, warp, warpmsg
and components — each named their own. That is the same number written down
six times, and the fix for it is a generator.

Four schemas now say it once, and zapgen prints the readers, the builders and
the offsets from them:

    schema/wire.zap     transactions, blocks, credentials, an owner set,
                        and the two fx envelopes an output is ordered by
    schema/genesis.zap  the genesis blob
    schema/warp.zap     the message and the aggregated proof over it
    schema/warpmsg.zap  what an L1 and the P-chain say to each other

One schema per namespace, because a package name is what a backend renders as
a namespace and a file declares one.

What is left beside them — 283 lines, down from 1,463 — holds not one offset.
It holds the ARRANGEMENT, which is a fact about this chain rather than about
the format: an output's owner addresses do not live in the output, they live
in one run shared by the whole transaction, and the output names a slice of
it. That is what lets a transaction be read without walking it, and it was a
decision this chain made, so it is stated once instead of at the nineteen
places a transaction is built.

The bytes did not move, and that is checked rather than asserted. txs_test,
block_test, genesis_test and components_test compare 39 buffers against hex
the GO REFERENCE printed — a round trip would only prove this port agrees
with itself, which is what a fork also does. All 18 suites pass, and the
chain's answers to all 159 P-chain corpus vectors are byte-identical to the
answers it gave before.

Two things came out of doing it. The generator now refuses a field named as
the struct that holds it: warp's Hash message has a Hash field, and the
accessor for it is a constructor with a return type that the compiler rejects
three files from the schema that caused it — one rule in the front end, all
three backends. And zap.cmake pinned the generator to the runtime's tag, a
version that does not exist in the generator's repository, so that target had
never run; the generator has its own pin now, at zap-proto/go v1.9.0, which
reproduces every committed header byte for byte.
2026-09-06 13:20:10 -07:00

513 lines
17 KiB
Plaintext

# SPDX-License-Identifier: BSD-3-Clause-Eco
# The P-chain's wire, as a schema.
#
# These offsets are the format. They were stated by hand in four places —
# txs.hpp, txs_wire.hpp, txs_wire.cpp and block.hpp — and are stated here
# once instead, so the readers and the builders come out of zapgen rather
# than out of a person, and the C++ chain and the Rust chain and the Go
# reference stop being three chances to mistype the same number.
#
# Every spending transaction opens with the same eight fields — which network,
# which chain, what is spent, what is made — and then says its own thing after
# them. Each kind repeats those eight rather than naming a shared one, because
# this file is a table of consensus offsets: what a kind is, read straight
# down, with nothing to resolve first. The kind byte at 0 says which table.
#
# Fields are written in the order they are declared, which is the order of
# their offsets, which is the order of the bytes.
package lux.platformvm.wire
type id32 = bytes_fixed[32]
type addr = bytes_fixed[20]
type key = bytes_fixed[48]
type proof = bytes_fixed[96]
type sig = bytes_fixed[65]
# One output, inline in an output list. Its owners are a run in the
# transaction-wide address array, which is what AddrStart and AddrCount name:
# that is what lets a transaction be read without walking it. The record
# reserves 72 bytes where its fields end at 68 — the four that make the stride
# eight-aligned — so the width is stated rather than inferred.
struct Out @72 {
Asset id32 @0
StakeLock u64 @32
Amount u64 @40
Threshold u32 @48
OwnerLock u64 @52
AddrStart u32 @60
AddrCount u32 @64
}
# One input, inline in an input list. Its signature indices are a run in the
# transaction-wide index array. Same reserved tail as Out.
struct In @96 {
TxID id32 @0
Index u32 @32
Asset id32 @36
StakeLock u64 @68
Amount u64 @76
SigStart u32 @84
SigCount u32 @88
}
# A plain transfer: the envelope and nothing after it.
struct Base {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
}
# The one kind with no envelope: it spends nothing and names the staker whose
# time is up.
struct RewardValidator {
Kind u8 @0
StakerTxID id32 @1
}
# Bringing value in from another chain.
struct Import {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Source id32 @77
Imported list<In> @109
ImportedSigs list<u32> @117
}
# Sending value out to another chain.
struct Export {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Destination id32 @77
Exported list<Out> @109
ExportedAddrs list<addr> @117
}
# A new chain on a network this transaction is authorized over.
struct CreateChain {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Chain id32 @77
VmID id32 @109
Name text @141
FxIds list<id32> @149
Genesis bytes @157
Auth list<u32> @165
}
# Handing a chain's control to a new owner set.
struct TransferChainOwnership {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Chain id32 @77
Auth list<u32> @109
OwnerThreshold u32 @117
OwnerLocktime u64 @121
NewOwnerAddrs list<addr> @129
}
# Taking a validator off a permissioned chain.
struct RemoveChainValidator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Chain id32 @97
Auth list<u32> @129
}
# A validator joining the primary network, with what it stakes and where its
# reward goes. The four validator fields at 77 are the 44-byte inline record
# every staking kind opens its own section with.
struct AddValidator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Start u64 @97
End u64 @105
Weight u64 @113
StakeOuts list<Out> @121
StakeAddrs list<addr> @129
RewardsThreshold u32 @137
RewardsLocktime u64 @141
RewardsAddrs list<addr> @149
DelegationShares u32 @157
}
# Weight lent to someone else's validator.
struct AddDelegator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Start u64 @97
End u64 @105
Weight u64 @113
StakeOuts list<Out> @121
StakeAddrs list<addr> @129
RewardsThreshold u32 @137
RewardsLocktime u64 @141
RewardsAddrs list<addr> @149
}
# A validator joining a permissioned chain.
struct AddChainValidator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Start u64 @97
End u64 @105
Weight u64 @113
Chain id32 @121
Auth list<u32> @153
}
# A validator joining a chain that anyone may join, with the key it will be
# aggregated under and two reward owners. The signer is a 145-byte inline
# record: one byte saying whether there is one, then the key and its proof.
struct AddPermissionlessValidator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Start u64 @97
End u64 @105
Weight u64 @113
Chain id32 @121
SignerKind u8 @153
SignerKey key @154
SignerProof proof @202
StakeOuts list<Out> @298
StakeAddrs list<addr> @306
ValidatorRewardsThreshold u32 @314
ValidatorRewardsLocktime u64 @318
ValidatorRewardsAddrs list<addr> @326
DelegatorRewardsThreshold u32 @334
DelegatorRewardsLocktime u64 @338
DelegatorRewardsAddrs list<addr> @346
DelegationShares u32 @354
}
# Weight lent on a chain that anyone may join.
struct AddPermissionlessDelegator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
NodeID addr @77
Start u64 @97
End u64 @105
Weight u64 @113
Chain id32 @121
StakeOuts list<Out> @153
StakeAddrs list<addr> @161
RewardsThreshold u32 @169
RewardsLocktime u64 @173
RewardsAddrs list<addr> @181
}
# Paying for an L1 validator's continued registration.
struct IncreaseL1ValidatorBalance {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
ValidationID id32 @77
Balance u64 @109
}
# Standing an L1 validator down.
struct DisableL1Validator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
ValidationID id32 @77
Auth list<u32> @109
}
# A validator joining an L1, carrying the signed message the L1 said it with
# and the balance that pays for it.
struct RegisterL1Validator {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Balance u64 @77
Proof proof @85
Message bytes @181
}
# An L1 restating one of its validators' weights.
struct SetL1ValidatorWeight {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Message bytes @77
}
# Turning a permissioned chain into a staked one: the asset stake is
# denominated in and every threshold measured against it.
struct TransformChain {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Chain id32 @77
Asset id32 @109
InitialSupply u64 @141
MaximumSupply u64 @149
MinConsumptionRate u64 @157
MaxConsumptionRate u64 @165
MinValidatorStake u64 @173
MaxValidatorStake u64 @181
MinStakeDuration u32 @189
MaxStakeDuration u32 @193
MinDelegationFee u32 @197
MinDelegatorStake u64 @201
MaxValidatorWeightFactor u8 @209
UptimeRequirement u32 @210
Auth list<u32> @214
}
# One validator in a genesis set, inline at a fixed stride. A node id and two
# owner address runs do not fit a fixed slot, so each entry names a run into
# one transaction-wide blob and one transaction-wide address array — the same
# shape the envelope uses for its outputs' owners.
struct NetworkValidator {
Weight u64 @0
Balance u64 @8
SignerKey key @16
SignerProof proof @64
NodeIDStart u32 @160
NodeIDLen u32 @164
RemoveThreshold u32 @168
RemoveAddrStart u32 @172
RemoveAddrCount u32 @176
DisableThreshold u32 @180
DisableAddrStart u32 @184
DisableAddrCount u32 @188
}
# A new network, its owner set, how it admits validators, and the genesis set
# it starts with.
struct CreateNetwork {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Parent id32 @77
OwnerThreshold u32 @109
OwnerLocktime u64 @113
NewOwnerAddrs list<addr> @121
RestakeParent u8 @129
Admission u8 @130
Manager u8 @131
Threshold u64 @132
Validators list<NetworkValidator> @140
NodeIDPool bytes @148
AddrPool list<addr> @156
ManagerChainID id32 @164
ManagerAddress bytes @196
}
# A network becoming an L1: it names its manager and the validator set that
# runs it from here on.
struct ConvertNetwork {
Kind u8 @0
NetworkID u32 @1
BlockchainID id32 @5
Outs list<Out> @37
OwnerAddrs list<addr> @45
Ins list<In> @53
SigIndices list<u32> @61
Memo bytes @69
Network id32 @77
Parent id32 @109
ManagerChainID id32 @141
ManagerAddress bytes @173
Validators list<NetworkValidator> @181
NodeIDPool bytes @189
AddrPool list<addr> @197
Auth list<u32> @205
RestakeParent u8 @213
Admission u8 @214
Manager u8 @215
Threshold u64 @216
}
# ---- blocks -------------------------------------------------------------
#
# Three shapes, one prefix. A decided block stops at its timestamp, a standard
# block carries the transactions it applies, and a proposal block carries one
# transaction of its own after them. The kind byte says which was written, and
# writing the shorter one is what makes the shorter bytes.
struct Decided {
Kind u8 @0
Parent id32 @1
Height u64 @33
Time u64 @41
}
struct Standard {
Kind u8 @0
Parent id32 @1
Height u64 @33
Time u64 @41
TxLengths list<u32> @49
TxBlob bytes @57
}
struct Proposal {
Kind u8 @0
Parent id32 @1
Height u64 @33
Time u64 @41
TxLengths list<u32> @49
TxBlob bytes @57
ProposalTx bytes @65
}
# ---- what state and signatures are stored as -----------------------------
# An owner set standing alone, rather than inline in a transaction. This is
# the shape a chain owner is kept in.
struct Owner {
Threshold u32 @0
Locktime u64 @4
Addrs list<addr> @12
}
# The signatures a transaction carries, as their own message: one entry per
# credential naming a run in one shared signature array. A claimed range that
# is not inside the array it names is a refusal, not a clamp — but that is a
# judgement about meaning, and it is made by the reader, not here.
struct CredentialRun {
Start u32 @0
Count u32 @4
}
struct Credentials {
Runs list<CredentialRun> @0
Signatures list<sig> @8
}
# ---- the fx envelopes an output is ordered by ------------------------------
#
# An output's canonical bytes are its SORT KEY, so they are consensus: two
# implementations that build them differently disagree about which
# transactions are well-formed. The two-byte (TypeKind, ShapeKind) pair each
# travels behind sits OUTSIDE this message — it says which fx family and
# which shape the bytes are, which is a question about the chain rather than
# about the payload.
struct Transfer {
Amount u64 @0
Locktime u64 @8
Threshold u32 @16
Addrs list<addr> @20
}
# A transfer output held until a time, carrying the whole envelope of the
# output it releases — discriminator and all, so it is a byte run here.
struct Locked {
Locktime u64 @0
Output bytes @8
}
# One unspent output standing alone, behind its own envelope: where it came
# from, and the whole output it is — discriminator and all.
struct Utxo {
TxID id32 @0
Index u32 @32
Asset id32 @36
Output bytes @68
}