platformvm/txs/zap_native: 5 batch-3 tx types composing the new primitives (LP-023 Phase 1)

TxKindBaseFull                    = 11  → BaseTxFull
  TxKindAddPermissionlessValidator  = 12  → AddPermissionlessValidatorTx
  TxKindImport                      = 13  → ImportTx
  TxKindExport                      = 14  → ExportTx
  TxKindCreateChain                 = 15  → CreateChainTx

Sizes (fixed section, schema v3):
  BaseTxFull                     =  85
  ImportTx                       = 125
  ExportTx                       = 125
  AddPermissionlessValidatorTx   = 381
  CreateChainTx                  = 221

Design highlights:

- BaseTxFull is the real P-chain spending envelope (Outs + Ins +
  Credentials + 2 shared arrays + Memo). The batch-2 BaseTx
  (TxKindBase) remains as the minimal metadata envelope for places
  that need only {NetworkID, BlockchainID, Memo} without spending
  state. Both kinds are first-class; they are NOT alternatives.

- Import / Export use a SINGLE combined Ins (or Outs) list with a
  header marker (ImportedInsStart/Count or ExportedOutsStart/Count)
  identifying the cross-chain slice. This collapses what would have
  been two parallel sig-index arrays into one shared array — fewer
  pointer pairs, no rebase bookkeeping. The slice-based indexing is
  byte-identical for the imported/exported half because they
  reference the same shared array.

- AddPermissionlessValidatorTx carries two single-address Owner stubs
  (validation rewards, delegation rewards) inline in the fixed
  section. The same OwnerStub type underlies CreateChainTx.Owner.
  Multi-address Owner ships in batch 4 along with the AddressList
  primitive; current callers needing multi-addr flow through the
  legacy codec gate.

- CreateChainTx embeds a 32-byte WarpMessageHash (sha256 of the
  originating Warp commit; zero when the chain is being created
  directly without a cross-network commit). The Warp message BODY
  is NOT embedded — it lives in the signed Warp envelope outside
  this tx. Hash-only embedding keeps the unsigned-tx bytes stable
  and the Warp envelope separately verifiable.

All five tx types follow the v3 invariant: TxKind@0, Wrap* rejects
mismatched discriminator with ErrWrongTxKind. Cross-confusion test
expansion verifies the new tx types in pairwise rejection (7
additional scenarios cover the batch 1+2+3 surface).

Deferred to batch 4 (no new primitives needed): the remaining ~18
classical platformvm tx types (CreateNetwork, AddDelegator,
AddPermissionlessDelegator, etc.) reuse the existing batch-3
primitives; their landings are mechanical.

go test -race ./vms/platformvm/txs/zap_native/...
  ok 	github.com/luxfi/node/vms/platformvm/txs/zap_native	1.488s
This commit is contained in:
Hanzo AI
2026-06-02 14:35:03 -07:00
parent 197dac4245
commit b6ce3a2159
7 changed files with 1327 additions and 11 deletions
@@ -0,0 +1,283 @@
// 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"
)
// AddPermissionlessValidatorTx is the v3 register-permissionless-validator
// envelope. It carries the BaseTxFull spending state, the validator
// identity (NodeID + bond stake metadata + signer), and two nested Owner
// stubs (validation rewards owner + delegation rewards owner). Both owners
// are single-address stubs in v3 (matching TransferChainOwnershipTx /
// OutputList semantics). Multi-address owners flow through legacy.
//
// Fixed-section layout (size 269 bytes):
//
// TxKind uint8 @ 0
// NetworkID uint32 @ 1
// BlockchainID 32B @ 5
// OutsList 8B @ 37
// InsList 8B @ 45
// CredsList 8B @ 53
// SigIndicesArr 8B @ 61
// SigArr 8B @ 69
// Memo 8B @ 77
// NodeID 20B @ 85 (ids.NodeID)
// StakeStart uint64 @ 105
// StakeEnd uint64 @ 113
// StakeWeight uint64 @ 121
// StakeAssetID 32B @ 129
// StakeAmount uint64 @ 161
// BLSPublicKey 48B @ 169 (bls.PublicKeyLen)
// BLSProofOfPossession 96B @ 217 (bls.SignatureLen)
// ValidationRewardsOwnerThresh uint32 @ 313
// ValidationRewardsOwnerLT uint64 @ 317
// ValidationRewardsOwnerAddr 20B @ 325
// DelegationRewardsOwnerThresh uint32 @ 345
// DelegationRewardsOwnerLT uint64 @ 349
// DelegationRewardsOwnerAddr 20B @ 357
// DelegationShares uint32 @ 377
// Size = 381
const (
OffsetAPVTx_NetworkID = 1
OffsetAPVTx_BlockchainID = 5
OffsetAPVTx_OutsList = 37
OffsetAPVTx_InsList = 45
OffsetAPVTx_CredsList = 53
OffsetAPVTx_SigIndicesArr = 61
OffsetAPVTx_SigArr = 69
OffsetAPVTx_Memo = 77
OffsetAPVTx_NodeID = 85
OffsetAPVTx_StakeStart = 105
OffsetAPVTx_StakeEnd = 113
OffsetAPVTx_StakeWeight = 121
OffsetAPVTx_StakeAssetID = 129
OffsetAPVTx_StakeAmount = 161
OffsetAPVTx_BLSPublicKey = 169
OffsetAPVTx_BLSProofOfPossession = OffsetAPVTx_BLSPublicKey + bls.PublicKeyLen
OffsetAPVTx_ValidationRewardsOwnerThreshold = OffsetAPVTx_BLSProofOfPossession + bls.SignatureLen
OffsetAPVTx_ValidationRewardsOwnerLocktime = OffsetAPVTx_ValidationRewardsOwnerThreshold + 4
OffsetAPVTx_ValidationRewardsOwnerAddress = OffsetAPVTx_ValidationRewardsOwnerLocktime + 8
OffsetAPVTx_DelegationRewardsOwnerThreshold = OffsetAPVTx_ValidationRewardsOwnerAddress + ids.ShortIDLen
OffsetAPVTx_DelegationRewardsOwnerLocktime = OffsetAPVTx_DelegationRewardsOwnerThreshold + 4
OffsetAPVTx_DelegationRewardsOwnerAddress = OffsetAPVTx_DelegationRewardsOwnerLocktime + 8
OffsetAPVTx_DelegationShares = OffsetAPVTx_DelegationRewardsOwnerAddress + ids.ShortIDLen
SizeAddPermissionlessValidatorTx = OffsetAPVTx_DelegationShares + 4
)
// AddPermissionlessValidatorTx is the zero-copy accessor.
type AddPermissionlessValidatorTx struct {
msg *zap.Message
obj zap.Object
}
func (t AddPermissionlessValidatorTx) NetworkID() uint32 {
return t.obj.Uint32(OffsetAPVTx_NetworkID)
}
func (t AddPermissionlessValidatorTx) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetAPVTx_BlockchainID + i)
}
return out
}
func (t AddPermissionlessValidatorTx) Outs() OutputList {
return OutputListView(t.obj, OffsetAPVTx_OutsList)
}
func (t AddPermissionlessValidatorTx) Ins() InputList {
return InputListView(t.obj, OffsetAPVTx_InsList)
}
func (t AddPermissionlessValidatorTx) Credentials() CredentialList {
return CredentialListView(t.obj, OffsetAPVTx_CredsList)
}
func (t AddPermissionlessValidatorTx) SigIndicesArray() SigIndicesArray {
return SigIndicesArrayView(t.obj, OffsetAPVTx_SigIndicesArr)
}
func (t AddPermissionlessValidatorTx) SignatureArray() SignatureArray {
return SignatureArrayView(t.obj, OffsetAPVTx_SigArr)
}
func (t AddPermissionlessValidatorTx) Memo() []byte {
return t.obj.Bytes(OffsetAPVTx_Memo)
}
func (t AddPermissionlessValidatorTx) NodeID() ids.NodeID {
var out ids.NodeID
for i := 0; i < ids.ShortIDLen; i++ {
out[i] = t.obj.Uint8(OffsetAPVTx_NodeID + i)
}
return out
}
func (t AddPermissionlessValidatorTx) StakeStart() uint64 {
return t.obj.Uint64(OffsetAPVTx_StakeStart)
}
func (t AddPermissionlessValidatorTx) StakeEnd() uint64 {
return t.obj.Uint64(OffsetAPVTx_StakeEnd)
}
func (t AddPermissionlessValidatorTx) StakeWeight() uint64 {
return t.obj.Uint64(OffsetAPVTx_StakeWeight)
}
func (t AddPermissionlessValidatorTx) StakeAssetID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetAPVTx_StakeAssetID + i)
}
return out
}
func (t AddPermissionlessValidatorTx) StakeAmount() uint64 {
return t.obj.Uint64(OffsetAPVTx_StakeAmount)
}
func (t AddPermissionlessValidatorTx) BLSPublicKey() [bls.PublicKeyLen]byte {
var out [bls.PublicKeyLen]byte
for i := 0; i < bls.PublicKeyLen; i++ {
out[i] = t.obj.Uint8(OffsetAPVTx_BLSPublicKey + i)
}
return out
}
func (t AddPermissionlessValidatorTx) BLSProofOfPossession() [bls.SignatureLen]byte {
var out [bls.SignatureLen]byte
for i := 0; i < bls.SignatureLen; i++ {
out[i] = t.obj.Uint8(OffsetAPVTx_BLSProofOfPossession + i)
}
return out
}
// ValidationRewardsOwner returns the (threshold, locktime, address) tuple
// for the validation-rewards owner. Single-address stub form (v3).
func (t AddPermissionlessValidatorTx) ValidationRewardsOwner() (uint32, uint64, ids.ShortID) {
threshold := t.obj.Uint32(OffsetAPVTx_ValidationRewardsOwnerThreshold)
locktime := t.obj.Uint64(OffsetAPVTx_ValidationRewardsOwnerLocktime)
var addr ids.ShortID
for i := 0; i < ids.ShortIDLen; i++ {
addr[i] = t.obj.Uint8(OffsetAPVTx_ValidationRewardsOwnerAddress + i)
}
return threshold, locktime, addr
}
// DelegationRewardsOwner mirrors ValidationRewardsOwner for the delegation
// rewards owner stub.
func (t AddPermissionlessValidatorTx) DelegationRewardsOwner() (uint32, uint64, ids.ShortID) {
threshold := t.obj.Uint32(OffsetAPVTx_DelegationRewardsOwnerThreshold)
locktime := t.obj.Uint64(OffsetAPVTx_DelegationRewardsOwnerLocktime)
var addr ids.ShortID
for i := 0; i < ids.ShortIDLen; i++ {
addr[i] = t.obj.Uint8(OffsetAPVTx_DelegationRewardsOwnerAddress + i)
}
return threshold, locktime, addr
}
// DelegationShares returns the delegation-shares percentage in units of
// reward.PercentDenominator.
func (t AddPermissionlessValidatorTx) DelegationShares() uint32 {
return t.obj.Uint32(OffsetAPVTx_DelegationShares)
}
func (t AddPermissionlessValidatorTx) Bytes() []byte { return t.msg.Bytes() }
func (t AddPermissionlessValidatorTx) IsZero() bool { return t.msg == nil }
func WrapAddPermissionlessValidatorTx(b []byte) (AddPermissionlessValidatorTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return AddPermissionlessValidatorTx{}, err
}
obj := msg.Root()
if TxKind(obj.Uint8(OffsetTxKind)) != TxKindAddPermissionlessValidator {
return AddPermissionlessValidatorTx{}, ErrWrongTxKind
}
return AddPermissionlessValidatorTx{msg: msg, obj: obj}, nil
}
// OwnerStub is the v3 single-address owner stub used by APV's two
// rewards-owner fields.
type OwnerStub struct {
Threshold uint32
Locktime uint64
Address ids.ShortID
}
// AddPermissionlessValidatorTxInput is the constructor input.
type AddPermissionlessValidatorTxInput struct {
NetworkID uint32
BlockchainID ids.ID
Outs []OutputListEntry
Ins []InputListEntry
Credentials []CredentialListEntry
Memo []byte
NodeID ids.NodeID
StakeStart uint64
StakeEnd uint64
StakeWeight uint64
StakeAssetID ids.ID
StakeAmount uint64
BLSPublicKey [bls.PublicKeyLen]byte
BLSProofOfPossession [bls.SignatureLen]byte
ValidationRewardsOwner OwnerStub
DelegationRewardsOwner OwnerStub
DelegationShares uint32
}
func NewAddPermissionlessValidatorTx(in AddPermissionlessValidatorTxInput) AddPermissionlessValidatorTx {
cap := zap.HeaderSize + 16 + SizeAddPermissionlessValidatorTx
cap += len(in.Outs) * SizeTransferableOutput
cap += len(in.Ins) * SizeTransferableInput
cap += len(in.Credentials) * SizeCredential
cap += len(in.Memo)
b := zap.NewBuilder(cap)
outsOff, outsCount := WriteOutputList(b, in.Outs)
insOff, insCount, sigIndices := WriteInputList(b, in.Ins)
credsOff, credsCount, sigBlobs := WriteCredentialList(b, in.Credentials)
sigIdxArrOff, sigIdxArrCount := WriteSigIndicesArray(b, sigIndices)
sigArrOff, sigArrCount := WriteSignatureArray(b, sigBlobs)
ob := b.StartObject(SizeAddPermissionlessValidatorTx)
ob.SetUint8(OffsetTxKind, uint8(TxKindAddPermissionlessValidator))
ob.SetUint32(OffsetAPVTx_NetworkID, in.NetworkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetAPVTx_BlockchainID+i, in.BlockchainID[i])
ob.SetUint8(OffsetAPVTx_StakeAssetID+i, in.StakeAssetID[i])
}
ob.SetList(OffsetAPVTx_OutsList, outsOff, outsCount)
ob.SetList(OffsetAPVTx_InsList, insOff, insCount)
ob.SetList(OffsetAPVTx_CredsList, credsOff, credsCount)
ob.SetList(OffsetAPVTx_SigIndicesArr, sigIdxArrOff, sigIdxArrCount)
ob.SetList(OffsetAPVTx_SigArr, sigArrOff, sigArrCount)
ob.SetBytes(OffsetAPVTx_Memo, in.Memo)
for i := 0; i < ids.ShortIDLen; i++ {
ob.SetUint8(OffsetAPVTx_NodeID+i, in.NodeID[i])
ob.SetUint8(OffsetAPVTx_ValidationRewardsOwnerAddress+i, in.ValidationRewardsOwner.Address[i])
ob.SetUint8(OffsetAPVTx_DelegationRewardsOwnerAddress+i, in.DelegationRewardsOwner.Address[i])
}
ob.SetUint64(OffsetAPVTx_StakeStart, in.StakeStart)
ob.SetUint64(OffsetAPVTx_StakeEnd, in.StakeEnd)
ob.SetUint64(OffsetAPVTx_StakeWeight, in.StakeWeight)
ob.SetUint64(OffsetAPVTx_StakeAmount, in.StakeAmount)
for i := 0; i < bls.PublicKeyLen; i++ {
ob.SetUint8(OffsetAPVTx_BLSPublicKey+i, in.BLSPublicKey[i])
}
for i := 0; i < bls.SignatureLen; i++ {
ob.SetUint8(OffsetAPVTx_BLSProofOfPossession+i, in.BLSProofOfPossession[i])
}
ob.SetUint32(OffsetAPVTx_ValidationRewardsOwnerThreshold, in.ValidationRewardsOwner.Threshold)
ob.SetUint64(OffsetAPVTx_ValidationRewardsOwnerLocktime, in.ValidationRewardsOwner.Locktime)
ob.SetUint32(OffsetAPVTx_DelegationRewardsOwnerThreshold, in.DelegationRewardsOwner.Threshold)
ob.SetUint64(OffsetAPVTx_DelegationRewardsOwnerLocktime, in.DelegationRewardsOwner.Locktime)
ob.SetUint32(OffsetAPVTx_DelegationShares, in.DelegationShares)
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return AddPermissionlessValidatorTx{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,178 @@
// 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"
)
// BaseTxFull is the full P-chain spending envelope: NetworkID + BlockchainID
// + Outs + Ins + Credentials + Memo. Higher-level tx types (Import/Export/
// AddPermissionlessValidator/CreateChain) embed this. The minimal
// metadata-only BaseTx (TxKindBase) remains for places that need only
// {NetworkID, BlockchainID, Memo} without spending state.
//
// Fixed-section layout (size 73 bytes; uint64 reads alignment-tolerant):
//
// TxKind uint8 @ 0
// NetworkID uint32 @ 1
// BlockchainID 32B @ 5
// OutsListRel uint32 @ 37 (list pointer; (offset,count))
// OutsListLen uint32 @ 41
// InsListRel uint32 @ 45
// InsListLen uint32 @ 49
// CredsListRel uint32 @ 53
// CredsListLen uint32 @ 57
// SigIndicesArrRel uint32 @ 61 (shared uint32 array; InputList slices here)
// SigIndicesArrLen uint32 @ 65
// SigArrRel uint32 @ 69 (shared signature blob array; CredentialList slices)
// SigArrLen uint32 @ 73
// Memo bytes @ 77 (relOffset + length, 8 bytes)
// (fixed-section size 85; the 4 list-pointer pairs are 8 bytes each,
// stored as 4-byte rel + 4-byte length consistent with zap.Object.List)
//
// Layout summary: header + 5 lists/arrays (each is a 4-byte relOffset and a
// 4-byte length, 40 bytes total for the 5 pointers) + Memo (8 bytes).
//
// Total fixed section = 1 (TxKind) + 4 (NetworkID) + 32 (BlockchainID) +
// 5*8 (4 lists + 1 array pair) + 8 (Memo) = 85 bytes. But the 5 list+arr
// pointers are split into 5×(4+4) = 40 bytes, beginning at offset 37.
const (
OffsetBaseTxFull_NetworkID = 1
OffsetBaseTxFull_BlockchainID = 5
OffsetBaseTxFull_OutsList = 37 // list pointer (8 bytes)
OffsetBaseTxFull_InsList = 45 // list pointer (8 bytes)
OffsetBaseTxFull_CredsList = 53 // list pointer (8 bytes)
OffsetBaseTxFull_SigIndicesArr = 61 // list pointer (8 bytes)
OffsetBaseTxFull_SigArr = 69 // list pointer (8 bytes)
OffsetBaseTxFull_Memo = 77 // bytes (8 bytes)
SizeBaseTxFull = 85
)
// BaseTxFull is a zero-copy typed accessor over a ZAP buffer carrying a v3
// full P-chain spending tx.
type BaseTxFull struct {
msg *zap.Message
obj zap.Object
}
// NetworkID returns the network ID the tx targets.
func (t BaseTxFull) NetworkID() uint32 {
return t.obj.Uint32(OffsetBaseTxFull_NetworkID)
}
// BlockchainID returns the chain ID the tx executes against.
func (t BaseTxFull) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetBaseTxFull_BlockchainID + i)
}
return out
}
// Outs returns the OutputList view.
func (t BaseTxFull) Outs() OutputList {
return OutputListView(t.obj, OffsetBaseTxFull_OutsList)
}
// Ins returns the InputList view.
func (t BaseTxFull) Ins() InputList {
return InputListView(t.obj, OffsetBaseTxFull_InsList)
}
// Credentials returns the CredentialList view.
func (t BaseTxFull) Credentials() CredentialList {
return CredentialListView(t.obj, OffsetBaseTxFull_CredsList)
}
// SigIndicesArray returns the shared uint32 sig-index array view that the
// InputList entries slice into.
func (t BaseTxFull) SigIndicesArray() SigIndicesArray {
return SigIndicesArrayView(t.obj, OffsetBaseTxFull_SigIndicesArr)
}
// SignatureArray returns the shared signature blob array view that the
// CredentialList entries slice into.
func (t BaseTxFull) SignatureArray() SignatureArray {
return SignatureArrayView(t.obj, OffsetBaseTxFull_SigArr)
}
// Memo returns the memo bytes.
//
// READ-ONLY: aliases the underlying ZAP buffer. Mutation corrupts the
// parsed tx and breaks TxID = hash(buffer). Use append([]byte(nil), m...)
// to take ownership.
func (t BaseTxFull) Memo() []byte {
return t.obj.Bytes(OffsetBaseTxFull_Memo)
}
func (t BaseTxFull) Bytes() []byte { return t.msg.Bytes() }
func (t BaseTxFull) IsZero() bool { return t.msg == nil }
// WrapBaseTxFull parses a ZAP buffer into a typed BaseTxFull accessor.
//
// Returns ErrWrongTxKind if the discriminator does not match TxKindBaseFull.
func WrapBaseTxFull(b []byte) (BaseTxFull, error) {
msg, err := zap.Parse(b)
if err != nil {
return BaseTxFull{}, err
}
obj := msg.Root()
if TxKind(obj.Uint8(OffsetTxKind)) != TxKindBaseFull {
return BaseTxFull{}, ErrWrongTxKind
}
return BaseTxFull{msg: msg, obj: obj}, nil
}
// BaseTxFullInput is the constructor input for NewBaseTxFull. The lists are
// converted to ZAP wire form inside the builder; no separate writer call
// needed by the caller.
type BaseTxFullInput struct {
NetworkID uint32
BlockchainID ids.ID
Outs []OutputListEntry
Ins []InputListEntry
Credentials []CredentialListEntry
Memo []byte
}
// NewBaseTxFull builds a v3 BaseTxFull into a fresh ZAP buffer. Outs/Ins/
// Credentials may each be empty (the corresponding list pointer is
// (0, 0)).
func NewBaseTxFull(in BaseTxFullInput) BaseTxFull {
// Capacity estimate: header + fixed section + list payloads + memo.
cap := zap.HeaderSize + 16 + SizeBaseTxFull
cap += len(in.Outs) * SizeTransferableOutput
cap += len(in.Ins) * SizeTransferableInput
cap += len(in.Credentials) * SizeCredential
cap += len(in.Memo)
b := zap.NewBuilder(cap)
// Write all lists FIRST so they're positioned in the variable section
// before the parent object's fixed section. The parent's list pointers
// will then be backward (negative) — ZAP's signed-int32 list/Object
// decoding accepts that, post-F1.
outsOff, outsCount := WriteOutputList(b, in.Outs)
insOff, insCount, sigIndices := WriteInputList(b, in.Ins)
credsOff, credsCount, sigBlobs := WriteCredentialList(b, in.Credentials)
sigIdxArrOff, sigIdxArrCount := WriteSigIndicesArray(b, sigIndices)
sigArrOff, sigArrCount := WriteSignatureArray(b, sigBlobs)
ob := b.StartObject(SizeBaseTxFull)
ob.SetUint8(OffsetTxKind, uint8(TxKindBaseFull))
ob.SetUint32(OffsetBaseTxFull_NetworkID, in.NetworkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetBaseTxFull_BlockchainID+i, in.BlockchainID[i])
}
ob.SetList(OffsetBaseTxFull_OutsList, outsOff, outsCount)
ob.SetList(OffsetBaseTxFull_InsList, insOff, insCount)
ob.SetList(OffsetBaseTxFull_CredsList, credsOff, credsCount)
ob.SetList(OffsetBaseTxFull_SigIndicesArr, sigIdxArrOff, sigIdxArrCount)
ob.SetList(OffsetBaseTxFull_SigArr, sigArrOff, sigArrCount)
ob.SetBytes(OffsetBaseTxFull_Memo, in.Memo)
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return BaseTxFull{msg: msg, obj: msg.Root()}
}
@@ -0,0 +1,328 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"bytes"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
)
// Helper to build a non-trivial sample outs/ins/creds set used by multiple
// round-trip tests.
func sampleOuts() []OutputListEntry {
return []OutputListEntry{
{AssetID: ids.ID{0xa1}, Amount: 100, Threshold: 1, Locktime: 0, OwnerAddress: ids.ShortID{0x11}},
{AssetID: ids.ID{0xa2}, Amount: 200, Threshold: 1, Locktime: 1_900_000_000, OwnerAddress: ids.ShortID{0x22}},
}
}
func sampleIns() []InputListEntry {
return []InputListEntry{
{TxID: ids.ID{0x01}, OutputIndex: 0, AssetID: ids.ID{0xa1}, Amount: 100, SigIndices: []uint32{0}},
{TxID: ids.ID{0x02}, OutputIndex: 1, AssetID: ids.ID{0xa2}, Amount: 200, SigIndices: []uint32{0, 1}},
}
}
func sampleCreds() []CredentialListEntry {
var s0, s1, s2 [SigBlobSize]byte
for i := range s0 {
s0[i] = 0xA0
s1[i] = 0xB0
s2[i] = 0xC0
}
return []CredentialListEntry{
{Sigs: [][SigBlobSize]byte{s0}},
{Sigs: [][SigBlobSize]byte{s1, s2}},
}
}
func TestBaseTxFullRoundTrip(t *testing.T) {
in := BaseTxFullInput{
NetworkID: 1337,
BlockchainID: ids.ID{0xc1},
Outs: sampleOuts(),
Ins: sampleIns(),
Credentials: sampleCreds(),
Memo: []byte("phase C canary"),
}
tx := NewBaseTxFull(in)
if !IsZAPBytes(tx.Bytes()) {
t.Fatal("BaseTxFull bytes not ZAP-formatted")
}
if tx.NetworkID() != in.NetworkID {
t.Errorf("NetworkID round-trip")
}
if tx.BlockchainID() != in.BlockchainID {
t.Errorf("BlockchainID round-trip")
}
if !bytes.Equal(tx.Memo(), in.Memo) {
t.Errorf("Memo round-trip")
}
if tx.Outs().Len() != len(in.Outs) {
t.Fatalf("Outs.Len() = %d, want %d", tx.Outs().Len(), len(in.Outs))
}
if tx.Ins().Len() != len(in.Ins) {
t.Fatalf("Ins.Len() = %d, want %d", tx.Ins().Len(), len(in.Ins))
}
if tx.Credentials().Len() != len(in.Credentials) {
t.Fatalf("Credentials.Len() = %d, want %d", tx.Credentials().Len(), len(in.Credentials))
}
if tx.SigIndicesArray().Len() != 3 {
t.Fatalf("SigIndicesArray.Len() = %d, want 3", tx.SigIndicesArray().Len())
}
if tx.SignatureArray().Len() != 3 {
t.Fatalf("SignatureArray.Len() = %d, want 3", tx.SignatureArray().Len())
}
tx2, err := WrapBaseTxFull(tx.Bytes())
if err != nil {
t.Fatalf("Wrap failed: %v", err)
}
if tx2.NetworkID() != in.NetworkID || tx2.BlockchainID() != in.BlockchainID {
t.Fatal("wrap-round-trip mismatch")
}
// Cross-confusion: WrapBaseTx (the metadata-only stub) must reject.
if _, err := WrapBaseTx(tx.Bytes()); err != ErrWrongTxKind {
t.Fatalf("WrapBaseTx on BaseTxFull bytes: want ErrWrongTxKind, got %v", err)
}
}
func TestImportTxRoundTrip(t *testing.T) {
in := ImportTxInput{
NetworkID: 1337,
BlockchainID: ids.ID{0xc1},
SourceNetwork: ids.ID{0xd1, 0xd2},
Outs: sampleOuts(),
LocalIns: sampleIns(),
ImportedIns: []InputListEntry{
{TxID: ids.ID{0x03}, OutputIndex: 0, AssetID: ids.ID{0xa3}, Amount: 999, SigIndices: []uint32{0}},
},
Credentials: sampleCreds(),
Memo: []byte("import"),
}
tx := NewImportTx(in)
if tx.SourceNetwork() != in.SourceNetwork {
t.Errorf("SourceNetwork round-trip")
}
wantStart, wantCount := uint32(len(in.LocalIns)), uint32(len(in.ImportedIns))
gotStart, gotCount := tx.ImportedInsRange()
if gotStart != wantStart || gotCount != wantCount {
t.Fatalf("ImportedInsRange = (%d,%d), want (%d,%d)", gotStart, gotCount, wantStart, wantCount)
}
// Combined Ins length includes both local and imported.
if tx.Ins().Len() != len(in.LocalIns)+len(in.ImportedIns) {
t.Fatalf("Ins.Len() = %d, want %d", tx.Ins().Len(), len(in.LocalIns)+len(in.ImportedIns))
}
tx2, err := WrapImportTx(tx.Bytes())
if err != nil {
t.Fatalf("Wrap failed: %v", err)
}
if tx2.SourceNetwork() != in.SourceNetwork {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestExportTxRoundTrip(t *testing.T) {
in := ExportTxInput{
NetworkID: 1337,
BlockchainID: ids.ID{0xc1},
DestinationNetwork: ids.ID{0xe1, 0xe2},
LocalOuts: sampleOuts(),
ExportedOuts: []OutputListEntry{
{AssetID: ids.ID{0xa9}, Amount: 500, Threshold: 1, Locktime: 0, OwnerAddress: ids.ShortID{0x99}},
},
Ins: sampleIns(),
Credentials: sampleCreds(),
Memo: []byte("export"),
}
tx := NewExportTx(in)
if tx.DestinationNetwork() != in.DestinationNetwork {
t.Errorf("DestinationNetwork round-trip")
}
wantStart, wantCount := uint32(len(in.LocalOuts)), uint32(len(in.ExportedOuts))
gotStart, gotCount := tx.ExportedOutsRange()
if gotStart != wantStart || gotCount != wantCount {
t.Fatalf("ExportedOutsRange = (%d,%d), want (%d,%d)", gotStart, gotCount, wantStart, wantCount)
}
tx2, err := WrapExportTx(tx.Bytes())
if err != nil {
t.Fatalf("Wrap failed: %v", err)
}
if tx2.DestinationNetwork() != in.DestinationNetwork {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestAddPermissionlessValidatorTxRoundTrip(t *testing.T) {
var blsPub [bls.PublicKeyLen]byte
for i := range blsPub {
blsPub[i] = byte(i + 1)
}
var pop [bls.SignatureLen]byte
for i := range pop {
pop[i] = byte(0xFF - i)
}
in := AddPermissionlessValidatorTxInput{
NetworkID: 1337,
BlockchainID: ids.ID{0xc1},
Outs: sampleOuts(),
Ins: sampleIns(),
Credentials: sampleCreds(),
Memo: []byte("apv"),
NodeID: ids.NodeID{0xaa, 0xbb, 0xcc},
StakeStart: 1_700_000_000,
StakeEnd: 1_800_000_000,
StakeWeight: 1_000_000,
StakeAssetID: ids.ID{0xaf},
StakeAmount: 2_000_000_000,
BLSPublicKey: blsPub,
BLSProofOfPossession: pop,
ValidationRewardsOwner: OwnerStub{Threshold: 1, Locktime: 0, Address: ids.ShortID{0x10}},
DelegationRewardsOwner: OwnerStub{Threshold: 1, Locktime: 0, Address: ids.ShortID{0x20}},
DelegationShares: 100_000,
}
tx := NewAddPermissionlessValidatorTx(in)
if tx.NodeID() != in.NodeID {
t.Errorf("NodeID round-trip")
}
if tx.StakeStart() != in.StakeStart {
t.Errorf("StakeStart round-trip")
}
if tx.StakeEnd() != in.StakeEnd {
t.Errorf("StakeEnd round-trip")
}
if tx.StakeWeight() != in.StakeWeight {
t.Errorf("StakeWeight round-trip")
}
if tx.StakeAssetID() != in.StakeAssetID {
t.Errorf("StakeAssetID round-trip")
}
if tx.StakeAmount() != in.StakeAmount {
t.Errorf("StakeAmount round-trip")
}
if tx.BLSPublicKey() != blsPub {
t.Errorf("BLSPublicKey round-trip")
}
if tx.BLSProofOfPossession() != pop {
t.Errorf("BLSProofOfPossession round-trip")
}
vrThresh, vrLT, vrAddr := tx.ValidationRewardsOwner()
if vrThresh != in.ValidationRewardsOwner.Threshold ||
vrLT != in.ValidationRewardsOwner.Locktime ||
vrAddr != in.ValidationRewardsOwner.Address {
t.Errorf("ValidationRewardsOwner round-trip")
}
drThresh, drLT, drAddr := tx.DelegationRewardsOwner()
if drThresh != in.DelegationRewardsOwner.Threshold ||
drLT != in.DelegationRewardsOwner.Locktime ||
drAddr != in.DelegationRewardsOwner.Address {
t.Errorf("DelegationRewardsOwner round-trip")
}
if tx.DelegationShares() != in.DelegationShares {
t.Errorf("DelegationShares round-trip")
}
tx2, err := WrapAddPermissionlessValidatorTx(tx.Bytes())
if err != nil {
t.Fatalf("Wrap failed: %v", err)
}
if tx2.NodeID() != in.NodeID {
t.Fatal("wrap-round-trip mismatch")
}
}
func TestCreateChainTxRoundTrip(t *testing.T) {
in := CreateChainTxInput{
NetworkID: 1337,
BlockchainID: ids.ID{0xc1},
Outs: sampleOuts(),
Ins: sampleIns(),
Credentials: sampleCreds(),
Memo: []byte("cc"),
ParentNetwork: ids.ID{0xf1},
VMID: ids.ID{0xf2},
Owner: OwnerStub{Threshold: 1, Locktime: 1_900_000_000, Address: ids.ShortID{0x77}},
WarpMessageHash: ids.ID{0xa1, 0xa2, 0xa3, 0xa4},
GenesisData: []byte("genesis bytes for the new chain"),
}
tx := NewCreateChainTx(in)
if tx.NetworkID() != in.NetworkID {
t.Errorf("NetworkID round-trip")
}
if tx.ParentNetwork() != in.ParentNetwork {
t.Errorf("ParentNetwork round-trip")
}
if tx.VMID() != in.VMID {
t.Errorf("VMID round-trip")
}
if tx.WarpMessageHash() != in.WarpMessageHash {
t.Errorf("WarpMessageHash round-trip")
}
if !bytes.Equal(tx.GenesisData(), in.GenesisData) {
t.Errorf("GenesisData round-trip")
}
ownerT, ownerLT, ownerAddr := tx.Owner()
if ownerT != in.Owner.Threshold || ownerLT != in.Owner.Locktime || ownerAddr != in.Owner.Address {
t.Errorf("Owner round-trip")
}
tx2, err := WrapCreateChainTx(tx.Bytes())
if err != nil {
t.Fatalf("Wrap failed: %v", err)
}
if tx2.VMID() != in.VMID {
t.Fatal("wrap-round-trip mismatch")
}
}
// TestBatch3CrossConfusionStaysClosed verifies all 5 new tx types reject
// every other type's bytes with ErrWrongTxKind, preserving the v3 TxKind
// invariant across the expanded set.
func TestBatch3CrossConfusionStaysClosed(t *testing.T) {
baseFull := NewBaseTxFull(BaseTxFullInput{NetworkID: 1, BlockchainID: ids.ID{1}}).Bytes()
importTx := NewImportTx(ImportTxInput{NetworkID: 1, BlockchainID: ids.ID{1}}).Bytes()
exportTx := NewExportTx(ExportTxInput{NetworkID: 1, BlockchainID: ids.ID{1}}).Bytes()
apv := NewAddPermissionlessValidatorTx(AddPermissionlessValidatorTxInput{
NetworkID: 1, BlockchainID: ids.ID{1},
}).Bytes()
cc := NewCreateChainTx(CreateChainTxInput{NetworkID: 1, BlockchainID: ids.ID{1}}).Bytes()
// Pairwise: every WrapX(bufY) where Y != X must return ErrWrongTxKind.
tests := []struct {
name string
fn func() error
}{
{"WrapBaseTxFull(importTx)", func() error { _, err := WrapBaseTxFull(importTx); return err }},
{"WrapImportTx(baseFull)", func() error { _, err := WrapImportTx(baseFull); return err }},
{"WrapExportTx(importTx)", func() error { _, err := WrapExportTx(importTx); return err }},
{"WrapAddPermissionlessValidatorTx(cc)", func() error {
_, err := WrapAddPermissionlessValidatorTx(cc)
return err
}},
{"WrapCreateChainTx(apv)", func() error { _, err := WrapCreateChainTx(apv); return err }},
{"WrapBaseTx(baseFull)", func() error { _, err := WrapBaseTx(baseFull); return err }},
{"WrapAdvanceTimeTx(exportTx)", func() error { _, err := WrapAdvanceTimeTx(exportTx); return err }},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if err := tc.fn(); err != ErrWrongTxKind {
t.Fatalf("%s: got %v, want ErrWrongTxKind", tc.name, err)
}
})
}
}
@@ -0,0 +1,199 @@
// 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"
)
// CreateChainTx is the v3 create-blockchain envelope. It carries the
// BaseTxFull spending state, the parent network ID, the child VM ID, an
// optional genesis-data byte blob (variable; lives in the variable section),
// a single-address Owner stub, and the SHA-256 hash of the corresponding
// Warp message (when this CreateChain was originated via cross-network
// commit). The Warp message body itself is NOT embedded — only its hash —
// because the body lives in the signed Warp envelope outside this tx.
//
// Fixed-section layout (size 173 bytes):
//
// TxKind uint8 @ 0
// NetworkID uint32 @ 1
// BlockchainID 32B @ 5
// OutsList 8B @ 37
// InsList 8B @ 45
// CredsList 8B @ 53
// SigIndicesArr 8B @ 61
// SigArr 8B @ 69
// Memo 8B @ 77
// ParentNetwork 32B @ 85 (ids.ID; the parent network)
// VMID 32B @ 117 (ids.ID; the VM identifier)
// OwnerThreshold uint32 @ 149
// OwnerLocktime uint64 @ 153
// OwnerAddress 20B @ 161
// WarpMessageHash 32B @ 181 (sha256 of the originating Warp msg; zero if none)
// GenesisData bytes @ 213 (offset+length; variable)
// Size = 221
const (
OffsetCreateChainTx_NetworkID = 1
OffsetCreateChainTx_BlockchainID = 5
OffsetCreateChainTx_OutsList = 37
OffsetCreateChainTx_InsList = 45
OffsetCreateChainTx_CredsList = 53
OffsetCreateChainTx_SigIndicesArr = 61
OffsetCreateChainTx_SigArr = 69
OffsetCreateChainTx_Memo = 77
OffsetCreateChainTx_ParentNetwork = 85
OffsetCreateChainTx_VMID = 117
OffsetCreateChainTx_OwnerThreshold = 149
OffsetCreateChainTx_OwnerLocktime = 153
OffsetCreateChainTx_OwnerAddress = 161
OffsetCreateChainTx_WarpMessageHash = OffsetCreateChainTx_OwnerAddress + ids.ShortIDLen
OffsetCreateChainTx_GenesisData = OffsetCreateChainTx_WarpMessageHash + 32
SizeCreateChainTx = OffsetCreateChainTx_GenesisData + 8
)
// CreateChainTx is the zero-copy accessor.
type CreateChainTx struct {
msg *zap.Message
obj zap.Object
}
func (t CreateChainTx) NetworkID() uint32 { return t.obj.Uint32(OffsetCreateChainTx_NetworkID) }
func (t CreateChainTx) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetCreateChainTx_BlockchainID + i)
}
return out
}
func (t CreateChainTx) ParentNetwork() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetCreateChainTx_ParentNetwork + i)
}
return out
}
func (t CreateChainTx) VMID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetCreateChainTx_VMID + i)
}
return out
}
func (t CreateChainTx) Owner() (uint32, uint64, ids.ShortID) {
threshold := t.obj.Uint32(OffsetCreateChainTx_OwnerThreshold)
locktime := t.obj.Uint64(OffsetCreateChainTx_OwnerLocktime)
var addr ids.ShortID
for i := 0; i < ids.ShortIDLen; i++ {
addr[i] = t.obj.Uint8(OffsetCreateChainTx_OwnerAddress + i)
}
return threshold, locktime, addr
}
// WarpMessageHash returns the SHA-256 hash of the originating Warp message.
// Returns the zero ids.ID when this CreateChain was not originated via
// a cross-network commit.
func (t CreateChainTx) WarpMessageHash() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetCreateChainTx_WarpMessageHash + i)
}
return out
}
// GenesisData returns the chain's genesis bytes.
//
// READ-ONLY: see BaseTxFull.Memo for the aliasing contract.
func (t CreateChainTx) GenesisData() []byte {
return t.obj.Bytes(OffsetCreateChainTx_GenesisData)
}
func (t CreateChainTx) Outs() OutputList { return OutputListView(t.obj, OffsetCreateChainTx_OutsList) }
func (t CreateChainTx) Ins() InputList { return InputListView(t.obj, OffsetCreateChainTx_InsList) }
func (t CreateChainTx) Credentials() CredentialList {
return CredentialListView(t.obj, OffsetCreateChainTx_CredsList)
}
func (t CreateChainTx) SigIndicesArray() SigIndicesArray {
return SigIndicesArrayView(t.obj, OffsetCreateChainTx_SigIndicesArr)
}
func (t CreateChainTx) SignatureArray() SignatureArray {
return SignatureArrayView(t.obj, OffsetCreateChainTx_SigArr)
}
func (t CreateChainTx) Memo() []byte { return t.obj.Bytes(OffsetCreateChainTx_Memo) }
func (t CreateChainTx) Bytes() []byte { return t.msg.Bytes() }
func (t CreateChainTx) IsZero() bool { return t.msg == nil }
func WrapCreateChainTx(b []byte) (CreateChainTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return CreateChainTx{}, err
}
obj := msg.Root()
if TxKind(obj.Uint8(OffsetTxKind)) != TxKindCreateChain {
return CreateChainTx{}, ErrWrongTxKind
}
return CreateChainTx{msg: msg, obj: obj}, nil
}
// CreateChainTxInput is the constructor input.
type CreateChainTxInput struct {
NetworkID uint32
BlockchainID ids.ID
Outs []OutputListEntry
Ins []InputListEntry
Credentials []CredentialListEntry
Memo []byte
ParentNetwork ids.ID
VMID ids.ID
Owner OwnerStub
WarpMessageHash ids.ID
GenesisData []byte
}
func NewCreateChainTx(in CreateChainTxInput) CreateChainTx {
cap := zap.HeaderSize + 16 + SizeCreateChainTx
cap += len(in.Outs) * SizeTransferableOutput
cap += len(in.Ins) * SizeTransferableInput
cap += len(in.Credentials) * SizeCredential
cap += len(in.Memo) + len(in.GenesisData)
b := zap.NewBuilder(cap)
outsOff, outsCount := WriteOutputList(b, in.Outs)
insOff, insCount, sigIndices := WriteInputList(b, in.Ins)
credsOff, credsCount, sigBlobs := WriteCredentialList(b, in.Credentials)
sigIdxArrOff, sigIdxArrCount := WriteSigIndicesArray(b, sigIndices)
sigArrOff, sigArrCount := WriteSignatureArray(b, sigBlobs)
ob := b.StartObject(SizeCreateChainTx)
ob.SetUint8(OffsetTxKind, uint8(TxKindCreateChain))
ob.SetUint32(OffsetCreateChainTx_NetworkID, in.NetworkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetCreateChainTx_BlockchainID+i, in.BlockchainID[i])
ob.SetUint8(OffsetCreateChainTx_ParentNetwork+i, in.ParentNetwork[i])
ob.SetUint8(OffsetCreateChainTx_VMID+i, in.VMID[i])
ob.SetUint8(OffsetCreateChainTx_WarpMessageHash+i, in.WarpMessageHash[i])
}
ob.SetList(OffsetCreateChainTx_OutsList, outsOff, outsCount)
ob.SetList(OffsetCreateChainTx_InsList, insOff, insCount)
ob.SetList(OffsetCreateChainTx_CredsList, credsOff, credsCount)
ob.SetList(OffsetCreateChainTx_SigIndicesArr, sigIdxArrOff, sigIdxArrCount)
ob.SetList(OffsetCreateChainTx_SigArr, sigArrOff, sigArrCount)
ob.SetBytes(OffsetCreateChainTx_Memo, in.Memo)
ob.SetUint32(OffsetCreateChainTx_OwnerThreshold, in.Owner.Threshold)
ob.SetUint64(OffsetCreateChainTx_OwnerLocktime, in.Owner.Locktime)
for i := 0; i < ids.ShortIDLen; i++ {
ob.SetUint8(OffsetCreateChainTx_OwnerAddress+i, in.Owner.Address[i])
}
ob.SetBytes(OffsetCreateChainTx_GenesisData, in.GenesisData)
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return CreateChainTx{msg: msg, obj: msg.Root()}
}
+158
View File
@@ -0,0 +1,158 @@
// 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"
)
// ExportTx is the v3 cross-chain export: send funds from this chain to a
// destination chain. The Outs list carries BOTH local change outputs and the
// exported outputs to the destination chain in a single fixed-stride array;
// ExportedOutsStart / ExportedOutsCount mark the exported slice within.
// Local Outs occupy positions [0, ExportedOutsStart); exported Outs occupy
// [ExportedOutsStart, ExportedOutsStart+ExportedOutsCount).
//
// Fixed-section layout (size 125 bytes):
//
// TxKind uint8 @ 0
// NetworkID uint32 @ 1
// BlockchainID 32B @ 5
// OutsList 8B @ 37 (combined: local change + exported)
// InsList 8B @ 45 (local fee + funding inputs)
// CredsList 8B @ 53
// SigIndicesArr 8B @ 61
// SigArr 8B @ 69
// Memo 8B @ 77
// DestinationNetwork 32B @ 85 (chain the exports flow to)
// ExportedOutsStart uint32 @ 117 (index into Outs list)
// ExportedOutsCount uint32 @ 121
const (
OffsetExportTx_NetworkID = 1
OffsetExportTx_BlockchainID = 5
OffsetExportTx_OutsList = 37
OffsetExportTx_InsList = 45
OffsetExportTx_CredsList = 53
OffsetExportTx_SigIndicesArr = 61
OffsetExportTx_SigArr = 69
OffsetExportTx_Memo = 77
OffsetExportTx_DestinationNetwork = 85
OffsetExportTx_ExportedOutsStart = 117
OffsetExportTx_ExportedOutsCount = 121
SizeExportTx = 125
)
// ExportTx is the zero-copy accessor for a v3 ExportTx.
type ExportTx struct {
msg *zap.Message
obj zap.Object
}
func (t ExportTx) NetworkID() uint32 { return t.obj.Uint32(OffsetExportTx_NetworkID) }
func (t ExportTx) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetExportTx_BlockchainID + i)
}
return out
}
// DestinationNetwork returns the network identifier the exports flow to.
func (t ExportTx) DestinationNetwork() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetExportTx_DestinationNetwork + i)
}
return out
}
func (t ExportTx) Outs() OutputList { return OutputListView(t.obj, OffsetExportTx_OutsList) }
func (t ExportTx) Ins() InputList { return InputListView(t.obj, OffsetExportTx_InsList) }
func (t ExportTx) Credentials() CredentialList {
return CredentialListView(t.obj, OffsetExportTx_CredsList)
}
func (t ExportTx) SigIndicesArray() SigIndicesArray {
return SigIndicesArrayView(t.obj, OffsetExportTx_SigIndicesArr)
}
func (t ExportTx) SignatureArray() SignatureArray {
return SignatureArrayView(t.obj, OffsetExportTx_SigArr)
}
// ExportedOutsRange returns (start, count) marking the exported-output slice
// within the Outs list. Outs[0..start) are local change;
// Outs[start..start+count) are the exported outputs.
func (t ExportTx) ExportedOutsRange() (uint32, uint32) {
return t.obj.Uint32(OffsetExportTx_ExportedOutsStart),
t.obj.Uint32(OffsetExportTx_ExportedOutsCount)
}
// Memo returns the memo bytes. READ-ONLY (see BaseTxFull.Memo).
func (t ExportTx) Memo() []byte { return t.obj.Bytes(OffsetExportTx_Memo) }
func (t ExportTx) Bytes() []byte { return t.msg.Bytes() }
func (t ExportTx) IsZero() bool { return t.msg == nil }
func WrapExportTx(b []byte) (ExportTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return ExportTx{}, err
}
obj := msg.Root()
if TxKind(obj.Uint8(OffsetTxKind)) != TxKindExport {
return ExportTx{}, ErrWrongTxKind
}
return ExportTx{msg: msg, obj: obj}, nil
}
// ExportTxInput is the constructor input for NewExportTx.
type ExportTxInput struct {
NetworkID uint32
BlockchainID ids.ID
DestinationNetwork ids.ID
LocalOuts []OutputListEntry
ExportedOuts []OutputListEntry
Ins []InputListEntry
Credentials []CredentialListEntry
Memo []byte
}
func NewExportTx(in ExportTxInput) ExportTx {
cap := zap.HeaderSize + 16 + SizeExportTx
cap += (len(in.LocalOuts) + len(in.ExportedOuts)) * SizeTransferableOutput
cap += len(in.Ins) * SizeTransferableInput
cap += len(in.Credentials) * SizeCredential
cap += len(in.Memo)
b := zap.NewBuilder(cap)
combinedOuts := make([]OutputListEntry, 0, len(in.LocalOuts)+len(in.ExportedOuts))
combinedOuts = append(combinedOuts, in.LocalOuts...)
combinedOuts = append(combinedOuts, in.ExportedOuts...)
outsOff, outsCount := WriteOutputList(b, combinedOuts)
insOff, insCount, sigIndices := WriteInputList(b, in.Ins)
credsOff, credsCount, sigBlobs := WriteCredentialList(b, in.Credentials)
sigIdxArrOff, sigIdxArrCount := WriteSigIndicesArray(b, sigIndices)
sigArrOff, sigArrCount := WriteSignatureArray(b, sigBlobs)
ob := b.StartObject(SizeExportTx)
ob.SetUint8(OffsetTxKind, uint8(TxKindExport))
ob.SetUint32(OffsetExportTx_NetworkID, in.NetworkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetExportTx_BlockchainID+i, in.BlockchainID[i])
ob.SetUint8(OffsetExportTx_DestinationNetwork+i, in.DestinationNetwork[i])
}
ob.SetList(OffsetExportTx_OutsList, outsOff, outsCount)
ob.SetList(OffsetExportTx_InsList, insOff, insCount)
ob.SetList(OffsetExportTx_CredsList, credsOff, credsCount)
ob.SetList(OffsetExportTx_SigIndicesArr, sigIdxArrOff, sigIdxArrCount)
ob.SetList(OffsetExportTx_SigArr, sigArrOff, sigArrCount)
ob.SetBytes(OffsetExportTx_Memo, in.Memo)
ob.SetUint32(OffsetExportTx_ExportedOutsStart, uint32(len(in.LocalOuts)))
ob.SetUint32(OffsetExportTx_ExportedOutsCount, uint32(len(in.ExportedOuts)))
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return ExportTx{msg: msg, obj: msg.Root()}
}
+164
View File
@@ -0,0 +1,164 @@
// 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"
)
// ImportTx is the v3 cross-chain import: pull funds from a source chain via
// imported UTXOs. The Ins list carries BOTH local fee-paying inputs and the
// remote (imported) inputs in a single fixed-stride array; the
// ImportedInsStart / ImportedInsCount header fields identify the imported
// slice within. This keeps the SigIndicesArray / SignatureArray single
// shared arrays across all inputs without re-base bookkeeping. Local Ins
// occupy positions [0, ImportedInsStart); imported Ins occupy
// [ImportedInsStart, ImportedInsStart+ImportedInsCount).
//
// Fixed-section layout (size 93 bytes):
//
// TxKind uint8 @ 0
// NetworkID uint32 @ 1
// BlockchainID 32B @ 5
// OutsList 8B @ 37
// InsList 8B @ 45 (combined: local + imported)
// CredsList 8B @ 53
// SigIndicesArr 8B @ 61
// SigArr 8B @ 69
// Memo 8B @ 77
// SourceNetwork 32B @ 85 (...wait, that's 117. Re-do.)
//
// Actually wait. 85 + 32 = 117. Plus ImportedInsStart/Count (8) = 125.
//
// Re-layout to keep TxKind@0 alignment and end size 125 bytes:
const (
OffsetImportTx_NetworkID = 1
OffsetImportTx_BlockchainID = 5
OffsetImportTx_OutsList = 37
OffsetImportTx_InsList = 45
OffsetImportTx_CredsList = 53
OffsetImportTx_SigIndicesArr = 61
OffsetImportTx_SigArr = 69
OffsetImportTx_Memo = 77
OffsetImportTx_SourceNetwork = 85
OffsetImportTx_ImportedInsStart = 117 // uint32 (index into Ins list)
OffsetImportTx_ImportedInsCount = 121 // uint32
SizeImportTx = 125
)
// ImportTx is the zero-copy accessor for an v3 ImportTx.
type ImportTx struct {
msg *zap.Message
obj zap.Object
}
func (t ImportTx) NetworkID() uint32 { return t.obj.Uint32(OffsetImportTx_NetworkID) }
func (t ImportTx) BlockchainID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetImportTx_BlockchainID + i)
}
return out
}
// SourceNetwork returns the network identifier the UTXOs are imported from.
func (t ImportTx) SourceNetwork() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = t.obj.Uint8(OffsetImportTx_SourceNetwork + i)
}
return out
}
func (t ImportTx) Outs() OutputList { return OutputListView(t.obj, OffsetImportTx_OutsList) }
func (t ImportTx) Ins() InputList { return InputListView(t.obj, OffsetImportTx_InsList) }
func (t ImportTx) Credentials() CredentialList {
return CredentialListView(t.obj, OffsetImportTx_CredsList)
}
func (t ImportTx) SigIndicesArray() SigIndicesArray {
return SigIndicesArrayView(t.obj, OffsetImportTx_SigIndicesArr)
}
func (t ImportTx) SignatureArray() SignatureArray {
return SignatureArrayView(t.obj, OffsetImportTx_SigArr)
}
// ImportedInsRange returns (start, count) marking the imported-input slice
// within the Ins list. Ins[0..start) are local fee-payers;
// Ins[start..start+count) are the imported (remote-source) inputs.
func (t ImportTx) ImportedInsRange() (uint32, uint32) {
return t.obj.Uint32(OffsetImportTx_ImportedInsStart),
t.obj.Uint32(OffsetImportTx_ImportedInsCount)
}
// Memo returns the memo bytes. READ-ONLY (see BaseTxFull.Memo).
func (t ImportTx) Memo() []byte { return t.obj.Bytes(OffsetImportTx_Memo) }
func (t ImportTx) Bytes() []byte { return t.msg.Bytes() }
func (t ImportTx) IsZero() bool { return t.msg == nil }
func WrapImportTx(b []byte) (ImportTx, error) {
msg, err := zap.Parse(b)
if err != nil {
return ImportTx{}, err
}
obj := msg.Root()
if TxKind(obj.Uint8(OffsetTxKind)) != TxKindImport {
return ImportTx{}, ErrWrongTxKind
}
return ImportTx{msg: msg, obj: obj}, nil
}
// ImportTxInput is the constructor input for NewImportTx. LocalIns and
// ImportedIns are written as a single combined list; the constructor
// records ImportedInsStart = len(LocalIns).
type ImportTxInput struct {
NetworkID uint32
BlockchainID ids.ID
SourceNetwork ids.ID
Outs []OutputListEntry
LocalIns []InputListEntry
ImportedIns []InputListEntry
Credentials []CredentialListEntry
Memo []byte
}
func NewImportTx(in ImportTxInput) ImportTx {
cap := zap.HeaderSize + 16 + SizeImportTx
cap += len(in.Outs) * SizeTransferableOutput
cap += (len(in.LocalIns) + len(in.ImportedIns)) * SizeTransferableInput
cap += len(in.Credentials) * SizeCredential
cap += len(in.Memo)
b := zap.NewBuilder(cap)
combinedIns := make([]InputListEntry, 0, len(in.LocalIns)+len(in.ImportedIns))
combinedIns = append(combinedIns, in.LocalIns...)
combinedIns = append(combinedIns, in.ImportedIns...)
outsOff, outsCount := WriteOutputList(b, in.Outs)
insOff, insCount, sigIndices := WriteInputList(b, combinedIns)
credsOff, credsCount, sigBlobs := WriteCredentialList(b, in.Credentials)
sigIdxArrOff, sigIdxArrCount := WriteSigIndicesArray(b, sigIndices)
sigArrOff, sigArrCount := WriteSignatureArray(b, sigBlobs)
ob := b.StartObject(SizeImportTx)
ob.SetUint8(OffsetTxKind, uint8(TxKindImport))
ob.SetUint32(OffsetImportTx_NetworkID, in.NetworkID)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetImportTx_BlockchainID+i, in.BlockchainID[i])
ob.SetUint8(OffsetImportTx_SourceNetwork+i, in.SourceNetwork[i])
}
ob.SetList(OffsetImportTx_OutsList, outsOff, outsCount)
ob.SetList(OffsetImportTx_InsList, insOff, insCount)
ob.SetList(OffsetImportTx_CredsList, credsOff, credsCount)
ob.SetList(OffsetImportTx_SigIndicesArr, sigIdxArrOff, sigIdxArrCount)
ob.SetList(OffsetImportTx_SigArr, sigArrOff, sigArrCount)
ob.SetBytes(OffsetImportTx_Memo, in.Memo)
ob.SetUint32(OffsetImportTx_ImportedInsStart, uint32(len(in.LocalIns)))
ob.SetUint32(OffsetImportTx_ImportedInsCount, uint32(len(in.ImportedIns)))
ob.FinishAsRoot()
msg, _ := zap.Parse(b.Finish())
return ImportTx{msg: msg, obj: msg.Root()}
}
+17 -11
View File
@@ -18,17 +18,23 @@ import "errors"
type TxKind uint8
const (
TxKindReserved TxKind = 0
TxKindAdvanceTime TxKind = 1
TxKindRewardValidator TxKind = 2
TxKindSetL1ValidatorWeight TxKind = 3
TxKindIncreaseL1ValidatorBalance TxKind = 4
TxKindDisableL1Validator TxKind = 5
TxKindBase TxKind = 6
TxKindRegisterL1Validator TxKind = 7
TxKindSlashValidator TxKind = 8
TxKindTransferChainOwnership TxKind = 9
TxKindRemoveChainValidator TxKind = 10
TxKindReserved TxKind = 0
TxKindAdvanceTime TxKind = 1
TxKindRewardValidator TxKind = 2
TxKindSetL1ValidatorWeight TxKind = 3
TxKindIncreaseL1ValidatorBalance TxKind = 4
TxKindDisableL1Validator TxKind = 5
TxKindBase TxKind = 6
TxKindRegisterL1Validator TxKind = 7
TxKindSlashValidator TxKind = 8
TxKindTransferChainOwnership TxKind = 9
TxKindRemoveChainValidator TxKind = 10
// Batch 3 — tx types that compose batch-3 list/object primitives:
TxKindBaseFull TxKind = 11 // BaseTx with Outs+Ins+Credentials
TxKindAddPermissionlessValidator TxKind = 12
TxKindImport TxKind = 13
TxKindExport TxKind = 14
TxKindCreateChain TxKind = 15
)
// OffsetTxKind is the fixed wire position of the discriminator. Every