diff --git a/.forgejo/workflows/gitleaks.yml b/.forgejo/workflows/gitleaks.yml deleted file mode 100644 index 10d7847..0000000 --- a/.forgejo/workflows/gitleaks.yml +++ /dev/null @@ -1,40 +0,0 @@ -# .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 diff --git a/BRANCH-NOTES.md b/BRANCH-NOTES.md deleted file mode 100644 index ce9692c..0000000 --- a/BRANCH-NOTES.md +++ /dev/null @@ -1,146 +0,0 @@ -# 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>` 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>>` 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>` 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>`), 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. diff --git a/pallas-addresses/src/lib.rs b/pallas-addresses/src/lib.rs index 1d14f15..8ffbbfc 100644 --- a/pallas-addresses/src/lib.rs +++ b/pallas-addresses/src/lib.rs @@ -545,11 +545,6 @@ 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 diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index 9d7aab2..a958078 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -4,10 +4,10 @@ use pallas_codec::utils::CborWrap; use pallas_crypto::hash::Hash; use pallas_primitives::{ conway::{ - AuxiliaryData, Certificate, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, - NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput, - PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag, - TransactionBody, TransactionInput, Tx, Value, VotingProcedures, WitnessSet, + DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData, + PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, + PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx, + Value, WitnessSet, }, Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin, }; @@ -240,18 +240,8 @@ impl BuildConway for StagingTransaction { ttl: self.invalid_from_slot, validity_interval_start: self.valid_from_slot, fee: self.fee.unwrap_or_default(), - certificates: NonEmptySet::from_vec( - self.certificates - .as_deref() - .unwrap_or(&[]) - .iter() - .map(|bytes| { - Certificate::decode_fragment(bytes) - .map_err(|_| TxBuilderError::CorruptedTxBytes) - }) - .collect::, _>>()?, - ), - withdrawals: None, // TODO + certificates: None, // TODO + withdrawals: None, // TODO auxiliary_data_hash: None, // TODO (accept user input) mint, script_data_hash, @@ -261,14 +251,7 @@ impl BuildConway for StagingTransaction { collateral_return, reference_inputs, total_collateral: None, // TODO - voting_procedures: self - .voting_procedures - .as_deref() - .map(|bytes| { - VotingProcedures::decode_fragment(bytes) - .map_err(|_| TxBuilderError::CorruptedTxBytes) - }) - .transpose()?, + voting_procedures: None, // TODO proposal_procedures: None, // TODO treasury_value: None, // TODO donation: None, // TODO @@ -287,14 +270,8 @@ impl BuildConway for StagingTransaction { Some(witness_set_redeemers) }, }, - success: true, // TODO - auxiliary_data: self - .auxiliary_data - .as_deref() - .map(AuxiliaryData::decode_fragment) - .transpose() - .map_err(|_| TxBuilderError::CorruptedTxBytes)? - .into(), + success: true, // TODO + auxiliary_data: None.into(), // TODO }; // TODO: pallas auxiliary_data_hash should be Hash<32> not Bytes @@ -398,180 +375,3 @@ 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 = 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()); - } -} diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index 8e20281..5e1227e 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -40,24 +40,10 @@ pub struct StagingTransaction { pub signature_amount_override: Option, pub change_address: Option
, pub language_view: Option, - /// 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>, - /// 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>>, - /// CBOR-encoded `pallas_primitives::conway::VotingProcedures`. A single - /// blob (the type is `NonEmptyKeyValuePairs>`, 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>, + // pub certificates: TODO // pub withdrawals: TODO // pub updates: TODO + // pub auxiliary_data: TODO // pub phase_2_valid: TODO } @@ -386,78 +372,6 @@ 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) -> 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) -> 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>` 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) -> 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 diff --git a/pallas-txbuilder/src/transaction/serialise.rs b/pallas-txbuilder/src/transaction/serialise.rs index f1b8bd5..e60008f 100644 --- a/pallas-txbuilder/src/transaction/serialise.rs +++ b/pallas-txbuilder/src/transaction/serialise.rs @@ -480,8 +480,6 @@ 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();