Compare commits
11 commits
sulkta-mai
...
feat-aux-d
| Author | SHA1 | Date | |
|---|---|---|---|
| 123de1d2a1 | |||
| 6c1480720a | |||
| 7e331636b2 | |||
| 383cde923a | |||
| 36adf8fe8c | |||
| 8091abd1b4 | |||
| 507fd9da15 | |||
| 310b0fe562 | |||
| 68221fbcb5 | |||
| 51a0d0bd77 | |||
| 57b36d3a7c |
9 changed files with 508 additions and 139 deletions
40
.forgejo/workflows/gitleaks.yml
Normal file
40
.forgejo/workflows/gitleaks.yml
Normal 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
|
||||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
|
|
@ -1,58 +0,0 @@
|
||||||
name: Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cargo_publish:
|
|
||||||
name: Publish to Crates.io
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.4.0
|
|
||||||
|
|
||||||
- name: Setup Rust Toolchain
|
|
||||||
uses: actions-rs/toolchain@v1.0.7
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
profile: minimal
|
|
||||||
|
|
||||||
- name: Setup Cargo Plugins
|
|
||||||
uses: actions-rs/cargo@v1.0.3
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-workspaces
|
|
||||||
|
|
||||||
- name: Publish to Crates.io
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cargo workspaces publish --from-git -y
|
|
||||||
|
|
||||||
github_release:
|
|
||||||
name: Create GitHub Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: ["cargo_publish"]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2.4.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: "1.16"
|
|
||||||
|
|
||||||
- name: Release Notes
|
|
||||||
run: |
|
|
||||||
go install github.com/git-chglog/git-chglog/cmd/git-chglog@v0.15.0
|
|
||||||
git-chglog -c .github/chglog/release.yml $(git describe --tags) > RELEASE.md
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
body_path: RELEASE.md
|
|
||||||
draft: true
|
|
||||||
70
.github/workflows/validate.yml
vendored
70
.github/workflows/validate.yml
vendored
|
|
@ -1,70 +0,0 @@
|
||||||
# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: { }
|
|
||||||
|
|
||||||
name: Validate
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ windows-latest, ubuntu-latest, macOS-latest ]
|
|
||||||
rust: [ stable ]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
|
|
||||||
- name: Run cargo check
|
|
||||||
run: cargo check
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test Suite
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ windows-latest, ubuntu-latest, macOS-latest ]
|
|
||||||
rust: [ stable ]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
|
|
||||||
- name: Run cargo test
|
|
||||||
run: cargo test --release
|
|
||||||
|
|
||||||
lints:
|
|
||||||
name: Lints
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
components: rustfmt, clippy
|
|
||||||
|
|
||||||
- name: Run cargo fmt
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
- name: Run cargo clippy
|
|
||||||
run: cargo clippy -- -D warnings
|
|
||||||
18
.gitleaks.toml
Normal file
18
.gitleaks.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# gitleaks config — pallas
|
||||||
|
#
|
||||||
|
# pallas is the Cardano serialization library. Its repo includes:
|
||||||
|
# - test fixtures in test_data/ and crates/*/test_data/ with vkeys,
|
||||||
|
# keyHashes, vssKeys — all public on-chain identifiers
|
||||||
|
# - mainnet-byron-genesis.json — the Byron genesis block which is
|
||||||
|
# PUBLIC chain data, ships signingKey/vssKey of original Byron
|
||||||
|
# delegates as part of the bootstrap state
|
||||||
|
# None of these are secrets in any operational sense.
|
||||||
|
|
||||||
|
[extend]
|
||||||
|
useDefault = true
|
||||||
|
|
||||||
|
[allowlist]
|
||||||
|
description = "Cardano test fixtures + Byron genesis (public on-chain data)"
|
||||||
|
paths = [
|
||||||
|
'''(.*/)?test_data/.*''',
|
||||||
|
]
|
||||||
146
BRANCH-NOTES.md
Normal file
146
BRANCH-NOTES.md
Normal 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.
|
||||||
|
|
@ -545,6 +545,11 @@ impl AsRef<[u8]> for StakePayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StakeAddress {
|
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
|
/// Gets the network assoaciated with this address
|
||||||
pub fn network(&self) -> Network {
|
pub fn network(&self) -> Network {
|
||||||
self.0
|
self.0
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ use pallas_codec::utils::CborWrap;
|
||||||
use pallas_crypto::hash::Hash;
|
use pallas_crypto::hash::Hash;
|
||||||
use pallas_primitives::{
|
use pallas_primitives::{
|
||||||
conway::{
|
conway::{
|
||||||
DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData,
|
AuxiliaryData, Certificate, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId,
|
||||||
PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript,
|
NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput,
|
||||||
PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx,
|
PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag,
|
||||||
Value, WitnessSet,
|
TransactionBody, TransactionInput, Tx, Value, VotingProcedures, WitnessSet,
|
||||||
},
|
},
|
||||||
Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin,
|
Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin,
|
||||||
};
|
};
|
||||||
|
|
@ -240,8 +240,18 @@ impl BuildConway for StagingTransaction {
|
||||||
ttl: self.invalid_from_slot,
|
ttl: self.invalid_from_slot,
|
||||||
validity_interval_start: self.valid_from_slot,
|
validity_interval_start: self.valid_from_slot,
|
||||||
fee: self.fee.unwrap_or_default(),
|
fee: self.fee.unwrap_or_default(),
|
||||||
certificates: None, // TODO
|
certificates: NonEmptySet::from_vec(
|
||||||
withdrawals: None, // TODO
|
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)
|
auxiliary_data_hash: None, // TODO (accept user input)
|
||||||
mint,
|
mint,
|
||||||
script_data_hash,
|
script_data_hash,
|
||||||
|
|
@ -251,7 +261,14 @@ impl BuildConway for StagingTransaction {
|
||||||
collateral_return,
|
collateral_return,
|
||||||
reference_inputs,
|
reference_inputs,
|
||||||
total_collateral: None, // TODO
|
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
|
proposal_procedures: None, // TODO
|
||||||
treasury_value: None, // TODO
|
treasury_value: None, // TODO
|
||||||
donation: None, // TODO
|
donation: None, // TODO
|
||||||
|
|
@ -270,8 +287,14 @@ impl BuildConway for StagingTransaction {
|
||||||
Some(witness_set_redeemers)
|
Some(witness_set_redeemers)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
success: true, // TODO
|
success: true, // TODO
|
||||||
auxiliary_data: None.into(), // 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
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,24 @@ pub struct StagingTransaction {
|
||||||
pub signature_amount_override: Option<u8>,
|
pub signature_amount_override: Option<u8>,
|
||||||
pub change_address: Option<Address>,
|
pub change_address: Option<Address>,
|
||||||
pub language_view: Option<scriptdata::LanguageView>,
|
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 withdrawals: TODO
|
||||||
// pub updates: TODO
|
// pub updates: TODO
|
||||||
// pub auxiliary_data: TODO
|
|
||||||
// pub phase_2_valid: TODO
|
// pub phase_2_valid: TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,6 +386,78 @@ impl StagingTransaction {
|
||||||
self.change_address = None;
|
self.change_address = None;
|
||||||
self
|
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
|
// TODO: Don't want our wrapper types in fields public
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,8 @@ mod tests {
|
||||||
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
|
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
|
||||||
script_data_hash: Some(Bytes32([0; 32])),
|
script_data_hash: Some(Bytes32([0; 32])),
|
||||||
language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])),
|
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();
|
let serialised_tx = serde_json::to_string(&tx).unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue