This file provides guidance to code agents when working in this repository.
# Build
cargo build -p mpc-node --release # Build MPC node
cargo near build non-reproducible-wasm --features abi --profile=release-contract --manifest-path crates/contract/Cargo.toml --locked # Build contract
# Test - warning, many tests are currently slow, prefer to run specific tests during development
cargo nextest run --cargo-profile=test-release --all-features # With nextest
# Run single test
cargo nextest run --cargo-profile=test-release test_name
# Linting and checks
cargo make check-all-fast # Fast checks (no tests)
cargo make check-all # All checks including tests
cargo clippy --all-targets --locked -- -D warnings
cargo fmt -- --check# We use cargo insta for snapshot testing (.snap files)
cargo nextest run --cargo-profile=test-release <test_name> # Run failing test to generate .snap.new
cargo insta review # Interactively review pending snapshots
cargo insta accept # Accept all pending snapshots
# Commit updated .snap files alongside code changescargo make e2e-tests # Build required binaries and run all E2E tests
cargo make e2e-tests-skip-build # Reuse binaries from a previous run
cargo make e2e-tests-skip-build <name> # Run tests matching a substring filter
cargo make e2e-tests-skip-build -E 'test(request_lifecycle)' --no-capture # Forward nextest flagsEverything after the task name is forwarded to cargo nextest run unchanged (filters, -E expressions, --no-capture, --retries, -j, etc.); no arguments runs the whole suite with the ci-e2e profile. Do not put flags after a -- separator: it is forwarded verbatim, and nextest only accepts filters, not flags, after --.
See crates/e2e-tests/README.md for details.
This is a Threshold Signature Scheme (TSS) implementation on NEAR blockchain. Users submit signature requests to an on-chain contract, and MPC nodes collaboratively generate signatures without any single party possessing the complete key.
- NEAR Indexer: Monitors the signer contract (
v1.signeron mainnet) for incomingsignrequests - MPC Signing: Threshold ECDSA based on cait-sith library with:
- Background Beaver triple generation (up to 1M per node)
- Signature generation: presign-and-sign in one computation over a pair of triples
| Crate | Purpose |
|---|---|
mpc-node |
Main node binary: indexer, coordinator, P2P networking, signature protocols |
mpc-contract |
NEAR smart contract: manages requests, participant set, protocol state |
near-mpc-contract-interface |
DTOs for contract communication and typed contract calls; plain data carriers |
near-mpc-sdk |
Client SDK: request builders, response verification; owns validation shared between consumers of the contract-interface crate |
mpc-primitives |
Core domain types (domain IDs, signature schemes) |
mpc-tls |
TLS transport for secure P2P communication |
test-utils |
Testing utilities for integration tests |
providers/: Signature implementations (ECDSA, EdDSA, CKD)coordinator.rs: Main state machine watching contract state, spawning MPC jobsprotocol.rs: Generic threshold protocol runnernetwork.rs: Mesh network with task-based multiplexingp2p.rs: TLS-based persistent connectionsindexer/: Blockchain monitoring, transaction submissiondb.rs: RocksDB persistence for keyshares, triples
NotInitialized → Running ↔ Initializing/Resharing
- Running: Normal operation (signing, CKD requests)
- Initializing: Key generation across multiple domains
- Resharing: Key redistribution after participant changes
- User calls
sign()on contract - Indexer detects receipt, adds to SignRequestStorage
- Coordinator spawns signature provider job
- Provider runs the domain's signing protocol
- Nodes exchange partial signatures via P2P
- Final signature submitted back to contract
Before writing or modifying code, read engineering-standards.md. It describes mandatory coding and testing conventions — including test structure, panic policy, and I/O separation — that apply to every change.
New tests must use this form:
#[test]
fn <system_under_test>__should_<test_assertion>() {
// Given
<setup>
// When
<action>
// Then
<assertion>
}See docs/development/engineering-standards.md for the full rationale and additional testing conventions.
Do not suggest using checked_add, checked_mul, checked_sub, saturating_add, or similar checked/saturating arithmetic in test code — this includes #[cfg(test)] modules, integration test crates, and e2e test crates. Raw arithmetic operators (+, -, *, /) are fine in tests — overflow will cause a panic, which is the desired behavior in tests.
Code that only unit tests use must be #[cfg(test)], whether it is a function, method, type, constant or import. Production builds then skip it, and the dead-code lint keeps catching code nothing uses. Do not keep such code compiled in with #[allow(dead_code)].
#[cfg(test)] items are invisible to integration tests in tests/ and to other crates. Put helpers those need behind the crate's test-utils feature (#[cfg(any(test, feature = "test-utils"))]). Public API meant for other crates or external users stays ungated even if only tests call it today.
We follow parse, don't validate: a validation method returns a validated struct rather than a bool, so the type system carries the proof of validity.
Validation logic needed by different consumers of the contract-interface crate should be shared, not re-implemented per consumer. A good place for such shared logic is the near-mpc-sdk crate.
A PR introducing a new contract method must add that method to the MpcContractHandle in the interface crate.
Traits should model a single capability, and be named after the action, not as an agent noun derived from it: ReadContractState, not ContractStateReader. This follows std-idiomatic patterns (From*/Into*/To* conversions). This applies to new traits and opportunistic renames, existing traits may deviate from this principle.
See docs/development/engineering-standards.md §Name capability traits after the action for the full rationale and a Don't / Do example.
If type A references type B, declare A before B. This applies to new code and reworked files, not bulk reorders.
See docs/development/engineering-standards.md §Declare referencing types first.
Where they make the code simpler, prefer the newest language features and std APIs of the toolchain pinned in rust-toolchain.toml over older idioms or third-party crates, e.g. Duration::from_hours(1) over Duration::from_secs(3600), or a let chain over nested if lets.
Default to writing no comments. Add one only in case one of the following applies:
- the why is non-obvious: an invariant, a constraint, a surprising behavior;
- it is part of the public API. In this case, focus on information relevant to the consumer of that code (provide brief example, explain complexity);
- the code requires a follow-up. In this case, there should be an issue for it, linked via a
TODO(#example_issue_number): <description>comment. The issue should do the heavy-lifting when it comes to explaining the desired behavior, not the code comment.
Avoid comments that are:
- paraphrasing the code;
- explaining common knowledge or terminology;
- burdening the reader with non-relevant information;
AI-generated code tends to arrive with obvious comments: restating what the next line does, labeling steps (// setup, // send the request), or narrating the edit that produced the code. Strip these before submitting. Keep a comment only if it says something the code cannot; if a reader can reconstruct it from the names and types on the same screen, delete it. This applies doubly in tests, where the // Given / // When / // Then structure already tells the story.
Prefer concise comments, using correct terminology.
In doc comments, reference other items with rustdoc intra-doc links ([`Foo`]), not plain `Foo` backticks. CI rejects broken links in everything rustdoc documents (test code is outside its view), and only linked references are checked at all; a plain backtick reference rots silently when the item is renamed. A backticked word that merely looks like an item (an algorithm name, a type from a crate we do not depend on, a cfg(test) item invisible to rustdoc) stays a plain code span.
See docs/development/engineering-standards.md §Write helpful code comments for the full rationale and a Don't / Do example.
- Unit test: Rust test in
/srcfolder - Integration test: Rust test in
/testsfolder - E2E test: Rust test in
crates/e2e-tests
Markdown under docs/ is sorted by audience: docs/guide/ for node operators, docs/development/ for people working on the code, docs/design/ for design docs. Archived documents keep their class under docs/archive/<class>/. A document that needs attachments (images, argument files, templates) gets its own folder named after it, with the document inside and the assets in an attachments/ subfolder or alongside it, e.g. docs/development/localnet/localnet.md; documents without attachments sit directly in their class folder.
Archived documents are recognizable by a **Status:** ARCHIVED banner directly below the title; such documents must never be modified, even when a change invalidates them: leave them as they are rather than un-archiving them to update them. When a PR archives a file, flag contents at risk of becoming stale (such as file paths). This is non-blocking, as version history preserves the paths valid at archiving time. The archiving procedure is described in CONTRIBUTING.md §Documentation.
All other documents are considered live and expected to be updated if invalidated by a change.
When authoring or reviewing a change that renames, removes, or reshapes code (types, methods, contract entry points, config fields, protocol state, architecture), verify that the surrounding documentation still describes the new behavior. This covers live Markdown under docs/ and any referenced templates, as well as Rust doc comments (///, //!) on and near the changed items — names, parameters, invariants, and examples in doc comments drift just as easily as prose docs. Live design documents (docs/design/, docs/*-design.md) that describe a superseded design must never be left silently stale: update them in the same PR, or — when the rewrite is too big for the PR that invalidated them — mark the stale sections with a status banner linking an issue that tracks the rewrite (e.g. **Status:** Partially superseded — TODO(#3825): …).
If you find stale passages in a live doc, flag them with file:line and, when authoring, fix them in the same PR. Doc drift is a review-blocking issue, not a follow-up. Archived docs are the exception: drift in them is expected and must not block a PR or prompt an update.