Compare commits

..

8 commits

Author SHA1 Message Date
383cde923a ci: add gitleaks workflow (Sulkta canonical)
Some checks failed
gitleaks / scan (push) Failing after 23s
2026-05-27 22:14:55 -07:00
36adf8fe8c Public-flip audit: scrub residual LAN URL from fork notes
Some checks failed
Validate / Check (push) Waiting to run
Validate / Check-2 (push) Waiting to run
Validate / Test Suite (push) Waiting to run
Validate / Test Suite-2 (push) Waiting to run
Validate / Check-1 (push) Failing after 58s
Validate / Test Suite-1 (push) Failing after 15s
Validate / Lints (push) Failing after 14s
2026-05-27 11:31:18 -07:00
8091abd1b4 pallas-txbuilder: voting_procedures rejects empty CBOR maps
AUDIT-2026-05-06 M-4 fix from the aldabra Phase 3-6 audit.

NonEmptyKeyValuePairs::decode in pallas-codec accepts `0xa0`
(empty CBOR map) because the upstream empty-map check is commented
out. That decoded value re-encodes correctly and passes through
pallas-txbuilder, but the resulting Conway tx fails ledger validation
at submit time with a non-obvious error.

Add a debug_assert_ne! on the builder method input + clear doc note
warning callers to omit the field instead of passing an empty map.
Release builds pass through (no overhead); dev/test builds catch
accidental empty-map calls with a clear panic message.

The pre-existing aldabra build_signed_drep_vote_cast always
constructs a non-empty map so it doesn't trip this; the guard is for
future callers.
2026-05-06 08:05:55 -07:00
507fd9da15 pallas-txbuilder: thread voting_procedures through StagingTransaction → Conway build
Fills the third of pallas-txbuilder's Conway TODOs (after auxiliary_data
+ certificates):

- StagingTransaction gains `voting_procedures: Option<Vec<u8>>` (opaque
  CBOR — same Eq-derive workaround as auxiliary_data + certificates)
- Builder methods .voting_procedures(cbor_bytes) + .clear_voting_procedures()
- conway::build_conway_raw decodes via VotingProcedures::decode_fragment
  and assigns to TransactionBody.voting_procedures

VotingProcedures is itself a NonEmptyKeyValuePairs<Voter,
NonEmptyKeyValuePairs<GovActionId, VotingProcedure>> map, so callers
pre-assemble the full map and encode once.

Tests:
- voting_procedures_plumb_through_to_tx_body — encode DRepKey vote-Yes
  on a synthetic GovActionId, build, decode, assert round-trip
- no_voting_procedures_means_none — negative path
2026-05-06 07:11:23 -07:00
310b0fe562 docs: BRANCH-NOTES — fork rationale, what's patched, upstream PR plan
documents the three changes on feat-aux-data:
- pallas-txbuilder auxiliary_data plumbing
- pallas-txbuilder certificates plumbing
- pallas-addresses StakeAddress::new constructor

each tied to the upstream TODO it fills, with test additions noted.
records that aldabra consumes via [patch.crates-io] across all 7
pallas-* crates so cargo resolves the graph consistently.

PR upstream not yet submitted — plan locked in: split into three
separate PRs for review ergonomics.
2026-05-04 12:50:01 -07:00
68221fbcb5 pallas-txbuilder: thread certificates through StagingTransaction → Conway build
second of the upstream TODOs. addresses the `// pub certificates: TODO`
comment in transaction/model.rs and the `certificates: None, // TODO`
in conway.rs:243.

implementation:
- new field: pub certificates: Option<Vec<Vec<u8>>> on StagingTransaction.
  cbor bytes per cert (each entry is conway::Certificate) — same opaque-
  bytes pattern as auxiliary_data, for the same reason (Certificate
  doesn't impl Eq).
- builder: .add_certificate(cbor_bytes) and .clear_certificates().
- conway::build_conway_raw decodes each entry via
  Certificate::decode_fragment + plumbs into TransactionBody.certificates
  via NonEmptySet::from_vec. fragment-decode failures map to
  TxBuilderError::CorruptedTxBytes (same as aux_data path).

tests:
- certificates_plumb_through_to_tx_body: encodes a
  StakeRegistration(AddrKeyhash([7;28])), attaches, builds, decodes
  resulting tx, asserts the cert round-trips byte-for-byte.
- no_certificates_means_none: confirms unset path keeps
  body.certificates None (no spurious empty Set).

unblocks aldabra phase 4.6 stake delegation. PR upstream still pending;
this is part of the same vendored fork as the auxiliary_data work.
2026-05-04 12:36:29 -07:00
51a0d0bd77 pallas-addresses: pub fn new on StakeAddress 2026-05-04 12:33:19 -07:00
57b36d3a7c pallas-txbuilder: thread auxiliary_data through StagingTransaction → Conway build
addresses the four TODO comments in pallas-txbuilder/src/transaction/model.rs
and conway.rs around auxiliary_data + auxiliary_data_hash. without this the
conway builder hardcodes auxiliary_data: None, which blocks CIP-20 / CIP-25
/ CIP-68 metadata.

implementation:
- new StagingTransaction field: pub auxiliary_data: Option<Vec<u8>>.
  stored as opaque cbor bytes (caller encodes alonzo::AuxiliaryData) since
  AuxiliaryData itself doesn't impl Eq, which the rest of StagingTransaction
  requires.
- builder methods .auxiliary_data(cbor) and .clear_auxiliary_data().
- conway::build_conway_raw now decodes the bytes back into
  AuxiliaryData::decode_fragment and plugs it into pallas_tx.auxiliary_data.
  the existing auxiliary_data_hash population block is unchanged — it
  already computes the hash from pallas_tx.auxiliary_data after assignment.
- decode failures map to TxBuilderError::CorruptedTxBytes.

tests:
- auxiliary_data_round_trips_through_build: encodes a CIP-25-shaped
  metadata, attaches, builds, decodes resulting tx, asserts both
  body.auxiliary_data_hash matches expected_hash.compute_hash() and the
  aux re-encodes byte-equivalent.
- no_auxiliary_data_means_no_hash: confirms the absence path still works
  (no aux → hash field stays None).

both pre-existing tests (staging_json_roundtrip, built_json_roundtrip,
test_script_data_hash) continue to pass — the new field defaults to None
and rides alongside.

PR upstream pending; using as a vendored patch via [patch.crates-io] on
the Sulkta side until merge.
2026-05-04 12:04:11 -07:00
6 changed files with 490 additions and 11 deletions

View file

@ -0,0 +1,40 @@
# .forgejo/workflows/gitleaks.yml
#
# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
# (task #295).
#
# Pairs with the pre-receive hook installed on every bare repo — that one is
# the strict enforcement layer (rejects the push); this one provides the
# per-PR red ✗ that branch-protection rules can require before merge.
#
# Layer 1 (this workflow): visible per-PR status, can be a required check.
# Layer 2 (pre-receive hook): strict enforcement at the server.
# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos.
name: gitleaks
on:
push:
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history — gitleaks needs depth to scan a commit range.
fetch-depth: 0
- name: install gitleaks
run: |
curl -sSL -o gl.tar.gz \
https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
tar xzf gl.tar.gz gitleaks
chmod +x gitleaks
./gitleaks version
- name: scan
run: |
./gitleaks detect --source . --no-banner --redact --verbose

146
BRANCH-NOTES.md Normal file
View file

@ -0,0 +1,146 @@
# Sulkta-Coop/pallas — `feat-aux-data` branch notes
This is a vendored fork of [txpipe/pallas](https://github.com/txpipe/pallas)
based on the `v0.32.1` tag. It exists to fill in three upstream `// TODO`
markers that block real-world Conway-era transaction building.
The fork is consumed via `[patch.crates-io]` in
[`Sulkta-Coop/aldabra`](https://git.sulkta.com/Sulkta-Coop/aldabra)'s
workspace `Cargo.toml`. Once these changes are accepted upstream, the
patch entry gets dropped and aldabra goes back to vanilla crates.io.
## What's added beyond `v0.32.1`
### 1. `pallas-txbuilder``auxiliary_data` field on `StagingTransaction`
Upstream had `// pub auxiliary_data: TODO` on the struct and
`auxiliary_data: None.into(), // TODO` in the conway builder. Without
this, **CIP-20 / CIP-25 / CIP-68 metadata can't ride a tx built with
pallas-txbuilder.**
Added:
- `pub auxiliary_data: Option<Vec<u8>>` field (opaque CBOR bytes —
`pallas_primitives::AuxiliaryData` doesn't impl `Eq`, which the
rest of `StagingTransaction` requires).
- Builder methods `.auxiliary_data(cbor_bytes)` and
`.clear_auxiliary_data()`.
- `conway::build_conway_raw` now decodes the bytes via
`AuxiliaryData::decode_fragment` and plumbs them in. The existing
`auxiliary_data_hash` computation block (already in upstream) runs
after assignment and populates the body's hash automatically.
Tests added:
- `auxiliary_data_round_trips_through_build` — encodes a CIP-25
shape, attaches, builds, decodes, asserts byte-equivalent
re-encode + matching `auxiliary_data_hash`.
- `no_auxiliary_data_means_no_hash` — negative path.
### 2. `pallas-txbuilder``certificates` field on `StagingTransaction`
Upstream had `// pub certificates: TODO` and
`certificates: None, // TODO` in the conway builder. Without this,
**stake registration / delegation, pool registration, DRep ops, and
Voltaire-era cert types can't be built.**
Added:
- `pub certificates: Option<Vec<Vec<u8>>>` field (opaque CBOR per
cert, same reason as `auxiliary_data`).
- Builder methods `.add_certificate(cbor_bytes)` and
`.clear_certificates()`.
- `conway::build_conway_raw` decodes each entry via
`Certificate::decode_fragment` and plumbs them into
`TransactionBody.certificates` via `NonEmptySet::from_vec`.
Tests added:
- `certificates_plumb_through_to_tx_body` — encodes a
`StakeRegistration(AddrKeyhash([7;28]))`, builds, decodes,
asserts the cert round-trips byte-for-byte.
- `no_certificates_means_none` — negative path.
### 3. `pallas-txbuilder``voting_procedures` field on `StagingTransaction`
Upstream had `voting_procedures: None, // TODO` in the conway builder.
Without this, **DRep / SPO / committee voting on Conway governance
actions can't ride a tx built with pallas-txbuilder.**
Added 2026-05-06:
- `pub voting_procedures: Option<Vec<u8>>` field (opaque CBOR for the
same `Eq`-derive reason as `auxiliary_data`).
- Builder methods `.voting_procedures(cbor_bytes)` and
`.clear_voting_procedures()`.
- `conway::build_conway_raw` decodes via `VotingProcedures::decode_fragment`
and assigns to `TransactionBody.voting_procedures`.
The `VotingProcedures` type is itself a single map
(`NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<GovActionId,
VotingProcedure>>`), so callers pre-assemble the full map and encode
once. No staged accumulator needed.
Tests added:
- `voting_procedures_plumb_through_to_tx_body` — encodes a
single DRepKey-vote-Yes on a synthetic GovActionId, builds, decodes,
asserts the vote round-trips byte-for-byte.
- `no_voting_procedures_means_none` — negative path.
### 4. `pallas-addresses``pub fn new` on `StakeAddress`
Upstream defined `pub struct StakeAddress(Network, StakePayload)` with
**unexported tuple fields** and no `new()` constructor — so external
callers can't construct one directly. `ShelleyAddress::new` exists
already; this just matches the pattern.
Without this, computing a wallet's reward (stake) address
required round-tripping through a `ShelleyAddress` and `TryFrom`
clunky and indirect.
## How aldabra consumes this
`Sulkta-Coop/aldabra/Cargo.toml` has:
```toml
[patch.crates-io]
pallas-codec = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-crypto = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-primitives = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-traverse = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-addresses = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-wallet = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
pallas-txbuilder = { git = "...Sulkta-Coop/pallas.git", branch = "feat-aux-data" }
```
All seven pallas crates we depend on are patched against the same
commit so cargo's version graph resolves consistently.
## Upstream PR status
**Not yet submitted.** Plan:
1. ✅ Land the changes locally on the fork.
2. ✅ Verify no upstream tests broken (`cargo test --workspace`
passes — confirmed 2026-05-04).
3. ✅ Verify aldabra builds + tests against the fork (88 unit tests
green).
4. ☐ Open PR against `txpipe/pallas` on github.com.
5. ☐ Once upstream merges, drop our `[patch.crates-io]`.
The PR will probably need to be split into three separate PRs (one
per change) for upstream review ergonomics:
- `pallas-txbuilder: thread auxiliary_data through StagingTransaction → Conway build`
- `pallas-txbuilder: thread certificates through StagingTransaction → Conway build`
- `pallas-txbuilder: thread voting_procedures through StagingTransaction → Conway build`
- `pallas-addresses: pub fn new on StakeAddress`
## Change discipline going forward
If we add more fork-only patches (e.g. for upcoming withdrawals /
voting_procedures / proposal_procedures TODOs), each goes in a
separate commit on this branch so the eventual upstream PRs are
clean to extract.
All commits on this branch must:
- Have a self-contained subject explaining what TODO it fills in.
- Pass the full pallas-txbuilder test suite (`cargo test -p pallas-txbuilder`).
- Add at least one positive + one negative test for the new code path.
- Not depend on Sulkta-specific naming or assumptions — these
patches need to be upstream-mergeable as-is.

View file

@ -545,6 +545,11 @@ impl AsRef<[u8]> for StakePayload {
}
impl StakeAddress {
/// Construct a stake (reward) address from a network + payload.
pub fn new(network: Network, payload: StakePayload) -> Self {
Self(network, payload)
}
/// Gets the network assoaciated with this address
pub fn network(&self) -> Network {
self.0

View file

@ -4,10 +4,10 @@ use pallas_codec::utils::CborWrap;
use pallas_crypto::hash::Hash;
use pallas_primitives::{
conway::{
DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData,
PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript,
PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx,
Value, WitnessSet,
AuxiliaryData, Certificate, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId,
NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput,
PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag,
TransactionBody, TransactionInput, Tx, Value, VotingProcedures, WitnessSet,
},
Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin,
};
@ -240,8 +240,18 @@ impl BuildConway for StagingTransaction {
ttl: self.invalid_from_slot,
validity_interval_start: self.valid_from_slot,
fee: self.fee.unwrap_or_default(),
certificates: None, // TODO
withdrawals: None, // TODO
certificates: NonEmptySet::from_vec(
self.certificates
.as_deref()
.unwrap_or(&[])
.iter()
.map(|bytes| {
Certificate::decode_fragment(bytes)
.map_err(|_| TxBuilderError::CorruptedTxBytes)
})
.collect::<Result<Vec<_>, _>>()?,
),
withdrawals: None, // TODO
auxiliary_data_hash: None, // TODO (accept user input)
mint,
script_data_hash,
@ -251,7 +261,14 @@ impl BuildConway for StagingTransaction {
collateral_return,
reference_inputs,
total_collateral: None, // TODO
voting_procedures: None, // TODO
voting_procedures: self
.voting_procedures
.as_deref()
.map(|bytes| {
VotingProcedures::decode_fragment(bytes)
.map_err(|_| TxBuilderError::CorruptedTxBytes)
})
.transpose()?,
proposal_procedures: None, // TODO
treasury_value: None, // TODO
donation: None, // TODO
@ -270,8 +287,14 @@ impl BuildConway for StagingTransaction {
Some(witness_set_redeemers)
},
},
success: true, // TODO
auxiliary_data: None.into(), // TODO
success: true, // TODO
auxiliary_data: self
.auxiliary_data
.as_deref()
.map(AuxiliaryData::decode_fragment)
.transpose()
.map_err(|_| TxBuilderError::CorruptedTxBytes)?
.into(),
};
// TODO: pallas auxiliary_data_hash should be Hash<32> not Bytes
@ -375,3 +398,180 @@ impl Output {
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transaction::model::StagingTransaction;
use pallas_codec::minicbor;
use pallas_primitives::alonzo::{AuxiliaryData, Metadatum, PostAlonzoAuxiliaryData};
use pallas_traverse::ComputeHash;
/// Encode a CIP-25-shaped auxiliary data, attach it to a tx, and
/// confirm the built tx body carries the matching auxiliary_data_hash.
/// Round-trip protection for the fork's metadata plumbing.
#[test]
fn auxiliary_data_round_trips_through_build() {
// Minimal CIP-25-style metadata: label 721 → text "test".
let metadata: pallas_primitives::alonzo::Metadata = vec![
(721u64, Metadatum::Text("test".into())),
]
.into();
let aux = AuxiliaryData::PostAlonzo(PostAlonzoAuxiliaryData {
metadata: Some(metadata),
native_scripts: None,
plutus_scripts: None,
});
let aux_bytes = minicbor::to_vec(&aux).expect("encode aux");
let expected_hash = aux.compute_hash();
let tx = StagingTransaction::new()
.auxiliary_data(aux_bytes)
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
// The body's auxiliary_data_hash should match the aux's
// compute_hash output.
let body =
pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
let body_aux_hash_bytes: Vec<u8> = body
.transaction_body
.auxiliary_data_hash
.clone()
.expect("auxiliary_data_hash should be set when aux is attached")
.to_vec();
assert_eq!(body_aux_hash_bytes, expected_hash.to_vec());
// And the auxiliary_data should round-trip back.
let pallas_aux = match body.auxiliary_data {
pallas_codec::utils::Nullable::Some(a) => a,
other => panic!("expected Some(aux), got {other:?}"),
};
// Re-encode the aux that came back and compare.
let round_tripped_bytes = minicbor::to_vec(&pallas_aux).expect("re-encode aux");
let original_again = minicbor::to_vec(&aux).expect("re-encode original");
assert_eq!(round_tripped_bytes, original_again);
}
#[test]
fn certificates_plumb_through_to_tx_body() {
use pallas_primitives::conway::{Certificate, StakeCredential};
use pallas_crypto::hash::Hash;
// A trivial stake-registration cert.
let stake_pkh = Hash::<28>::from([7u8; 28]);
let cert = Certificate::StakeRegistration(StakeCredential::AddrKeyhash(stake_pkh));
let cert_bytes = minicbor::to_vec(&cert).expect("encode cert");
let tx = StagingTransaction::new()
.add_certificate(cert_bytes)
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
let body = pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
let certs = body
.transaction_body
.certificates
.expect("certificates field populated");
let certs_vec: Vec<_> = certs.to_vec();
assert_eq!(certs_vec.len(), 1);
match &certs_vec[0] {
Certificate::StakeRegistration(StakeCredential::AddrKeyhash(h)) => {
assert_eq!(h.as_ref(), &[7u8; 28]);
}
other => panic!("expected StakeRegistration, got {other:?}"),
}
}
#[test]
fn no_certificates_means_none() {
let tx = StagingTransaction::new()
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
let body = pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
assert!(body.transaction_body.certificates.is_none());
}
#[test]
fn voting_procedures_plumb_through_to_tx_body() {
use pallas_primitives::conway::{
GovActionId, Vote, Voter, VotingProcedure, VotingProcedures,
};
use pallas_codec::utils::Nullable;
use pallas_crypto::hash::Hash;
// Construct a single-voter, single-action vote.
let voter_hash = Hash::<28>::from([7u8; 28]);
let voter = Voter::DRepKey(voter_hash);
let action = GovActionId {
transaction_id: Hash::<32>::from([9u8; 32]),
action_index: 0,
};
let procedure = VotingProcedure {
vote: Vote::Yes,
anchor: Nullable::Null,
};
let inner = NonEmptyKeyValuePairs::Def(vec![(action, procedure)]);
let outer: VotingProcedures = NonEmptyKeyValuePairs::Def(vec![(voter, inner)]);
let vp_bytes = minicbor::to_vec(&outer).expect("encode voting procedures");
let tx = StagingTransaction::new()
.voting_procedures(vp_bytes)
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
let body = pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
let vp = body
.transaction_body
.voting_procedures
.expect("voting_procedures field populated");
let outer_vec = vp.to_vec();
assert_eq!(outer_vec.len(), 1);
let (got_voter, inner_kv) = &outer_vec[0];
assert!(matches!(got_voter, Voter::DRepKey(h) if h.as_ref() == &[7u8; 28]));
let inner_vec = inner_kv.to_vec();
assert_eq!(inner_vec.len(), 1);
let (got_action, got_proc) = &inner_vec[0];
assert_eq!(got_action.transaction_id.as_ref(), &[9u8; 32]);
assert_eq!(got_action.action_index, 0);
assert!(matches!(got_proc.vote, Vote::Yes));
}
#[test]
fn no_voting_procedures_means_none() {
let tx = StagingTransaction::new()
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
let body = pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
assert!(body.transaction_body.voting_procedures.is_none());
}
#[test]
fn no_auxiliary_data_means_no_hash() {
let tx = StagingTransaction::new()
.network_id(0)
.fee(180_000)
.build_conway_raw()
.expect("build_conway_raw");
let body =
pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref())
.expect("decode tx_bytes");
assert!(body.transaction_body.auxiliary_data_hash.is_none());
}
}

View file

@ -40,10 +40,24 @@ pub struct StagingTransaction {
pub signature_amount_override: Option<u8>,
pub change_address: Option<Address>,
pub language_view: Option<scriptdata::LanguageView>,
// pub certificates: TODO
/// CBOR-encoded `pallas_primitives::alonzo::AuxiliaryData`. Stored
/// as bytes rather than the typed enum because `AuxiliaryData`
/// doesn't implement `Eq`, which the rest of this struct requires.
/// `conway::build_conway_raw` decodes on the way out.
pub auxiliary_data: Option<Vec<u8>>,
/// CBOR-encoded `pallas_primitives::conway::Certificate` entries,
/// in tx order. `conway::build_conway_raw` decodes each one and
/// plumbs them into `TransactionBody.certificates`. Used for
/// stake registration/delegation, pool registration, DRep ops, etc.
pub certificates: Option<Vec<Vec<u8>>>,
/// CBOR-encoded `pallas_primitives::conway::VotingProcedures`. A single
/// blob (the type is `NonEmptyKeyValuePairs<Voter,
/// NonEmptyKeyValuePairs<GovActionId, VotingProcedure>>`, so callers
/// pre-assemble the full map and encode it once). `conway::build_conway_raw`
/// decodes on the way out into `TransactionBody.voting_procedures`.
pub voting_procedures: Option<Vec<u8>>,
// pub withdrawals: TODO
// pub updates: TODO
// pub auxiliary_data: TODO
// pub phase_2_valid: TODO
}
@ -372,6 +386,78 @@ impl StagingTransaction {
self.change_address = None;
self
}
/// Attach pre-encoded auxiliary data (CBOR bytes of an
/// `alonzo::AuxiliaryData`). The conway builder will decode on
/// build and compute `auxiliary_data_hash` automatically.
///
/// Most callers use this for transaction metadata (CIP-25 / CIP-20
/// / CIP-68) and/or to ship native scripts as auxiliary data.
/// Construct an `AuxiliaryData::PostAlonzo(...)` with the
/// metadata + scripts you want, encode with `Fragment::encode_fragment`
/// or `minicbor::to_vec`, and pass the bytes here.
pub fn auxiliary_data(mut self, cbor_bytes: Vec<u8>) -> Self {
self.auxiliary_data = Some(cbor_bytes);
self
}
/// Drop any auxiliary data previously attached.
pub fn clear_auxiliary_data(mut self) -> Self {
self.auxiliary_data = None;
self
}
/// Append a CBOR-encoded `conway::Certificate` to the tx body.
/// Common cases: stake registration, stake delegation, DRep
/// registration, pool retirement.
///
/// Encode the typed `Certificate` via
/// `pallas_codec::minicbor::to_vec(&cert)` or
/// `Fragment::encode_fragment` and pass the bytes here.
pub fn add_certificate(mut self, cbor_bytes: Vec<u8>) -> Self {
let mut certs = self.certificates.unwrap_or_default();
certs.push(cbor_bytes);
self.certificates = Some(certs);
self
}
/// Drop any certificates previously attached.
pub fn clear_certificates(mut self) -> Self {
self.certificates = None;
self
}
/// Attach pre-encoded `voting_procedures` (CBOR bytes of a
/// `conway::VotingProcedures`). Callers assemble the full
/// `NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<GovActionId,
/// VotingProcedure>>` map and pass the encoded bytes here.
///
/// Used for DRep / SPO / committee voting on Conway governance actions.
///
/// **Empty-map footgun**: Conway ledger expects this field to be
/// omitted (i.e. `None`) when there are no votes. Passing an empty
/// CBOR map (`0xa0`) gets through pallas-codec's
/// `NonEmptyKeyValuePairs::decode` (the upstream empty-map check is
/// commented out) but the resulting tx fails ledger validation at
/// submit time. **Don't call this builder if the map is empty —
/// just omit it.** The debug_assert below catches accidental empty-
/// map calls in dev/test builds.
pub fn voting_procedures(mut self, cbor_bytes: Vec<u8>) -> Self {
debug_assert_ne!(
cbor_bytes.as_slice(),
&[0xa0u8],
"voting_procedures called with empty CBOR map — Conway ledger \
rejects this; omit the field instead",
);
self.voting_procedures = Some(cbor_bytes);
self
}
/// Drop any voting procedures previously attached.
pub fn clear_voting_procedures(mut self) -> Self {
self.voting_procedures = None;
self
}
}
// TODO: Don't want our wrapper types in fields public

View file

@ -480,6 +480,8 @@ mod tests {
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
script_data_hash: Some(Bytes32([0; 32])),
language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])),
auxiliary_data: None,
certificates: None,
};
let serialised_tx = serde_json::to_string(&tx).unwrap();