Date: 2026-05-19 Status: Draft
Adding a new Signed Entity Type typically requires changes across several parts of the system:
- Define the Signed Entity type and its beacon.
- Optionally define an associated configuration, transmitted through the Mithril Protocol Configuration mechanism.
- Implement the Signable builder, which computes the message to be signed for the Signed Entity.
- Define the Signed Entity artifacts type, which contain the metadata needed to reconstruct the Signed Entity.
- Implement the Signed Entity artifact builder, which may publish data to third-party services such as cloud storage.
- Update the Mithril client library, WASM library, and CLI to support the new Signed Entity.
Some Signed Entities may require additional steps depending on their requirements and capabilities.
Because this process affects multiple components and requires careful planning and testing, adding a new Signed Entity is expected to be done incrementally, usually across multiple pull requests.
When adding a new Signed Entity, follow these guidelines:
-
Mirror the new Signed Entity type in the
SignedEntityTypeMessageenum: This enum is used for communication between Mithril nodes. It mirrors the Signed Entity type enum and adds anUnknownvariant so that nodes can gracefully handle variants they do not know yet. -
Define the new Signed Entity type as unstable initially: Add its discriminant to the
UNSTABLE_DISCRIMINANTSconst array while the Signed Entity is still being implemented. This allows the type to exist in the codebase while excluding it from production signing flows or tests that require a fully implemented Signed Entity.Remove the discriminant from
UNSTABLE_DISCRIMINANTSonce the Signed Entity is fully implemented and covered by end-to-end tests. -
Add integration tests once the Signable builder is implemented: Add or extend signer and aggregator integration tests for the new Signed Entity as soon as its Signable builder is ready.
- New Signed Entities can be implemented incrementally, making planning and review easier.
- Partial implementations are expected, reviewers should not require a Signed Entity to be complete in a single PR.
- New Signed Entities are tested earlier in the development process.
- The unstable discriminant list becomes a temporary maintenance point and must be updated once the Signed Entity is fully supported.
Date: 2025-04-23 Status: Accepted
Some tests are inherently too slow to run on every CI execution — typically because they rely on complex logic or cryptography that cannot be optimized further. This was significantly slowing down the CI pipeline, in some cases doubling the total test run time and hurting developer productivity.
A test must be categorized as "slow" if it consistently takes more than 30 seconds to run on CI.
Because run times can vary significantly based on environmental conditions (e.g. machine performance or load), developers may also categorize a test as "slow" at their discretion if it takes more than 15 seconds on their local machine.
The CI should automatically flag tests exceeding the 30-second threshold using tools such as cargo nextest slow test output, so that slow tests can be identified and categorized.
To mark a test as "slow":
- Move it into a dedicated
slowsubmodule, placed at the end of thetestsmodule:
#[cfg(test)]
mod tests {
#[test]
fn normal_test() { }
mod slow {
use super::*;
#[test]
fn heavy_test() {
// heavy test logic
}
}
}- If the slow test belongs to a package not yet listed in
filter-slow-tests.sh, or if its source path is not already covered by an existing entry, update the script's slow test entries accordingly. See the script's README.md for more details.
Caution
If a test is moved to a slow submodule but the script is not updated, it will never run on CI.
Both developers and CI should run slow tests only when a related source path has changed or when explicitly requested.
Use the filter-slow-tests.sh script to generate a
cargo nextest filter expression that selectively includes slow tests based on which source paths have changed.
The CI must run this script automatically to keep total test run times low.
The CI must also allow developers to explicitly request a full test run, regardless of which source paths have changed,
via the run-slow-tests Pull Request label.
- Faster testing feedback loop for developers
- Faster CI runs in the common case
- Developers can easily skip slow tests in local runs:
- For
cargo test:cargo test -- --skip slow:: - For
cargo nextest:cargo nextest run -E "not test(#*slow::*)"
- For
- Developers can run only slow tests when needed
cargo nextest run --workspace --profile ci -E "$(.github/workflows/scripts/filter-slow-tests.sh)"filter-slow-tests.shbecomes a maintenance dependency. Stale or missing entries will silently cause slow tests to be excluded from CI runs. Entries should be reviewed whenever a slow test is added, renamed, or moved.- Marking a test as "slow" and keeping the script in sync introduces a small but ongoing maintenance overhead.
Date: 2025-04-15 Status: Accepted
As specified in DEV-ADR-6, arithmetic wrapper types enforce type safety for numeric values.
However, when such types are exposed to WebAssembly, wasm_bindgen represents their inner tuple field as .0 in
JavaScript, forcing callers to access the underlying primitive through that field:
const block_number = cardano_transaction_proof.latest_block_number;
// Expected usage:
console.log(block_number);
// Actual usage:
console.log(block_number.0);The .0 accessor is an escape hatch for Rust interop, not an intended public interface. Exposing it to JavaScript
consumers makes the API awkward and leaks implementation details.
-
Do not annotate arithmetic wrapper types with
#[wasm_bindgen]: The attribute should be absent from all wrapper types so they are never exposed as JavaScript classes. -
Hide arithmetic wrapper fields on exposed types: On any
#[wasm_bindgen]struct that holds an arithmetic wrapper field, mark that field with#[cfg_attr(target_family = "wasm", wasm_bindgen(skip))]to prevent it from being directly accessible in JavaScript. -
Expose the value through a getter instead: Provide a
#[wasm_bindgen(getter)]method that returns the underlying primitive type, scoped to#[cfg(target_family = "wasm")]so Rust code continues to access the field directly.
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(target_family = "wasm", wasm_bindgen)]
pub struct ExampleType {
#[cfg_attr(target_family = "wasm", wasm_bindgen(skip))]
pub block_number: BlockNumber,
}
#[cfg(target_family = "wasm")]
#[wasm_bindgen]
impl ExampleType {
#[wasm_bindgen(getter)]
pub fn block_number(&self) -> u64 {
*self.block_number
}
}- Arithmetic wrapper types remain internal to Rust and are never surfaced as JavaScript classes.
- JavaScript consumers receive plain primitives (e.g.
number,bigint) from getters, keeping the API idiomatic and free of Rust-specific field naming conventions.
Date: 2025-01-06 Status: Accepted
Numeric values in the codebase often represent distinct domain concepts (e.g.,BlockNumber, Epoch, SlotNumber,
KesPeriod, KesEvolutions). Using raw primitive types makes it easy to accidentally mix unrelated values, leading
to subtle bugs that the compiler cannot catch.
When wrapping arithmetic types, follow these guidelines:
-
Restrict direct conversions: Do not implement
From/Intotraits for conversions between the wrapper and primitive types. This prevents accidental escaping from the wrapper and forces explicit usage. -
Use constructor notation: Prefer the
Type(value)pattern for creating instances (e.g.,KesPeriod(42)). This is more readable than.into()and makes the intent explicit. -
Implement arithmetic traits selectively: Only implement arithmetic operations (
Add,Sub, etc.) that make semantic sense for the domain concept. -
Exception for persistence: Fallible conversions to
i64(TryFrom) may be implemented when required by the persistence layer, as explicit wrapper usage there provides limited benefit.
- The compiler enforces type safety, preventing accidental mixing of unrelated numeric values.
- Code is more readable with explicit
Type(value)notation. - Developers must consciously decide when to escape the wrapper, making type boundaries intentional.
Date: 2025-07-25 Status: Accepted
Some errors and logs currently lack enough context to understand the cause of an issue.
This is especially true for failures that occur during requests to or from external sources.
At the same time, adding too much context can make logs noisy and hard to read, and can flood log storage with low-value or sensitive data.
When writing log messages, adding error context, or designing error structures, follow these guidelines:
-
Prefer structured logging for internal context.
- Add identifiers as structured fields rather than embedding them in the message text (e.g., party id, signed entity type, beacon, request id, entity id).
- Keep the human-readable message short, put “what happened” in the message and “what it relates to” in structured fields.
-
Avoid unnecessary or sensitive context in logs by default.
- Do not log secrets or high-risk material (e.g., cryptographic keys, seeds, tokens, credentials).
- Do not log large payloads unless they are required for troubleshooting (see “External sources” below).
-
Handle large debug output explicitly.
- If a type’s
Debugoutput is too large or contains sensitive fields, implementDebugmanually to provide a safe, non-exhaustive representation by default. - Optionally support an “alternate” representation (e.g.,
{:#?}) that includes additional detail when it is safe and useful.
- If a type’s
-
External sources: allow exceptions when needed to troubleshoot.
- For interactions with external sources, it can be acceptable to include additional context such as request/response payloads only when necessary to diagnose issues.
- When logging external payloads, prefer safeguards such as truncation/size limits and logging only at error/debug level (and redaction when applicable).
- Logs are more readable and actionable.
- Errors are easier to understand and troubleshoot without routinely leaking sensitive data or producing excessive log volume.
Date: 2025-07-25 Status: Accepted
- Testing requires reusable utilities that may need to be shared across crates
- Test utilities should be isolated from production code while remaining accessible to child crates
- We need to minimize feature flags to optimize Rust compiler artifact reuse and reduce build times
Test utilities must follow this organizational structure:
Core Rules:
- All test utilities belong in a dedicated
testmodule within each crate - Utilities become public only when used by child crates or integration tests
- Public test utilities must not introduce additional dependencies
- Private test utilities are gated behind
cfg(test) - Import paths must explicitly include
testmodules to prevent accidental production usage - Feature flags are prohibited for test utility isolation
Module Organization:
- Test doubles (mocks, fakes, stubs):
test::doublemodule - Test data builders:
test::buildermodule - Test-only type extensions: Extension traits in
testmodule- Trait names end with
TestExtension - Implementations follow trait definitions, except when accessing private fields
- Trait names end with
- Consistent codebase organization across all crates
- Clear separation between production and test code
- Improved discoverability and maintainability of test utilities
- Reduced build times through minimal feature flag usage
- Enhanced reusability of test utilities across child crates
Date: 2025-07-22 Status: Accepted
The use of dummy() functions for creating test doubles is widespread across the codebase. However, inconsistencies
in their placement and visibility have led to maintenance challenges and reduced code clarity.
A Dummy trait will be introduced, functioning similarly to Rust's Default trait.
The following guidelines will be adopted for implementing the Dummy trait:
- Most implementations should reside in a
test::double::dummiesmodule within the crate where the type is defined. - For types with non-public fields, the
Dummytrait should be implemented directly below the type's definition.
- Enhanced consistency in code organization.
- Improved discoverability of test doubles.
- Clearer distinction between production and test code.
- Simplified maintenance of test implementations.
date: 2025-02-26 status: Accepted
After the update to rust 1.85 on 2025-02-21, we noticed that mithril-client-wasm was failing to compile with the error:
[INFO]: ⬇️ Installing wasm-bindgen...
thread 'main' panicked at crates/wasm-interpreter/src/lib.rs:245:21:
mithril_common::signable_builder::interface::_::__ctor::h4977fb9f7c35308c: Read a negative address value from the stack. Did we run out of memory?
Investigating the problem, we found that the use of typetag::serde attribute was causing the issue, as removing them
allowed the project to compile successfully.
Furthermore, we found that this is a known issue with typetag and WebAssembly, as documented in the typetag repository
issue #54.
Why this issue wasn't happening before the update to rust 1.85 is still unknown, as this incompatibility predates the update.
We use the typetag::serde attribute to serialize and deserialize Artifacts which are implementing a common trait.
As of today, this serialization is only used by mithril-aggregator to store the Artifacts in the database.
We will remove the typetag::serde attribute from the Artifacts when compiling to WebAssembly.
Web Assembly will not be able to serialize and deserialize Artifacts using the generic Artifact trait.
This will not affect mithril-aggregator as it is not compiled to WebAssembly.
In the future, if we need to serialize and deserialize Artifacts in WebAssembly, we will need to find an alternative solution.
date: 2025-02-26 status: Accepted
We already have a few ADRs in the docs/website/adr directory which document project-wide architectural decisions.
But we also want to document the rationale behind smaller decisions, so they are not lost in the shuffle, avoiding
the need to rehash the same discussions in the future.
We will use Architecture Decision Records, as described by Michael Nygard in this article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
To keep things simple, we will store these ADRs in a single file, and use a simple format to keep them readable.
See Michael Nygard's article, linked above.