platformvm/txs/zap_native: 5 more tx types (LP-023 Phase 1 batch 2)

Native ZAP encoding — no marshal step, struct IS wire format — for the
next batch of platformvm tx types. Same pattern as Phase 1 batch 1.

New types (v1 schemas; variable-length nested fields deferred to batch 3):
  base_tx.go                     — NetworkID + BlockchainID + Memo
                                   (Outs/Ins → batch 3)
  register_l1_validator_tx.go    — ValidationID + BLS pubkey 48B + PoP 96B
                                   + Expiry + RemainingBalanceOwnerID stub
                                   (Warp Message + full OutputOwners → batch 3)
  slash_validator_tx.go          — NodeID + Subnet + SlashPercentage
                                   (Evidence variable-length → batch 3)
  transfer_chain_ownership_tx.go — Chain + Owner{threshold,locktime,addr} v1 stub
                                   (full OutputOwners list → batch 3)
  remove_chain_validator_tx.go   — NodeID + Subnet
                                   (ChainAuth lives in signed wrapper)

Tests + benches extend the existing all_tx_types_test.go +
all_types_bench_test.go infra (no duplication). 18 new accessor zero-alloc
sites verified, including 48B BLSPublicKey and 96B PoP by-value returns.

Aggregate bench results (Apple M1 Max, Go 1.24, -benchtime=2s):

  Parse Legacy vs ZAP:
    BaseTx                      470.0  →  60.42 ns  ( 7.8x)  152→24 B  3→1 alloc
    RegisterL1ValidatorTx       1008   →  75.03 ns  (13.4x)  384→24 B  6→1 alloc
    SlashValidatorTx            690.6  →  82.74 ns  ( 8.3x)  176→24 B  4→1 alloc
    TransferChainOwnershipTx    2459   → 161.5  ns  (15.2x)  192→24 B  4→1 alloc
    RemoveChainValidatorTx      879.6  → 117.2  ns  ( 7.5x)  176→24 B  4→1 alloc

  Parse cross-type mean (10 types now native): 8.5x speedup.

  Build (one-time per proposer, dominated by Parse on consensus path):
    BaseTx       1.5x slower (memo SetBytes deferred-copy; acceptable trade)
    Register     1.03x       (allocs 7→2)
    Slash        1.4x faster (allocs 5→2)
    Transfer     0.96x       (allocs 5→2)
    Remove       1.3x faster (allocs 5→2)

All accessor reads zero-alloc. All builds 1 alloc on read path.

Verification:
  cd ~/work/lux/node
  GOWORK=off go build ./vms/platformvm/txs/zap_native/...
  GOWORK=off go test  ./vms/platformvm/txs/zap_native/...
  GOWORK=off go test -bench=. -benchmem -benchtime=2s \\
    ./vms/platformvm/txs/zap_native/...

Activation 1782604800 (2026-07-01 00:00 UTC) unchanged.
LUXD_ENABLE_LEGACY_CODEC=1 opts INTO legacy unchanged.

Refs LP-023. Next: batch 3 — variable-length nested-object schemas (Outs/Ins
lists, Warp Message payloads, full OutputOwners, Evidence) + remaining tx
types via codegen.
This commit is contained in:
Hanzo AI
2026-06-02 13:34:12 -07:00
parent 6f1c1811ca
commit 7aa01da346
8 changed files with 1000 additions and 0 deletions
+2
View File
@@ -39,6 +39,8 @@ selection, and EVM contract auth.
| SHA | Tag | Impact |
|-----|-----|--------|
| (pending) | next | LP-023 Phase 1 batch 2: 5 more native-ZAP tx types — BaseTx v1, RegisterL1ValidatorTx v1, SlashValidatorTx v1, TransferChainOwnershipTx v1, RemoveChainValidatorTx. Cross-type Parse mean speedup 8.5× over linearcodec. Variable-length nested-object schemas (Outs/Ins/full OutputOwners/Warp Message/Evidence) deferred to batch 3. |
| `e77a7ef78e` | (pre) | LP-023 Phase 1 batch 1: 4 simple tx types + bench harness. 37× Parse, 5.6× cross-type mean. |
| `9df72a6f55` | v1.26.10 | Wire ChainSecurityProfile into bootstrap (closes F102) |
| `c4af52411e` | v1.26.10 | X-Chain (avm) mempool refuses classical creds under strict-PQ |
| `a14a1601f4` | v1.26.10 | P-Chain (platformvm) mempool refuses classical creds under strict-PQ |
@@ -4,8 +4,10 @@
package zap_native
import (
"bytes"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
)
@@ -90,17 +92,182 @@ func TestDisableL1ValidatorTxRoundTrip(t *testing.T) {
}
}
func TestBaseTxRoundTrip(t *testing.T) {
const wantNetID uint32 = 1337
wantChain := ids.ID{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
wantMemo := []byte("LP-023 batch 2 canary memo")
tx := NewBaseTx(wantNetID, wantChain, wantMemo)
if !IsZAPBytes(tx.Bytes()) {
t.Fatal("Bytes() not ZAP-formatted")
}
if got := tx.NetworkID(); got != wantNetID {
t.Fatalf("NetworkID() = %d, want %d", got, wantNetID)
}
if got := tx.BlockchainID(); got != wantChain {
t.Fatalf("BlockchainID() = %v, want %v", got, wantChain)
}
if got := tx.Memo(); !bytes.Equal(got, wantMemo) {
t.Fatalf("Memo() = %x, want %x", got, wantMemo)
}
tx2, err := WrapBaseTx(tx.Bytes())
if err != nil {
t.Fatal(err)
}
if tx2.NetworkID() != wantNetID || tx2.BlockchainID() != wantChain || !bytes.Equal(tx2.Memo(), wantMemo) {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestBaseTxNilMemo(t *testing.T) {
// Memo is variable-length; nil/empty memo must round-trip cleanly.
tx := NewBaseTx(1, ids.ID{0xfe, 0xed}, nil)
if memo := tx.Memo(); len(memo) != 0 {
t.Fatalf("nil-memo round-trip got %x, want empty", memo)
}
}
func TestRegisterL1ValidatorTxRoundTrip(t *testing.T) {
wantValID := ids.ID{0xaa, 0xbb, 0xcc, 0xdd}
var wantBLS [bls.PublicKeyLen]byte
for i := range wantBLS {
wantBLS[i] = byte(i + 1)
}
var wantPoP [bls.SignatureLen]byte
for i := range wantPoP {
wantPoP[i] = byte(0xff - i)
}
const wantExpiry uint64 = 1_900_000_000
wantOwnerID := ids.ID{0x12, 0x34, 0x56, 0x78}
tx := NewRegisterL1ValidatorTx(wantValID, wantBLS, wantPoP, wantExpiry, wantOwnerID)
if tx.ValidationID() != wantValID {
t.Fatal("ValidationID mismatch")
}
if tx.BLSPublicKey() != wantBLS {
t.Fatal("BLSPublicKey mismatch")
}
if tx.ProofOfPossession() != wantPoP {
t.Fatal("ProofOfPossession mismatch")
}
if tx.Expiry() != wantExpiry {
t.Fatalf("Expiry = %d, want %d", tx.Expiry(), wantExpiry)
}
if tx.RemainingBalanceOwnerID() != wantOwnerID {
t.Fatal("RemainingBalanceOwnerID mismatch")
}
tx2, err := WrapRegisterL1ValidatorTx(tx.Bytes())
if err != nil {
t.Fatal(err)
}
if tx2.ValidationID() != wantValID || tx2.BLSPublicKey() != wantBLS ||
tx2.ProofOfPossession() != wantPoP || tx2.Expiry() != wantExpiry ||
tx2.RemainingBalanceOwnerID() != wantOwnerID {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestSlashValidatorTxRoundTrip(t *testing.T) {
wantNode := ids.NodeID{0x01, 0x02, 0x03, 0x04, 0x05}
wantSubnet := ids.ID{0xa1, 0xa2, 0xa3, 0xa4}
const wantPct uint32 = 250_000 // 25% in PercentDenominator units
tx := NewSlashValidatorTx(wantNode, wantSubnet, wantPct)
if tx.NodeID() != wantNode {
t.Fatalf("NodeID = %v, want %v", tx.NodeID(), wantNode)
}
if tx.Subnet() != wantSubnet {
t.Fatal("Subnet mismatch")
}
if tx.SlashPercentage() != wantPct {
t.Fatalf("SlashPercentage = %d, want %d", tx.SlashPercentage(), wantPct)
}
tx2, err := WrapSlashValidatorTx(tx.Bytes())
if err != nil {
t.Fatal(err)
}
if tx2.NodeID() != wantNode || tx2.Subnet() != wantSubnet || tx2.SlashPercentage() != wantPct {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestTransferChainOwnershipTxRoundTrip(t *testing.T) {
wantChain := ids.ID{0xc0, 0xc1, 0xc2}
const wantThreshold uint32 = 1
const wantLocktime uint64 = 1_800_000_000
wantAddr := ids.ShortID{0xbe, 0xef, 0xca, 0xfe}
tx := NewTransferChainOwnershipTx(wantChain, wantThreshold, wantLocktime, wantAddr)
if tx.Chain() != wantChain {
t.Fatal("Chain mismatch")
}
if tx.OwnerThreshold() != wantThreshold {
t.Fatalf("OwnerThreshold = %d, want %d", tx.OwnerThreshold(), wantThreshold)
}
if tx.OwnerLocktime() != wantLocktime {
t.Fatalf("OwnerLocktime = %d, want %d", tx.OwnerLocktime(), wantLocktime)
}
if tx.OwnerAddress() != wantAddr {
t.Fatal("OwnerAddress mismatch")
}
tx2, err := WrapTransferChainOwnershipTx(tx.Bytes())
if err != nil {
t.Fatal(err)
}
if tx2.Chain() != wantChain || tx2.OwnerThreshold() != wantThreshold ||
tx2.OwnerLocktime() != wantLocktime || tx2.OwnerAddress() != wantAddr {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestRemoveChainValidatorTxRoundTrip(t *testing.T) {
wantNode := ids.NodeID{0x10, 0x20, 0x30, 0x40, 0x50}
wantSubnet := ids.ID{0xfa, 0xce, 0xb0, 0x0c}
tx := NewRemoveChainValidatorTx(wantNode, wantSubnet)
if tx.NodeID() != wantNode {
t.Fatal("NodeID mismatch")
}
if tx.Subnet() != wantSubnet {
t.Fatal("Subnet mismatch")
}
tx2, err := WrapRemoveChainValidatorTx(tx.Bytes())
if err != nil {
t.Fatal(err)
}
if tx2.NodeID() != wantNode || tx2.Subnet() != wantSubnet {
t.Fatal("wrap-round-trip mismatch")
}
}
// Zero-allocation accessor verification across all the simple types. Reading
// a field from a wrapped accessor must allocate 0 — that's the whole point
// of native ZAP.
func TestAllAccessorsZeroAlloc(t *testing.T) {
id := ids.ID{1, 2, 3}
nodeID := ids.NodeID{4, 5, 6}
shortID := ids.ShortID{7, 8, 9}
var blsPub [bls.PublicKeyLen]byte
var pop [bls.SignatureLen]byte
atx := NewAdvanceTimeTx(123)
rtx := NewRewardValidatorTx(id)
stx := NewSetL1ValidatorWeightTx(id, 1, 1)
itx := NewIncreaseL1ValidatorBalanceTx(id, 1)
dtx := NewDisableL1ValidatorTx(id)
btx := NewBaseTx(1337, id, []byte("memo"))
regTx := NewRegisterL1ValidatorTx(id, blsPub, pop, 1, id)
slTx := NewSlashValidatorTx(nodeID, id, 100_000)
tcoTx := NewTransferChainOwnershipTx(id, 1, 0, shortID)
rcvTx := NewRemoveChainValidatorTx(nodeID, id)
cases := []struct {
name string
@@ -115,6 +282,47 @@ func TestAllAccessorsZeroAlloc(t *testing.T) {
// for callers. AllocsPerRun reports 0 for these on stack-able sites.
{"RewardValidatorTx.TxID", func() { _ = rtx.TxID() }},
{"DisableL1ValidatorTx.ValidationID", func() { _ = dtx.ValidationID() }},
// Batch 2 accessors.
{"BaseTx.NetworkID", func() { _ = btx.NetworkID() }},
{"BaseTx.BlockchainID", func() { _ = btx.BlockchainID() }},
{"BaseTx.Memo", func() { _ = btx.Memo() }},
{"RegisterL1ValidatorTx.Expiry", func() { _ = regTx.Expiry() }},
{"RegisterL1ValidatorTx.ValidationID", func() { _ = regTx.ValidationID() }},
{"SlashValidatorTx.NodeID", func() { _ = slTx.NodeID() }},
{"SlashValidatorTx.Subnet", func() { _ = slTx.Subnet() }},
{"SlashValidatorTx.SlashPercentage", func() { _ = slTx.SlashPercentage() }},
{"TransferChainOwnershipTx.Chain", func() { _ = tcoTx.Chain() }},
{"TransferChainOwnershipTx.OwnerThreshold", func() { _ = tcoTx.OwnerThreshold() }},
{"TransferChainOwnershipTx.OwnerLocktime", func() { _ = tcoTx.OwnerLocktime() }},
{"TransferChainOwnershipTx.OwnerAddress", func() { _ = tcoTx.OwnerAddress() }},
{"RemoveChainValidatorTx.NodeID", func() { _ = rcvTx.NodeID() }},
{"RemoveChainValidatorTx.Subnet", func() { _ = rcvTx.Subnet() }},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
if got := testing.AllocsPerRun(100, c.fn); got != 0 {
t.Fatalf("%s: %.2f allocs/run, want 0", c.name, got)
}
})
}
}
// 192-byte and 48-byte by-value array accessors. Same stack-able-by-return
// guarantee as the 32-byte ones; this test pins it explicitly because the
// Go compiler's escape analysis is sensitive to return size.
func TestRegisterL1ValidatorTxLargeArrayAccessorsZeroAlloc(t *testing.T) {
var blsPub [bls.PublicKeyLen]byte
var pop [bls.SignatureLen]byte
regTx := NewRegisterL1ValidatorTx(ids.ID{1}, blsPub, pop, 1, ids.ID{2})
cases := []struct {
name string
fn func()
}{
{"BLSPublicKey", func() { _ = regTx.BLSPublicKey() }},
{"ProofOfPossession", func() { _ = regTx.ProofOfPossession() }},
{"RemainingBalanceOwnerID", func() { _ = regTx.RemainingBalanceOwnerID() }},
}
for _, c := range cases {
c := c
@@ -8,6 +8,7 @@ import (
"github.com/luxfi/codec"
"github.com/luxfi/codec/linearcodec"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
)
@@ -34,6 +35,39 @@ type legacyDisableL1ValidatorTx struct {
ValidationID ids.ID `serialize:"true"`
}
// Batch 2 legacy stubs — same field shape as the v1 native schemas above.
type legacyBaseTx struct {
NetworkID uint32 `serialize:"true"`
BlockchainID ids.ID `serialize:"true"`
Memo []byte `serialize:"true"`
}
type legacyRegisterL1ValidatorTx struct {
ValidationID ids.ID `serialize:"true"`
BLSPublicKey [bls.PublicKeyLen]byte `serialize:"true"`
ProofOfPossession [bls.SignatureLen]byte `serialize:"true"`
Expiry uint64 `serialize:"true"`
RemainingBalanceOwnerID ids.ID `serialize:"true"`
}
type legacySlashValidatorTx struct {
NodeID ids.NodeID `serialize:"true"`
Subnet ids.ID `serialize:"true"`
SlashPercentage uint32 `serialize:"true"`
}
type legacyTransferChainOwnershipTx struct {
Chain ids.ID `serialize:"true"`
OwnerThreshold uint32 `serialize:"true"`
OwnerLocktime uint64 `serialize:"true"`
OwnerAddress ids.ShortID `serialize:"true"`
}
type legacyRemoveChainValidatorTx struct {
NodeID ids.NodeID `serialize:"true"`
Subnet ids.ID `serialize:"true"`
}
// newLegacyManagerFor builds a codec.Manager with the given types registered.
func newLegacyManagerFor(b *testing.B, types ...interface{}) codec.Manager {
c := linearcodec.NewDefault()
@@ -264,3 +298,301 @@ func BenchmarkBuild_ZAP_DisableL1ValidatorTx(b *testing.B) {
_ = tx.Bytes()
}
}
// ─────────────────────────────────────────────────────────────────────────
// BaseTx (Batch 2)
// ─────────────────────────────────────────────────────────────────────────
var benchBaseTxMemo = []byte("LP-023 batch 2 realistic memo payload for parse/build cost measurement")
func BenchmarkParse_Legacy_BaseTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyBaseTx{})
src := &legacyBaseTx{NetworkID: 1337, BlockchainID: ids.ID{0x11}, Memo: benchBaseTxMemo}
encoded, err := m.Marshal(0, src)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst legacyBaseTx
if _, err := m.Unmarshal(encoded, &dst); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse_ZAP_BaseTx(b *testing.B) {
tx := NewBaseTx(1337, ids.ID{0x11}, benchBaseTxMemo)
buf := tx.Bytes()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := WrapBaseTx(buf); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_Legacy_BaseTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyBaseTx{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := &legacyBaseTx{NetworkID: uint32(i), BlockchainID: ids.ID{byte(i)}, Memo: benchBaseTxMemo}
if _, err := m.Marshal(0, src); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_ZAP_BaseTx(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx := NewBaseTx(uint32(i), ids.ID{byte(i)}, benchBaseTxMemo)
_ = tx.Bytes()
}
}
// ─────────────────────────────────────────────────────────────────────────
// RegisterL1ValidatorTx (Batch 2)
// ─────────────────────────────────────────────────────────────────────────
var (
benchRegBLS [bls.PublicKeyLen]byte
benchRegPoP [bls.SignatureLen]byte
)
func init() {
for i := range benchRegBLS {
benchRegBLS[i] = byte(i + 1)
}
for i := range benchRegPoP {
benchRegPoP[i] = byte(0xff - i)
}
}
func BenchmarkParse_Legacy_RegisterL1ValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyRegisterL1ValidatorTx{})
src := &legacyRegisterL1ValidatorTx{
ValidationID: ids.ID{0xaa},
BLSPublicKey: benchRegBLS,
ProofOfPossession: benchRegPoP,
Expiry: 1_900_000_000,
RemainingBalanceOwnerID: ids.ID{0xbb},
}
encoded, err := m.Marshal(0, src)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst legacyRegisterL1ValidatorTx
if _, err := m.Unmarshal(encoded, &dst); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse_ZAP_RegisterL1ValidatorTx(b *testing.B) {
tx := NewRegisterL1ValidatorTx(ids.ID{0xaa}, benchRegBLS, benchRegPoP, 1_900_000_000, ids.ID{0xbb})
buf := tx.Bytes()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := WrapRegisterL1ValidatorTx(buf); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_Legacy_RegisterL1ValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyRegisterL1ValidatorTx{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := &legacyRegisterL1ValidatorTx{
ValidationID: ids.ID{byte(i)},
BLSPublicKey: benchRegBLS,
ProofOfPossession: benchRegPoP,
Expiry: uint64(i),
RemainingBalanceOwnerID: ids.ID{byte(i)},
}
if _, err := m.Marshal(0, src); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_ZAP_RegisterL1ValidatorTx(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx := NewRegisterL1ValidatorTx(ids.ID{byte(i)}, benchRegBLS, benchRegPoP, uint64(i), ids.ID{byte(i)})
_ = tx.Bytes()
}
}
// ─────────────────────────────────────────────────────────────────────────
// SlashValidatorTx (Batch 2)
// ─────────────────────────────────────────────────────────────────────────
func BenchmarkParse_Legacy_SlashValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacySlashValidatorTx{})
src := &legacySlashValidatorTx{NodeID: ids.NodeID{0xa1}, Subnet: ids.ID{0xa2}, SlashPercentage: 100_000}
encoded, err := m.Marshal(0, src)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst legacySlashValidatorTx
if _, err := m.Unmarshal(encoded, &dst); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse_ZAP_SlashValidatorTx(b *testing.B) {
tx := NewSlashValidatorTx(ids.NodeID{0xa1}, ids.ID{0xa2}, 100_000)
buf := tx.Bytes()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := WrapSlashValidatorTx(buf); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_Legacy_SlashValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacySlashValidatorTx{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := &legacySlashValidatorTx{NodeID: ids.NodeID{byte(i)}, Subnet: ids.ID{byte(i)}, SlashPercentage: uint32(i)}
if _, err := m.Marshal(0, src); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_ZAP_SlashValidatorTx(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx := NewSlashValidatorTx(ids.NodeID{byte(i)}, ids.ID{byte(i)}, uint32(i))
_ = tx.Bytes()
}
}
// ─────────────────────────────────────────────────────────────────────────
// TransferChainOwnershipTx (Batch 2)
// ─────────────────────────────────────────────────────────────────────────
func BenchmarkParse_Legacy_TransferChainOwnershipTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyTransferChainOwnershipTx{})
src := &legacyTransferChainOwnershipTx{Chain: ids.ID{0xc0}, OwnerThreshold: 1, OwnerLocktime: 0, OwnerAddress: ids.ShortID{0xbe, 0xef}}
encoded, err := m.Marshal(0, src)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst legacyTransferChainOwnershipTx
if _, err := m.Unmarshal(encoded, &dst); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse_ZAP_TransferChainOwnershipTx(b *testing.B) {
tx := NewTransferChainOwnershipTx(ids.ID{0xc0}, 1, 0, ids.ShortID{0xbe, 0xef})
buf := tx.Bytes()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := WrapTransferChainOwnershipTx(buf); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_Legacy_TransferChainOwnershipTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyTransferChainOwnershipTx{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := &legacyTransferChainOwnershipTx{Chain: ids.ID{byte(i)}, OwnerThreshold: uint32(i), OwnerLocktime: uint64(i), OwnerAddress: ids.ShortID{byte(i)}}
if _, err := m.Marshal(0, src); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_ZAP_TransferChainOwnershipTx(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx := NewTransferChainOwnershipTx(ids.ID{byte(i)}, uint32(i), uint64(i), ids.ShortID{byte(i)})
_ = tx.Bytes()
}
}
// ─────────────────────────────────────────────────────────────────────────
// RemoveChainValidatorTx (Batch 2)
// ─────────────────────────────────────────────────────────────────────────
func BenchmarkParse_Legacy_RemoveChainValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyRemoveChainValidatorTx{})
src := &legacyRemoveChainValidatorTx{NodeID: ids.NodeID{0x10}, Subnet: ids.ID{0xfa}}
encoded, err := m.Marshal(0, src)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var dst legacyRemoveChainValidatorTx
if _, err := m.Unmarshal(encoded, &dst); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkParse_ZAP_RemoveChainValidatorTx(b *testing.B) {
tx := NewRemoveChainValidatorTx(ids.NodeID{0x10}, ids.ID{0xfa})
buf := tx.Bytes()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := WrapRemoveChainValidatorTx(buf); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_Legacy_RemoveChainValidatorTx(b *testing.B) {
m := newLegacyManagerFor(b, &legacyRemoveChainValidatorTx{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
src := &legacyRemoveChainValidatorTx{NodeID: ids.NodeID{byte(i)}, Subnet: ids.ID{byte(i)}}
if _, err := m.Marshal(0, src); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBuild_ZAP_RemoveChainValidatorTx(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tx := NewRemoveChainValidatorTx(ids.NodeID{byte(i)}, ids.ID{byte(i)})
_ = tx.Bytes()
}
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// BaseTx v1 schema — NetworkID + BlockchainID + Memo. The Outs/Ins fields of
// the legacy lux.BaseTx are variable-length nested-object lists and need a
// proper ZAP list schema; they ship in batch 3. This v1 form is the metadata
// envelope every higher-level platformvm tx embeds.
//
// Fixed-section layout (size 44 bytes):
//
// NetworkID uint32 @ 0
// BlockchainID 32B @ 4
// Memo bytes @ 36 (offset+length pair, 8 bytes)
const (
SchemaVersionBaseTx uint16 = 2
OffsetBaseTx_NetworkID = 0 // uint32
OffsetBaseTx_BlockchainID = 4 // ids.ID (32 bytes)
OffsetBaseTx_Memo = 36 // bytes (offset+length, 8 bytes)
SizeBaseTx = 44
)
// BaseTx is a zero-copy typed accessor over a ZAP buffer carrying a v1 BaseTx.
type BaseTx struct {
msg *zap.Message
obj zap.Object
}
// NetworkID returns the network ID the tx targets.
func (t BaseTx) NetworkID() uint32 {
return t.obj.Uint32(OffsetBaseTx_NetworkID)
}
// BlockchainID returns the chain ID the tx executes against.
func (t BaseTx) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetBaseTx_BlockchainID + i)
}
return out
}
// Memo returns the memo bytes. Zero-copy: the returned slice aliases the
// underlying ZAP buffer. Callers MUST NOT mutate it.
func (t BaseTx) Memo() []byte {
return t.obj.Bytes(OffsetBaseTx_Memo)
}
func (t BaseTx) Bytes() []byte { return t.msg.Bytes() }
func (t BaseTx) IsZero() bool { return t.msg == nil }
// WrapBaseTx parses a ZAP buffer into a typed BaseTx accessor.
func WrapBaseTx(b []byte) (BaseTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return BaseTx{}, err
}
return BaseTx{msg: msg, obj: msg.Root()}, nil
}
// NewBaseTx builds a v1 BaseTx into a fresh ZAP buffer. Memo may be nil.
func NewBaseTx(networkID uint32, blockchainID ids.ID, memo []byte) BaseTx {
b := zap.NewBuilder(zap.HeaderSize + 16 + SizeBaseTx + len(memo))
ob := b.StartObject(SizeBaseTx)
ob.SetUint32(OffsetBaseTx_NetworkID, networkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetBaseTx_BlockchainID+i, blockchainID[i])
}
ob.SetBytes(OffsetBaseTx_Memo, memo)
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return BaseTx{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,122 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// RegisterL1ValidatorTx v1 schema — ValidationID + Signer (BLS pubkey + PoP) +
// Expiry + RemainingBalanceOwnerID. The legacy struct's Message is a Warp
// payload (variable-length addressed-call envelope) and RemainingBalanceOwner
// is an OutputOwners with a variable AddressIDs list — both ship as proper
// list/object schemas in batch 3. This v1 fixes the foundational identity
// fields validators look up on every block.
//
// Fixed-section layout (size 192 bytes):
//
// ValidationID 32B @ 0
// BLSPublicKey 48B @ 32 (bls.PublicKeyLen)
// ProofOfPossession 96B @ 80 (bls.SignatureLen)
// Expiry uint64 @ 176
// RemainingBalanceOwnerID ids.ID @ 184 (placeholder 32B, full Owner in batch 3)
const (
SchemaVersionRegisterL1ValidatorTx uint16 = 2
OffsetRegisterL1ValidatorTx_ValidationID = 0
OffsetRegisterL1ValidatorTx_BLSPublicKey = 32
OffsetRegisterL1ValidatorTx_ProofOfPossession = OffsetRegisterL1ValidatorTx_BLSPublicKey + bls.PublicKeyLen
OffsetRegisterL1ValidatorTx_Expiry = OffsetRegisterL1ValidatorTx_ProofOfPossession + bls.SignatureLen
OffsetRegisterL1ValidatorTx_RemainingBalanceOwnerID = OffsetRegisterL1ValidatorTx_Expiry + 8
SizeRegisterL1ValidatorTx = OffsetRegisterL1ValidatorTx_RemainingBalanceOwnerID + 32
)
type RegisterL1ValidatorTx struct {
msg *zap.Message
obj zap.Object
}
func (t RegisterL1ValidatorTx) ValidationID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetRegisterL1ValidatorTx_ValidationID + i)
}
return out
}
// BLSPublicKey returns the compressed-G1 BLS public key (48 bytes) registered
// for this validator. Zero-copy by-value return; backing storage is the ZAP
// buffer.
func (t RegisterL1ValidatorTx) BLSPublicKey() [bls.PublicKeyLen]byte {
var out [bls.PublicKeyLen]byte
for i := 0; i < bls.PublicKeyLen; i++ {
out[i] = t.obj.Uint8(OffsetRegisterL1ValidatorTx_BLSPublicKey + i)
}
return out
}
// ProofOfPossession returns the BLS signature over the public key proving
// the registrant controls the matching secret key.
func (t RegisterL1ValidatorTx) ProofOfPossession() [bls.SignatureLen]byte {
var out [bls.SignatureLen]byte
for i := 0; i < bls.SignatureLen; i++ {
out[i] = t.obj.Uint8(OffsetRegisterL1ValidatorTx_ProofOfPossession + i)
}
return out
}
func (t RegisterL1ValidatorTx) Expiry() uint64 {
return t.obj.Uint64(OffsetRegisterL1ValidatorTx_Expiry)
}
// RemainingBalanceOwnerID is a v1 placeholder for the OutputOwners pointer.
// Batch 3 replaces this with a full OutputOwners nested-object schema
// (threshold + AddressIDs list).
func (t RegisterL1ValidatorTx) RemainingBalanceOwnerID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetRegisterL1ValidatorTx_RemainingBalanceOwnerID + i)
}
return out
}
func (t RegisterL1ValidatorTx) Bytes() []byte { return t.msg.Bytes() }
func (t RegisterL1ValidatorTx) IsZero() bool { return t.msg == nil }
func WrapRegisterL1ValidatorTx(b []byte) (RegisterL1ValidatorTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return RegisterL1ValidatorTx{}, err
}
return RegisterL1ValidatorTx{msg: msg, obj: msg.Root()}, nil
}
func NewRegisterL1ValidatorTx(
validationID ids.ID,
blsPublicKey [bls.PublicKeyLen]byte,
proofOfPossession [bls.SignatureLen]byte,
expiry uint64,
remainingBalanceOwnerID ids.ID,
) RegisterL1ValidatorTx {
b := zap.NewBuilder(zap.HeaderSize + 16 + SizeRegisterL1ValidatorTx)
ob := b.StartObject(SizeRegisterL1ValidatorTx)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetRegisterL1ValidatorTx_ValidationID+i, validationID[i])
}
for i := 0; i < bls.PublicKeyLen; i++ {
ob.SetUint8(OffsetRegisterL1ValidatorTx_BLSPublicKey+i, blsPublicKey[i])
}
for i := 0; i < bls.SignatureLen; i++ {
ob.SetUint8(OffsetRegisterL1ValidatorTx_ProofOfPossession+i, proofOfPossession[i])
}
ob.SetUint64(OffsetRegisterL1ValidatorTx_Expiry, expiry)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetRegisterL1ValidatorTx_RemainingBalanceOwnerID+i, remainingBalanceOwnerID[i])
}
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return RegisterL1ValidatorTx{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,73 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// RemoveChainValidatorTx v1 schema — NodeID (20B) + Subnet (32B).
//
// Legacy struct also has ChainAuth (verify.Verifiable credential) which lives
// outside the unsigned-tx bytes in the signed wrapper — the unsigned encoding
// here pins only the identity tuple the executor matches against state.
//
// Fixed-section layout (size 52 bytes):
//
// NodeID 20B @ 0 (ids.NodeID)
// Subnet 32B @ 20 (ids.ID)
const (
SchemaVersionRemoveChainValidatorTx uint16 = 2
OffsetRemoveChainValidatorTx_NodeID = 0
OffsetRemoveChainValidatorTx_Subnet = ids.ShortIDLen
SizeRemoveChainValidatorTx = OffsetRemoveChainValidatorTx_Subnet + 32
)
type RemoveChainValidatorTx struct {
msg *zap.Message
obj zap.Object
}
func (t RemoveChainValidatorTx) NodeID() ids.NodeID {
var out ids.NodeID
for i := 0; i < ids.ShortIDLen; i++ {
out[i] = t.obj.Uint8(OffsetRemoveChainValidatorTx_NodeID + i)
}
return out
}
func (t RemoveChainValidatorTx) Subnet() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetRemoveChainValidatorTx_Subnet + i)
}
return out
}
func (t RemoveChainValidatorTx) Bytes() []byte { return t.msg.Bytes() }
func (t RemoveChainValidatorTx) IsZero() bool { return t.msg == nil }
func WrapRemoveChainValidatorTx(b []byte) (RemoveChainValidatorTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return RemoveChainValidatorTx{}, err
}
return RemoveChainValidatorTx{msg: msg, obj: msg.Root()}, nil
}
func NewRemoveChainValidatorTx(nodeID ids.NodeID, subnet ids.ID) RemoveChainValidatorTx {
b := zap.NewBuilder(zap.HeaderSize + 16 + SizeRemoveChainValidatorTx)
ob := b.StartObject(SizeRemoveChainValidatorTx)
for i := 0; i < ids.ShortIDLen; i++ {
ob.SetUint8(OffsetRemoveChainValidatorTx_NodeID+i, nodeID[i])
}
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetRemoveChainValidatorTx_Subnet+i, subnet[i])
}
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return RemoveChainValidatorTx{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,82 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// SlashValidatorTx v1 schema — NodeID (20B) + Subnet (32B).
//
// The legacy struct also carries an Evidence object (height, type, two
// message/signature pairs) and SlashPercentage. Evidence is variable-length
// nested bytes and ships with the proper list/object schema in batch 3.
// SlashPercentage is included here as a uint32 because it's fixed-size and
// the executor needs it before evaluating the evidence.
//
// Fixed-section layout (size 56 bytes):
//
// NodeID 20B @ 0 (ids.NodeID, ShortIDLen)
// Subnet 32B @ 20 (ids.ID)
// SlashPercentage uint32 @ 52
const (
SchemaVersionSlashValidatorTx uint16 = 2
OffsetSlashValidatorTx_NodeID = 0
OffsetSlashValidatorTx_Subnet = ids.ShortIDLen
OffsetSlashValidatorTx_SlashPercentage = OffsetSlashValidatorTx_Subnet + 32
SizeSlashValidatorTx = OffsetSlashValidatorTx_SlashPercentage + 4
)
type SlashValidatorTx struct {
msg *zap.Message
obj zap.Object
}
func (t SlashValidatorTx) NodeID() ids.NodeID {
var out ids.NodeID
for i := 0; i < ids.ShortIDLen; i++ {
out[i] = t.obj.Uint8(OffsetSlashValidatorTx_NodeID + i)
}
return out
}
func (t SlashValidatorTx) Subnet() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetSlashValidatorTx_Subnet + i)
}
return out
}
func (t SlashValidatorTx) SlashPercentage() uint32 {
return t.obj.Uint32(OffsetSlashValidatorTx_SlashPercentage)
}
func (t SlashValidatorTx) Bytes() []byte { return t.msg.Bytes() }
func (t SlashValidatorTx) IsZero() bool { return t.msg == nil }
func WrapSlashValidatorTx(b []byte) (SlashValidatorTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return SlashValidatorTx{}, err
}
return SlashValidatorTx{msg: msg, obj: msg.Root()}, nil
}
func NewSlashValidatorTx(nodeID ids.NodeID, subnet ids.ID, slashPercentage uint32) SlashValidatorTx {
b := zap.NewBuilder(zap.HeaderSize + 16 + SizeSlashValidatorTx)
ob := b.StartObject(SizeSlashValidatorTx)
for i := 0; i < ids.ShortIDLen; i++ {
ob.SetUint8(OffsetSlashValidatorTx_NodeID+i, nodeID[i])
}
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetSlashValidatorTx_Subnet+i, subnet[i])
}
ob.SetUint32(OffsetSlashValidatorTx_SlashPercentage, slashPercentage)
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return SlashValidatorTx{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,101 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// TransferChainOwnershipTx v1 schema — Chain + a single-address Owner stub.
//
// The legacy struct's Owner is an fx.Owner interface — almost always
// *secp256k1fx.OutputOwners with {Threshold uint32, Locktime uint64,
// AddressIDs []ids.ShortID}. The variable AddressIDs list ships with the
// proper schema in batch 3. The v1 form pins the single most common
// configuration: threshold-1 / locktime-0 / single owner address. Callers
// that need richer Owner shapes still have the legacy encoder available
// behind LUXD_ENABLE_LEGACY_CODEC.
//
// The legacy struct also has ChainAuth (verify.Verifiable, a credential
// proof produced by the signer). This is itself signature material that
// rides outside the unsigned-tx bytes in the signed wrapper, so it stays
// in the signed-tx schema (separate from the unsigned encoding here).
//
// Fixed-section layout (size 56 bytes):
//
// Chain 32B @ 0 (ids.ID)
// OwnerThreshold uint32 @ 32
// OwnerLocktime uint64 @ 40
// OwnerAddress 20B @ 48 (ids.ShortID; single owner v1 stub)
const (
SchemaVersionTransferChainOwnershipTx uint16 = 2
OffsetTransferChainOwnershipTx_Chain = 0
OffsetTransferChainOwnershipTx_OwnerThreshold = 32
OffsetTransferChainOwnershipTx_OwnerLocktime = 40
OffsetTransferChainOwnershipTx_OwnerAddress = 48
SizeTransferChainOwnershipTx = OffsetTransferChainOwnershipTx_OwnerAddress + ids.ShortIDLen
)
type TransferChainOwnershipTx struct {
msg *zap.Message
obj zap.Object
}
func (t TransferChainOwnershipTx) Chain() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetTransferChainOwnershipTx_Chain + i)
}
return out
}
func (t TransferChainOwnershipTx) OwnerThreshold() uint32 {
return t.obj.Uint32(OffsetTransferChainOwnershipTx_OwnerThreshold)
}
func (t TransferChainOwnershipTx) OwnerLocktime() uint64 {
return t.obj.Uint64(OffsetTransferChainOwnershipTx_OwnerLocktime)
}
func (t TransferChainOwnershipTx) OwnerAddress() ids.ShortID {
var out ids.ShortID
for i := 0; i < ids.ShortIDLen; i++ {
out[i] = t.obj.Uint8(OffsetTransferChainOwnershipTx_OwnerAddress + i)
}
return out
}
func (t TransferChainOwnershipTx) Bytes() []byte { return t.msg.Bytes() }
func (t TransferChainOwnershipTx) IsZero() bool { return t.msg == nil }
func WrapTransferChainOwnershipTx(b []byte) (TransferChainOwnershipTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return TransferChainOwnershipTx{}, err
}
return TransferChainOwnershipTx{msg: msg, obj: msg.Root()}, nil
}
func NewTransferChainOwnershipTx(
chain ids.ID,
ownerThreshold uint32,
ownerLocktime uint64,
ownerAddress ids.ShortID,
) TransferChainOwnershipTx {
b := zap.NewBuilder(zap.HeaderSize + 16 + SizeTransferChainOwnershipTx)
ob := b.StartObject(SizeTransferChainOwnershipTx)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetTransferChainOwnershipTx_Chain+i, chain[i])
}
ob.SetUint32(OffsetTransferChainOwnershipTx_OwnerThreshold, ownerThreshold)
ob.SetUint64(OffsetTransferChainOwnershipTx_OwnerLocktime, ownerLocktime)
for i := 0; i < ids.ShortIDLen; i++ {
ob.SetUint8(OffsetTransferChainOwnershipTx_OwnerAddress+i, ownerAddress[i])
}
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return TransferChainOwnershipTx{msg: msg, obj: msg.Root()}
}