Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,37 @@ jobs:
- uses: dtolnay/rust-toolchain@1.93
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --all-features

publisher-parity:
name: Real publisher parity
runs-on: ubuntu-latest
env:
STELAE_PARITY_NEW_REVISION: ${{ github.sha }}
STELAE_PARITY_REPORT: ${{ github.workspace }}/target/publisher-parity.json
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@1.93
- uses: Swatinem/rust-cache@v2
- run: cargo test --locked -p stelae-cardano --test publisher_parity -- --ignored --nocapture
- uses: actions/upload-artifact@v4
if: always()
with:
name: publisher-parity-report
path: target/publisher-parity.json
if-no-files-found: warn

registry-parity:
name: Local OCI fault and restart parity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@1.93
- uses: Swatinem/rust-cache@v2
# These tests deliberately spawn disposable, authenticated registry:2
# containers. Keeping the ignored selector here makes Docker a visible
# CI requirement instead of silently treating the registry gate as run.
- run: cargo test --locked -p stelae --all-features --test oci -- --ignored --nocapture
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ deny = [
{ crate = "dolos-cardano", wrappers = ["stelae-cardano", "dolos", "dolos-snapshot", "dolos-testing"], reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-core", wrappers = ["stelae-cardano", "dolos", "dolos-cardano", "dolos-snapshot", "dolos-mithril", "dolos-fjall", "dolos-redb3", "dolos-testing"], reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-fjall", wrappers = ["dolos", "dolos-testing"], reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-flatfiles", wrappers = ["dolos-fjall"], reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-flatfiles", wrappers = ["stelae-cardano", "dolos-fjall"], reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-minibf", reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-minikupo", reason = "Dolos belongs only below the Cardano integration" },
{ crate = "dolos-mithril", wrappers = ["stelae-cardano"], reason = "Dolos belongs only below the Cardano integration" },
Expand Down
104 changes: 104 additions & 0 deletions docs/publisher-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Publisher host parity

This is the executable gate for replacing the Dolos-hosted publisher with
`stelae-publisher`. It compares application hosts without changing the Stelae
wire format or the Dolos consumer.

## Pinned identities

- old host: Dolos `1ae4e91c18a9e1456a3612af402d7b9b97546d30`
(`dolos snapshot publish` and its headless snapshot facade);
- new host base: Stelae `47b38a4`, the merge of publisher-host PR 3;
- protocol/profile: Stelae 0.2.0 and `io.txpipe.dolos.cardano` v1;
- real fixture: `stelae-cardano/tests/fixtures/preview-epoch-0`, identified
and explained by its README;
- expected first-boundary identity:
`sha256:6232659be34afdf24f56784b9ba3db3ccee70cd7476708652b8ba0381bea0027`.

The fixture contains 4,958 public Preview blocks. It starts at origin, crosses
the exact slot-86,400 epoch boundary, and ends at slot 99,140. Both hosts replay
the same immutable bodies into separate fresh stores. Replay stops at the
boundary, as the production backfill loop does, so publication observes an
equivalent checkpoint and identical predecessor/history inputs.

## Automated report

Run the real replay, directory/OCI publication, and consumer restore gate with
Docker available:

```sh
STELAE_PARITY_NEW_REVISION="$(git rev-parse HEAD)" \
STELAE_PARITY_REPORT="$(pwd)/target/publisher-parity.json" \
cargo test --locked -p stelae-cardano --test publisher_parity \
-- --ignored --nocapture
```

The JSON report identifies both hosts and all pins, records pass/fail checks,
and measures replay/publish wall time, process peak RSS, artifact size,
transfer, and scratch use. Directory publication moves zero network bytes and
uses no OCI scratch, so both values are explicitly zero rather than omitted.
The report also records each host's first-push layer and byte transfer counters
from separate repositories in a disposable `registry:2`. CI runs the command
in the `Real publisher parity` job and uploads `publisher-parity-report`.

The test requires all of the following before it writes a passing report:

- identical boundary cursor;
- identical layer descriptors, diffIds, retained epoch-1 dump, and canonical
inscription bytes;
- identical inscription digest;
- actual old/new publication to separate OCI repositories, including equal
transfer results, dry-run/no-op policy, and forced reproduction;
- successful directory and OCI restore of the new-host artifact through the
unchanged Dolos profile consumer;
- reproduction of the same identity from the restored cursor/state/history.

The checked run on 2026-09-13 passed in a debug build. Old/new replay took
2.166/2.013 seconds and directory publication took 1.050/1.025 seconds. Both
directory artifacts were 2,003,124 bytes. Peak process RSS after each host was
416,694,272/421,937,152 bytes. Each OCI host uploaded 1,953,824 layer bytes,
skipped 6,288 bytes already present, and produced a 1,960,112-byte compressed
artifact with the expected identity. These tiny-fixture numbers are regression
evidence only; they make no mainnet throughput or memory claim.

## Local OCI gate

The host parity gate above uses an anonymous loopback registry so both immutable
old-host and new-host code paths publish the real fixture under the same simple
transport policy. The deeper transport suite uses disposable, authenticated
`registry:2` containers and separate repository names. It covers first push
and pull, subsequent missing-
blob-only upload, layer reuse and verified reuse refusal, forced concurrent and
serial agreement, corrupt and wrong-layer refusal, interruption before manifest
publication, restart with the previous `latest` intact, staging cleanup,
credential refusal, and bounded upload/download memory.

```sh
cargo test --locked -p stelae --all-features --test oci \
-- --ignored --nocapture
```

The tests are marked ignored because they require Docker, and the dedicated
`Local OCI fault and restart parity` CI job explicitly selects them. A run on
2026-09-13 passed all 16 Docker-backed cases. The same run observed a 49,495,816
byte uncompressed transport fixture with 4,251,854 peak bytes held while
uploading and 1,255,355 while pulling.

Host policy and failure boundaries remain covered by the workspace suite:
dry-run/no-op/require-new and predecessor-gap decisions, transient retry and
cancellation, publish-pending-before-advance, replay finalization after failure,
fail-closed latest restore, explicit genesis initialization, corrupt or missing
input refusal, and layer-journal/checkpoint resume. The real-fixture host test
and transport fault tests both exercise an actual registry; narrower unit tests
exercise the orchestration seams around it.

## Scope and interpretation

All repositories are loopback/disposable and no production registry is named.
The old source is required only by immutable git revision, not by a future
Dolos release branch. The real fixture and genesis inputs live here by content
identity, so removal of the old Dolos command does not remove the evidence.

This gate establishes code-path parity for packaging. Production canary, soak,
and mainnet performance remain separate operations and must not be inferred
from this fixture.
3 changes: 3 additions & 0 deletions docs/publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ This increment does not ship an image, change a Helm chart, deploy, publish to
a production registry, or claim full production parity. Packaging, operational
cutover, differential live-registry evidence and later pin cleanup remain the
following publisher-pipeline steps.

The executable old/new replay, publication, recovery, resource, and local OCI
evidence is documented in [publisher-parity.md](publisher-parity.md).
6 changes: 6 additions & 0 deletions stelae-cardano/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ tokio-util = { version = "0.7.11", features = ["rt"] }
tracing = "0.1.37"

[dev-dependencies]
dolos-flatfiles = { git = "https://github.com/txpipe/dolos", rev = "1ae4e91c18a9e1456a3612af402d7b9b97546d30" }
dolos-testing = { git = "https://github.com/txpipe/dolos", rev = "1ae4e91c18a9e1456a3612af402d7b9b97546d30" }
hex = "0.4.3"
libc = "0.2.186"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
sha2 = "0.10.9"
tempfile = "3.20.0"
toml = "0.8.13"
zstd-safe = "7.2.4"
2 changes: 2 additions & 0 deletions stelae-cardano/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub enum PublishOutcome {
identity: String,
built: usize,
reused: usize,
transfer: stelae::oci::Transfer,
},
}

Expand Down Expand Up @@ -279,6 +280,7 @@ pub fn publish_once(
identity: published.identity.to_string(),
built: published.layers_built,
reused: published.layers_reused,
transfer: published.transfer,
}
}
}
Expand Down
Binary file not shown.
39 changes: 39 additions & 0 deletions stelae-cardano/tests/fixtures/preview-epoch-0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Preview epoch-zero replay fixture

This fixture is the first 4,958 public Cardano Preview blocks, from origin
through the first epoch transition. It is enough to make both publisher hosts
execute real Byron replay and the epoch-boundary state transition without a
network dependency.

The blocks were acquired from `relay.cnode-m1.demeter.run:3002` on 2026-09-13
with Dolos revision `1ae4e91c18a9e1456a3612af402d7b9b97546d30`. The bounded
capture was interrupted after its first 1,000-block request and resumed with a
4,000-block request; intersection overlap left 4,958 canonical blocks.
`000000.segment` is the Dolos v4 flat-file
format: one independently checksummed zstd frame per raw block, using the
dictionary pinned by that Dolos revision. No store indexes or mutable state are
part of the fixture.

`byron.json`, `shelley.json`, `alonzo.json`, and `conway.json` are the public
Preview genesis inputs supplied by that same revision's `dolos init`. The test
loads the equivalent embedded genesis from the pinned dependency and verifies
the file identities recorded below, so deleting the old publisher command in a
later Dolos release does not delete either the fixture or its inputs.

Identities:

- segment sha256:
`728c5aa9c7c78c4221bdee765f3cd109b630cb00266ab824f6db2bfa43276b4d`
- blocks: 4,958
- first: slot 0, block 0,
`268ae601af8f9214804735910a3301881fbe0eec9936db7d1fb9fc39e93d1e37`
- boundary: slot 86,400, block 4,320,
`4a9761ddc291b0c352d1712b624759132936a07b3e02d1d3bdaaf17b9abfe683`
- final: slot 99,140, block 4,957,
`da6efb517e112a8439820d505286964bcc65ddd073b560a8f04d1c0902f1125f`
- old host: Dolos `1ae4e91c18a9e1456a3612af402d7b9b97546d30`
- protocol/profile: Stelae 0.2.0, `io.txpipe.dolos.cardano` v1
- first-publication predecessor/history: none / empty

The fixture contains only public chain protocol data and public network
configuration. It contains no credentials, registry state, or production data.
Loading
Loading