Pure zap tx: pure Tx (Parse=wrap+split, Sign=build) + delete reflection codec

tx.go: Tx holds Unsigned (zap-backed) + Creds; Parse wraps signed bytes and
splits unsigned/creds at the self-delimiting boundary; Sign builds unsigned‖creds;
no codec.Manager param anywhere. Deleted codec.go (V0/V1/V2 reflection Manager)
and codec_activation.go. WIP: consumer sites that passed txs.Codec / called the
old Parse(codec, bytes) flip next.
This commit is contained in:
zeekay
2026-07-10 09:08:15 -07:00
parent 1dede2e3d2
commit 1bc07cb1ca
3 changed files with 98 additions and 543 deletions
-349
View File
@@ -1,349 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"errors"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/utxo/secp256k1fx"
)
const (
// CodecVersionV0 is the v1.23.x ("Apricot/Banff") wire layout. It is
// retained as a READ-ONLY decoder so that pre-codec-v1 blocks and txs
// on disk (mainnet, testnet) continue to deserialize. All write paths
// at ts < ZAPCodecActivationTimestamp MUST use CodecVersionV1.
CodecVersionV0 uint16 = 0
// CodecVersionV1 is the canonical linearcodec layout — big-endian,
// produced by every binary up to and including the pre-ZAP cutover.
// Read forever; written only when block.Timestamp <
// ZAPCodecActivationTimestamp.
CodecVersionV1 uint16 = 1
// CodecVersionV2 is the ZAP-native layout — little-endian, structurally
// identical to V1 (same slot map, same field order), only the wire
// encoding differs. Produced when block.Timestamp >=
// ZAPCodecActivationTimestamp. Read always.
//
// V2 is NOT a slot-map change; it's a wire-encoding change. The same
// Go types are registered at the same slot IDs as V1. A V1-encoded
// tx and a V2-encoded tx of the same logical content differ in
// byte content (LE vs BE) but produce identical Go values after
// Unmarshal.
CodecVersionV2 uint16 = 2
// CodecVersion is the deprecated alias preserved for code that
// historically referenced "the write version". New code MUST use
// CodecVersionForTimestamp instead — there is no single canonical
// write version after the ZAP cutover.
CodecVersion = CodecVersionV1
// Version is retained as a deprecated alias for code that referenced
// the pre-multi-version constant.
Version = CodecVersion
)
var (
// Codec is the standard-size multi-version codec used for normal txs.
// Registers V0 + V1 (linearcodec) AND V2 (zapcodec). The right write
// version is selected via CodecVersionForTimestamp; reads dispatch on
// the 2-byte wire prefix.
Codec pcodecs.Manager
// GenesisCodec allows txs of larger than usual size to be parsed.
// It registers the same versioned slot layouts as Codec but with an
// unbounded maximum size. New, unverified txs MUST be processed by
// Codec; GenesisCodec is reserved for genesis decode + state read
// fallback paths.
GenesisCodec pcodecs.Manager
)
func init() {
cV0 := pcodecs.NewLinearCodec()
cV1 := pcodecs.NewLinearCodec()
cV2 := pcodecs.NewZAPCodec()
gcV0 := pcodecs.NewLinearCodec()
gcV1 := pcodecs.NewLinearCodec()
gcV2 := pcodecs.NewZAPCodec()
errs := pcodecs.Errs{}
errs.Add(
registerV0TxTypes(cV0),
registerV0TxTypes(gcV0),
registerV1TxTypes(cV1),
registerV1TxTypes(gcV1),
// V2 reuses the V1 slot map verbatim — only the wire encoding
// differs. Same Go types, same IDs.
registerV1TxTypes(cV2),
registerV1TxTypes(gcV2),
)
Codec = pcodecs.NewDefaultManager()
GenesisCodec = pcodecs.NewMaxInt32Manager()
errs.Add(
Codec.RegisterCodec(CodecVersionV0, cV0),
Codec.RegisterCodec(CodecVersionV1, cV1),
Codec.RegisterCodec(CodecVersionV2, cV2),
GenesisCodec.RegisterCodec(CodecVersionV0, gcV0),
GenesisCodec.RegisterCodec(CodecVersionV1, gcV1),
GenesisCodec.RegisterCodec(CodecVersionV2, gcV2),
)
if errs.Errored() {
panic(errs.Err)
}
}
// slotRegistrar is the subset of {linearcodec.Codec, zapcodec.Codec}
// that registerV0TxTypes and registerV1TxTypes need. Identifying it as
// an in-package interface keeps the registration functions wire-
// agnostic: they decide the SLOT MAP, the codec implementation decides
// the WIRE FORMAT. Same slot IDs across all codec implementations.
type slotRegistrar interface {
RegisterType(interface{}) error
SkipRegistrations(int)
}
// CodecVersionForTimestamp returns the canonical write-codec version
// for a transaction (or block) whose timestamp is ts. Reads should NOT
// use this — dispatch on the wire prefix via Codec.Unmarshal directly.
//
// The cutover is strict — a tx with ts == ZAPCodecActivationTimestamp
// is V2. A tx with ts == ZAPCodecActivationTimestamp - 1 is V1. There
// is no fallback; if the wire bytes do not match the expected version
// for the chain's current time, the tx is rejected by the codec
// version-not-allowed gate in TxsForTimestamp.
func CodecVersionForTimestamp(ts uint64) uint16 {
if ts >= ZAPCodecActivationTimestamp {
return CodecVersionV2
}
return CodecVersionV1
}
// CodecForTimestamp returns the codec.Manager configured to write at
// the canonical version for the given timestamp. The returned Manager
// is always the package-level Codec — it hosts every registered
// version. The Manager's Marshal(version, ...) method is what selects
// the wire encoding; this helper just returns the right version too,
// via the companion CodecVersionForTimestamp.
//
// Why expose both Manager AND version: callers historically pass a
// version into Codec.Marshal/Codec.Unmarshal. They keep that pattern;
// CodecForTimestamp is a no-op for the manager handle, kept here so
// future evolutions can swap the manager itself (e.g. a ZAP-only
// manager after V1 retirement) without changing call sites.
func CodecForTimestamp(_ uint64) pcodecs.Manager {
return Codec
}
// CodecAllowsRead reports whether v is one of the codec versions this
// binary recognises for reads. Used by tx-parse gates: a wire whose
// prefix is V0/V1/V2 is accepted; anything else is rejected before
// the codec.Manager would surface ErrUnknownVersion.
//
// V0 reads are always allowed (legacy historical txs).
// V1 reads are always allowed.
// V2 reads are always allowed once the binary ships with this
// constant — there is no "too early" rejection. The activation gate
// only protects WRITES.
func CodecAllowsRead(v uint16) bool {
return v == CodecVersionV0 || v == CodecVersionV1 || v == CodecVersionV2
}
// CodecRequiresLegacy reports whether wire-version v is in the
// pre-ZAP-activation legacy set. After activation a node MAY still
// accept a V1-encoded tx — there is no historical-bytes lookback
// problem — but the build path MUST refuse to PRODUCE V1 once ts
// crosses the activation threshold. This predicate distinguishes
// "legacy but accepted" from "current write version".
func CodecRequiresLegacy(v uint16) bool {
return v == CodecVersionV0 || v == CodecVersionV1
}
// RegisterTypes registers the v1 tx-codec types (the only version a
// new build path uses) on the given linearcodec. Existing call sites
// outside this package continue to compose with RegisterTypes; the v0
// decoder is gated to the txs.Codec / txs.GenesisCodec entries.
func RegisterTypes(targetCodec pcodecs.LinearCodec) error {
return registerV1TxTypes(targetCodec)
}
// registerV1TxTypes registers the v1 (current) slot layout. Slot map:
//
// 0-4 reserved for block codec (Skip(5))
// 5-11 secp256k1fx (TransferInput, MintOutput, TransferOutput,
// MintOperation, Credential, Input, OutputOwners)
// 12-20 Apricot txs
// 21-22 stakeable.{LockIn, LockOut}
// 23-26 reserved for block codec (Skip(4))
// 27-30 Banff txs
// 31-32 signer.{Empty, ProofOfPossession}
// 33-34 Durango (TransferChainOwnershipTx, BaseTx)
// 35-43 Etna + post-Etna (ConvertNetworkToL1Tx,
// CreateSovereignL1Tx, RegisterL1ValidatorTx,
// SetL1ValidatorWeightTx, IncreaseL1ValidatorBalanceTx,
// DisableL1ValidatorTx, SlashValidatorTx, CreateAssetTx,
// OperationTx)
func registerV1TxTypes(targetCodec slotRegistrar) error {
// Reserve 5 slots for the four canonical block types + one historical
// slot (atomic block ID) so existing tx type IDs remain stable.
targetCodec.SkipRegistrations(5)
errs := pcodecs.Errs{}
// secp256k1fx types are registered here because this matches the
// XVM registration order — utxos crossed in shared memory must
// have identical codec IDs across chains.
errs.Add(
targetCodec.RegisterType(&secp256k1fx.TransferInput{}),
targetCodec.RegisterType(&secp256k1fx.MintOutput{}),
targetCodec.RegisterType(&secp256k1fx.TransferOutput{}),
targetCodec.RegisterType(&secp256k1fx.MintOperation{}),
targetCodec.RegisterType(&secp256k1fx.Credential{}),
targetCodec.RegisterType(&secp256k1fx.Input{}),
targetCodec.RegisterType(&secp256k1fx.OutputOwners{}),
// Canonical tx kinds (block-1 era).
targetCodec.RegisterType(&AddValidatorTx{}),
targetCodec.RegisterType(&AddChainValidatorTx{}),
targetCodec.RegisterType(&AddDelegatorTx{}),
targetCodec.RegisterType(&CreateNetworkTx{}),
targetCodec.RegisterType(&CreateChainTx{}),
targetCodec.RegisterType(&ImportTx{}),
targetCodec.RegisterType(&ExportTx{}),
targetCodec.RegisterType(&AdvanceTimeTx{}),
targetCodec.RegisterType(&RewardValidatorTx{}),
targetCodec.RegisterType(&stakeable.LockIn{}),
targetCodec.RegisterType(&stakeable.LockOut{}),
)
// Skip 4 historical slots so subsequent types keep their ID.
targetCodec.SkipRegistrations(4)
errs.Add(
targetCodec.RegisterType(&RemoveChainValidatorTx{}),
targetCodec.RegisterType(&TransformChainTx{}),
targetCodec.RegisterType(&AddPermissionlessValidatorTx{}),
targetCodec.RegisterType(&AddPermissionlessDelegatorTx{}),
targetCodec.RegisterType(&signer.Empty{}),
targetCodec.RegisterType(&signer.ProofOfPossession{}),
targetCodec.RegisterType(&TransferChainOwnershipTx{}),
targetCodec.RegisterType(&BaseTx{}),
targetCodec.RegisterType(&ConvertNetworkToL1Tx{}),
// CreateSovereignL1Tx is the single-step alternative to
// CreateNetworkTx + AddChainValidatorTx ×N + CreateChainTx ×K
// + ConvertNetworkToL1Tx — registers a sovereign L1 atomically.
targetCodec.RegisterType(&CreateSovereignL1Tx{}),
targetCodec.RegisterType(&RegisterL1ValidatorTx{}),
targetCodec.RegisterType(&SetL1ValidatorWeightTx{}),
targetCodec.RegisterType(&IncreaseL1ValidatorBalanceTx{}),
targetCodec.RegisterType(&DisableL1ValidatorTx{}),
targetCodec.RegisterType(&SlashValidatorTx{}),
// P-only primary network — assets that historically lived on the
// X-Chain are first-class P-Chain UTXO operations.
targetCodec.RegisterType(&CreateAssetTx{}),
targetCodec.RegisterType(&OperationTx{}),
)
return errors.Join(errs.Err)
}
// registerV0TxTypes registers the v1.23.x ("Apricot/Banff") tx-codec
// slot layout. Wire-format-identical to the layout produced by
// luxfi/node@v1.23.31. v0 is registered as a READ-ONLY decoder; the
// write path (Marshal at CodecVersionV0) is never invoked from inside
// this package, and downstream code must not Marshal at CodecVersionV0.
//
// Slot map (tx-only codec):
//
// 0-4 reserved for block codec (Skip(5))
// 5 TransferInput
// 6 [skip — was secp256k1fx.MintOutput in XVM; never used on P]
// 7 TransferOutput
// 8 [skip — was secp256k1fx.MintOperation in XVM; never used on P]
// 9-11 Credential, Input, OutputOwners
// 12-20 Apricot txs
// 21-22 stakeable.{LockIn, LockOut}
// 23-26 Banff txs (RemoveChainValidator..AddPermissionlessDelegator)
// 27-28 signer.{Empty, ProofOfPossession}
// 29-32 reserved for Banff block slots (Skip(4))
// 33-34 Durango (TransferChainOwnershipTx, BaseTx)
// 35-39 Etna (ConvertChainToL1Tx [decoded as ConvertNetworkToL1Tx;
// same struct layout, name-only change],
// RegisterL1ValidatorTx, SetL1ValidatorWeightTx,
// IncreaseL1ValidatorBalanceTx, DisableL1ValidatorTx)
//
// Pre-Etna v0 blobs that contain only slots 5..28 decode cleanly
// without touching the post-Etna types.
func registerV0TxTypes(targetCodec slotRegistrar) error {
// Slots 0-4: reserved for block-codec block types.
targetCodec.SkipRegistrations(5)
errs := pcodecs.Errs{}
// Slots 5-11: secp256k1fx with the two v0 historical holes.
errs.Add(targetCodec.RegisterType(&secp256k1fx.TransferInput{}))
targetCodec.SkipRegistrations(1) // slot 6 hole.
errs.Add(targetCodec.RegisterType(&secp256k1fx.TransferOutput{}))
targetCodec.SkipRegistrations(1) // slot 8 hole.
errs.Add(
targetCodec.RegisterType(&secp256k1fx.Credential{}),
targetCodec.RegisterType(&secp256k1fx.Input{}),
targetCodec.RegisterType(&secp256k1fx.OutputOwners{}),
)
// Slots 12-22: Apricot txs + stakeable locks.
errs.Add(
targetCodec.RegisterType(&AddValidatorTx{}),
targetCodec.RegisterType(&AddChainValidatorTx{}),
targetCodec.RegisterType(&AddDelegatorTx{}),
targetCodec.RegisterType(&CreateNetworkTx{}),
targetCodec.RegisterType(&CreateChainTx{}),
targetCodec.RegisterType(&ImportTx{}),
targetCodec.RegisterType(&ExportTx{}),
targetCodec.RegisterType(&AdvanceTimeTx{}),
targetCodec.RegisterType(&RewardValidatorTx{}),
targetCodec.RegisterType(&stakeable.LockIn{}),
targetCodec.RegisterType(&stakeable.LockOut{}),
)
// Slots 23-28: Banff txs + signer.
errs.Add(
targetCodec.RegisterType(&RemoveChainValidatorTx{}),
targetCodec.RegisterType(&TransformChainTx{}),
targetCodec.RegisterType(&AddPermissionlessValidatorTx{}),
targetCodec.RegisterType(&AddPermissionlessDelegatorTx{}),
targetCodec.RegisterType(&signer.Empty{}),
targetCodec.RegisterType(&signer.ProofOfPossession{}),
)
// Slots 29-32: reserved for Banff block slots.
targetCodec.SkipRegistrations(4)
// Slots 33-39: Durango + Etna. Slot 35 in v0 was named
// ConvertChainToL1Tx; its struct layout matches the post-rename
// ConvertNetworkToL1Tx byte-for-byte, so the same Go type is
// registered at both v0 slot 35 and v1 slot 35.
errs.Add(
targetCodec.RegisterType(&TransferChainOwnershipTx{}),
targetCodec.RegisterType(&BaseTx{}),
targetCodec.RegisterType(&ConvertNetworkToL1Tx{}),
targetCodec.RegisterType(&RegisterL1ValidatorTx{}),
targetCodec.RegisterType(&SetL1ValidatorWeightTx{}),
targetCodec.RegisterType(&IncreaseL1ValidatorBalanceTx{}),
targetCodec.RegisterType(&DisableL1ValidatorTx{}),
)
return errors.Join(errs.Err)
}
-24
View File
@@ -1,24 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
// ZAPCodecActivationTimestamp is the unix timestamp at which the
// canonical write codec for the P-chain transitions from V1 to V2.
//
// Value: 0 — ZAP-native is mandatory from genesis (LP-023). Aligned
// with proto/zap_native/codec_select.go: ZAPActivationUnix=0. There
// is no pre-activation regime in this binary; every emitted block
// carries the V2 wire prefix.
//
// V1 remains a registered READ slot for nominal historical
// compatibility (the slot map mechanism that would otherwise serve a
// pre-activation network). In this binary V1 and V2 use identical
// LE wire encoding and identical slot maps, so the version byte is
// the only thing that distinguishes them on the wire.
//
// Read path is timestamp-blind: any binary with this constant baked
// in unmarshals both V1 and V2 bytes via the multi-version codec's
// wire-prefix dispatch. The timestamp gates only the WRITE path —
// which version the mempool/block-builder/signer emits.
const ZAPCodecActivationTimestamp uint64 = 0
+98 -170
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
@@ -12,7 +12,6 @@ import (
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/p2p/gossip"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
@@ -27,176 +26,130 @@ var (
errSignedTxNotInitialized = errors.New("signed tx was never initialized and is not valid")
)
// Tx is a signed transaction
// Tx is a signed transaction: an unsigned tx (itself a zap buffer) plus its
// credentials. The signed bytes are unsigned ‖ creds; there is no codec.
type Tx struct {
// The body of this transaction
Unsigned UnsignedTx `serialize:"true" json:"unsignedTx"`
// Unsigned is the tx body (a zap-backed UnsignedTx).
Unsigned UnsignedTx `json:"unsignedTx"`
// The credentials of this transaction
Creds []verify.Verifiable `serialize:"true" json:"credentials"`
// Creds are the credentials authorizing the tx.
Creds []verify.Verifiable `json:"credentials"`
TxID ids.ID `json:"id"`
bytes []byte
}
func NewSigned(
unsigned UnsignedTx,
c pcodecs.Manager,
signers [][]*secp256k1.PrivateKey,
) (*Tx, error) {
res := &Tx{Unsigned: unsigned}
return res, res.Sign(c, signers)
// NewSigned builds a signed tx from an unsigned tx (already a zap buffer) and
// signs it.
func NewSigned(unsigned UnsignedTx, signers [][]*secp256k1.PrivateKey) (*Tx, error) {
tx := &Tx{Unsigned: unsigned}
return tx, tx.Sign(signers)
}
// Initialize marshals tx with the canonical write codec (v1) and
// derives TxID = hash(signedBytes). This is appropriate ONLY for txs
// built fresh in this process (e.g. by the wallet or by the block
// builder). For txs loaded from disk or received from the wire, use
// InitializeFromBytes — Initialize would re-encode at v1, changing the
// TxID of any tx that was originally serialized at v0.
func (tx *Tx) Initialize(c pcodecs.Manager) error {
signedBytes, err := c.Marshal(CodecVersion, tx)
if err != nil {
return fmt.Errorf("couldn't marshal ProposalTx: %w", err)
}
unsignedBytesLen, err := c.Size(CodecVersion, &tx.Unsigned)
if err != nil {
return fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err)
}
unsignedBytes := signedBytes[:unsignedBytesLen]
tx.SetBytes(unsignedBytes, signedBytes)
return nil
}
// InitializeFromBytes binds the tx to the EXACT signedBytes it was
// decoded from, deriving TxID = hash(signedBytes) under the codec
// version the bytes were originally written at. It never re-marshals,
// so a tx whose bytes were written at v0 keeps its v0-derived TxID
// forever — the chain commitment is therefore stable across the v0->v1
// codec migration.
//
// version is the codec version the bytes were decoded under (returned
// by codec.Manager.Unmarshal). It selects the c.Size(...) call used to
// split signedBytes into unsignedBytes (the prefix the signature
// covers) and the credentials suffix.
func (tx *Tx) InitializeFromBytes(c pcodecs.Manager, version uint16, signedBytes []byte) error {
unsignedBytesLen, err := c.Size(version, &tx.Unsigned)
if err != nil {
return fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err)
}
if unsignedBytesLen > len(signedBytes) {
return fmt.Errorf("unsigned length %d exceeds signed length %d", unsignedBytesLen, len(signedBytes))
}
unsignedBytes := signedBytes[:unsignedBytesLen]
tx.SetBytes(unsignedBytes, signedBytes)
return nil
}
// InitializeFromBytesAtVersion is the byte-preserving init used after
// a struct-level Unmarshal where the outer container (a genesis blob,
// a stored block) decoded the tx field directly — in that case we
// only have the struct value and the codec version it was decoded
// under, not the raw signed bytes (which were never sliced out of the
// outer stream by the codec).
//
// We re-derive signedBytes by Marshal'ing the just-decoded tx at the
// SAME version, then bind via SetBytes. The wire format is
// deterministic and the codec is closed at v0, so the re-marshal
// produces the exact bytes the original v0 producer emitted; therefore
// TxID = hash(re-marshaled v0 bytes) = TxID the chain committed.
//
// This single bounded re-marshal is the ONLY place we allow
// c.Marshal(v0, ...) inside the read path. It is justified because:
// 1. Linearcodec marshal is a pure function of the struct fields
// under a fixed slot map, so the output is byte-equal to the
// original v0 producer's output.
// 2. The struct fields were just populated by c.Unmarshal at the
// same version, so no information has been lost or rotated.
// 3. The c.Size(v0, ...) inverse used by InitializeFromBytes is the
// same path, just in the opposite direction — the size of an
// Unsigned in v0 layout is a fixed function of its fields.
//
// Once every genesis blob and every stored block on disk has been
// re-encoded at v1 (a future migration), this method becomes dead
// code and the v0 codec entry can be removed.
func (tx *Tx) InitializeFromBytesAtVersion(c pcodecs.Manager, version uint16) error {
signedBytes, err := c.Marshal(version, tx)
if err != nil {
return fmt.Errorf("couldn't re-marshal tx at v%d: %w", version, err)
}
return tx.InitializeFromBytes(c, version, signedBytes)
}
func (tx *Tx) SetBytes(unsignedBytes, signedBytes []byte) {
tx.Unsigned.SetBytes(unsignedBytes)
tx.bytes = signedBytes
tx.TxID = hash.ComputeHash256Array(signedBytes)
}
// Parse signed tx starting from its byte representation. The wire
// version is taken from the 2-byte prefix that c.Unmarshal reads;
// c.Size(...) is then called under THAT version so the split point
// between unsignedBytes and the credentials suffix is correct for
// either v0 or v1 layouts.
//
// Parse never re-marshals: TxID = hash(signedBytes) verbatim. This is
// the byte-preserving path that all from-disk and from-wire reads must
// go through to keep TxIDs stable across the v0→v1 migration.
//
// We explicitly pass the codec in Parse since some call sites (genesis,
// state fallback) must use GenesisCodec to admit txs larger than the
// max length of Codec.
func Parse(c pcodecs.Manager, signedBytes []byte) (*Tx, error) {
tx := &Tx{}
version, err := c.Unmarshal(signedBytes, tx)
// Parse wraps signed bytes zero-copy: the leading self-delimiting zap buffer
// is the unsigned body; any remainder is the credential buffer. TxID =
// hash(signedBytes), no re-encoding.
func Parse(signedBytes []byte) (*Tx, error) {
n, err := zapLen(signedBytes)
if err != nil {
return nil, fmt.Errorf("couldn't parse tx: %w", err)
}
if err := tx.InitializeFromBytes(c, version, signedBytes); err != nil {
return nil, fmt.Errorf("couldn't initialize tx from bytes: %w", err)
unsigned, err := parseUnsigned(signedBytes[:n])
if err != nil {
return nil, fmt.Errorf("couldn't parse unsigned tx: %w", err)
}
tx := &Tx{
Unsigned: unsigned,
bytes: signedBytes,
TxID: hash.ComputeHash256Array(signedBytes),
}
if len(signedBytes) > n {
creds, err := parseCredsBuf(signedBytes[n:])
if err != nil {
return nil, fmt.Errorf("couldn't parse credentials: %w", err)
}
tx.Creds = creds
}
return tx, nil
}
func (tx *Tx) Bytes() []byte {
return tx.bytes
// Initialize binds the tx's cached bytes and TxID from its Unsigned buffer and
// Creds. Used for txs built fresh in-process (wallet, block builder) whose
// Unsigned buffer is already set. For txs from disk/wire use Parse.
func (tx *Tx) Initialize() error {
unsignedBytes := tx.Unsigned.Bytes()
signedBytes := unsignedBytes
if len(tx.Creds) > 0 {
credsBuf, err := writeCredsBuf(tx.Creds)
if err != nil {
return fmt.Errorf("couldn't encode credentials: %w", err)
}
signedBytes = concat(unsignedBytes, credsBuf)
}
tx.SetBytes(signedBytes)
return nil
}
func (tx *Tx) Size() int {
return len(tx.bytes)
// SetBytes binds the exact signed bytes and derives TxID = hash(signedBytes).
func (tx *Tx) SetBytes(signedBytes []byte) {
tx.bytes = signedBytes
tx.TxID = hash.ComputeHash256Array(signedBytes)
}
func (tx *Tx) ID() ids.ID {
return tx.TxID
// Sign attaches credentials for the provided signer sets over the unsigned
// bytes, then binds signed bytes = unsigned ‖ creds.
func (tx *Tx) Sign(signers [][]*secp256k1.PrivateKey) error {
unsignedBytes := tx.Unsigned.Bytes()
h := hash.ComputeHash256(unsignedBytes)
tx.Creds = nil
for _, keys := range signers {
cred := &secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}
for i, key := range keys {
sig, err := key.SignHash(h)
if err != nil {
return fmt.Errorf("problem generating credential: %w", err)
}
copy(cred.Sigs[i][:], sig)
}
tx.Creds = append(tx.Creds, cred)
}
signedBytes := unsignedBytes
if len(tx.Creds) > 0 {
credsBuf, err := writeCredsBuf(tx.Creds)
if err != nil {
return fmt.Errorf("couldn't encode credentials: %w", err)
}
signedBytes = concat(unsignedBytes, credsBuf)
}
tx.SetBytes(signedBytes)
return nil
}
func (tx *Tx) GossipID() ids.ID {
return tx.TxID
}
func (tx *Tx) Bytes() []byte { return tx.bytes }
func (tx *Tx) Size() int { return len(tx.bytes) }
func (tx *Tx) ID() ids.ID { return tx.TxID }
func (tx *Tx) GossipID() ids.ID { return tx.TxID }
// UTXOs returns the UTXOs transaction is producing.
// UTXOs returns the UTXOs this transaction produces.
func (tx *Tx) UTXOs() []*lux.UTXO {
outs := tx.Unsigned.Outputs()
utxos := make([]*lux.UTXO, len(outs))
for i, out := range outs {
utxos[i] = &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: tx.TxID,
OutputIndex: uint32(i),
},
Asset: lux.Asset{ID: out.AssetID()},
Out: out.Out,
UTXOID: lux.UTXOID{TxID: tx.TxID, OutputIndex: uint32(i)},
Asset: lux.Asset{ID: out.AssetID()},
Out: out.Out,
}
}
return utxos
}
// InputIDs returns the set of inputs this transaction consumes
func (tx *Tx) InputIDs() set.Set[ids.ID] {
return tx.Unsigned.InputIDs()
}
// InputIDs returns the set of inputs this transaction consumes.
func (tx *Tx) InputIDs() set.Set[ids.ID] { return tx.Unsigned.InputIDs() }
func (tx *Tx) SyntacticVerify(rt *runtime.Runtime) error {
switch {
@@ -209,35 +162,10 @@ func (tx *Tx) SyntacticVerify(rt *runtime.Runtime) error {
}
}
// Sign this transaction with the provided signers
// Note: We explicitly pass the codec in Sign since we may need to sign P-Chain
// genesis txs whose length exceed the max length of txs.Codec.
func (tx *Tx) Sign(c pcodecs.Manager, signers [][]*secp256k1.PrivateKey) error {
unsignedBytes, err := c.Marshal(CodecVersion, &tx.Unsigned)
if err != nil {
return fmt.Errorf("couldn't marshal UnsignedTx: %w", err)
}
// Attach credentials
hash := hash.ComputeHash256(unsignedBytes)
for _, keys := range signers {
cred := &secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}
for i, key := range keys {
sig, err := key.SignHash(hash) // Sign hash
if err != nil {
return fmt.Errorf("problem generating credential: %w", err)
}
copy(cred.Sigs[i][:], sig)
}
tx.Creds = append(tx.Creds, cred) // Attach credential
}
signedBytes, err := c.Marshal(CodecVersion, tx)
if err != nil {
return fmt.Errorf("couldn't marshal ProposalTx: %w", err)
}
tx.SetBytes(unsignedBytes, signedBytes)
return nil
// concat returns a ‖ b as a fresh slice.
func concat(a, b []byte) []byte {
out := make([]byte, 0, len(a)+len(b))
out = append(out, a...)
out = append(out, b...)
return out
}