The Sig Network Distributed MPC integration for the Midnight Blockchain allows contracts on Midnight to execute arbitrary transactions on foreign blockchains.
This Sig Network Midnight Integration is still Under Construction. Use at your own risk and expect rapid iteration.
This branch documents 0.11.0, published on npm under the
nexttag. The currentlatestis 0.10.0, which is what a plainnpm install @sig-net/midnightinstalls.0.11.0 is a breaking protocol release and is not servable yet: it needs a fakenet MPC responder image built against it, and that image has not shipped. Install 0.11.0 only if you are tracking the migration.
For the 0.10.0 documentation, read this file at the
v0.10.0tag, with one correction: that revision describes remote execution responses as Schnorr attestations by the MPC's Jubjub key, verified in-circuit by the singleton. That was never the case. Nothing in the protocol uses Schnorr or Jubjub, the singleton stores responses unverified, and the client contract must verify them in its own circuit. This applies to 0.10.0 as well as to 0.11.0.Migrating from 0.10.0 to 0.11.0 requires recompiling and redeploying your contracts: every request id and every derived address changes. The protocol surface was also renamed.
EVMType2TxParams,EVMCalldataandEVMAccessListEntrybecameEvmType2TxParams,EvmCalldataandEvmAccessListEntry; the signer circuitsignBidirectionalEventbecamesignBidirectional; andgetSignedEVMTransactionbecamegetSignedEvmTransaction.
This integration achieves this this by exposing the MPC's sign bidirectional flow to contracts on Midnight.
This repository contains the pieces that make that flow available on Midnight: the Sig Network protocol singleton contract, the client-agnostic SDK that contract builders integrate against, and two test caller contracts that exercise the protocol end to end. Example applications built on this integration (such as an ERC20 cross chain vault demo) live in sig-net/midnight-examples.
Read more about the Sign Bidirectional Flow, or jump straight to the Integrator Guide or the Contributor Guide depending on your goal. The Prerequisites are relevant to both.
The flow comprises 5 steps:
- Client calls a contract on Midnight which requests a signature for a transaction destined for a foreign chain. The signature is made with a key derived for the requesting contract (see Derived keys).
- Sig Network MPC honours the request, generating the transaction signature and posting it back to Midnight
- Client extracts the signature, using it to submit the signed transaction to the foreign chain
- Sig Network MPC observes the foreign transaction and posts the output of the execution (signed) back to Midnight
- Client extracts the signed foreign execution output and submits it back to the Midnight contract, which verifies the MPC's signature over it in-circuit against the contract's own response key (see Derived keys), completing the foreign transaction execution.
Every key the MPC uses is derived for the requesting contract and a path. There are two kinds: the request signing key, whose path each contract chooses, and the response signing key, whose path is fixed by the protocol. Both key derivations are scoped by the address of the requesting contract.
The key the MPC signs requested foreign transactions with:
requestSigningKey = f(mpcRootKey[keyVersion], contractAddress, path)
The path is 32 opaque bytes of the contract's choosing (e.g. a fixed literal for a contract-owned account like "vault" or a hash of a caller's secret for per-user accounts). There are no format requirements. The contract address is always part of the derivation, so no contract can reach another contract's derived keys.
The key the MPC signs foreign execution outputs with when posting them back to Midnight:
responseKey = f(mpcRootKey[keyVersion], contractAddress, "midnight response key")
The same derivation, but with the path fixed to the literal "midnight response key", giving each contract one well-known response key. A contract pins its own response key in its ledger after deploy and verifies every response against it in-circuit (step 5 of the flow above).
Integrating a contract on Midnight with the Sig Network MPC consists of:
- 4 once-off setup steps
- 5 per-request runtime steps that drive the full sign bidirectional flow
Set up your contract for integration with the Sig Network MPC's sign bidirectional flow:
-
Add the protocol library to your project:
yarn add @sig-net/midnight # or: npm install @sig-net/midnight -
Import the Signet module at the top of your contract (resolved through
node_modulesviaCOMPACT_PATH):import "@sig-net/midnight/src/Signet";Then tell the compact compiler about the npm packages with its
COMPACT_PATHenvironment variable at compile time:COMPACT_PATH=node_modules compact compile --feature-zkir-v3 src/my-contract.compact src/managed/my-contract
The Compact toolchain requirements in Prerequisites apply to integrators too: compile with the pinned compiler version (currently
compact update 0.33.0-rc.2) and always pass--feature-zkir-v3, as above. -
Declare the required Sig Network protocol state in your ledger (plus recommended deployer identity and initialisation state):
// Required: Map of SignBidirectionalEvent signature requests, configured by transaction type. // Configured and sized here for an EVM Type 2 transaction with // <1 calldata word, 0 access-list entries, 0 storage keys> and // 34-byte serialisation schemas. export ledger signBidirectionalEventMap: SignBidirectionalEventMap<EvmType2TxParams<1, 0, 0>, 34, 34>; // Required: The Signet singleton signer interface, set at deploy. // Used to notify the MPC of events you add to your signBidirectionalEventMap. sealed ledger signetSigner: SignetSigner; // Required: This contract's MPC response key, set in step 4. // Used to verify RespondBidirectionalEvents containing the serialised output of foreign chain execution. export ledger mpcResponseKey: Secp256k1Point; // Recommended: used in step 4 to ensure initialisation runs only once. export ledger initialised: Counter; // Recommended: set on deploy, used in step 4 to ensure only the deployer may set the mpcResponseKey. sealed ledger deployer: Bytes<32>; // Recommended: supplies the deployer's identity secret from private state // off-chain; only its commitment (below) ever reaches the ledger. witness witnessDeployerSecretKey(): Bytes<32>; // Recommended: the deployer identity commitment scheme. Exported so deploy // tooling can compute the constructor argument by calling the compiled circuit. export pure circuit calculateDeployerCommitment(sk: Bytes<32>): Bytes<32> { return persistentHash<Vector<2, Bytes<32>>>([pad(32, "my-contract:deployer:"), sk]); } // Required: set signet contract and (recommended) deployer commitment on deployment. constructor(signetContract: SignetSigner, deployerCommitment: Bytes<32>) { signetSigner = disclose(signetContract); deployer = disclose(deployerCommitment); } -
Set the contract's MPC response key once, right after deploy. Deriving this key requires the address of the contract, which only exists after deploy (see Response key):
export circuit initialise(responseKey: Secp256k1Point): [] { // Recommended: confirm that only the deployer may initialise, and only once: assert(deployer == calculateDeployerCommitment(witnessDeployerSecretKey()), "Not the deployer"); assert(initialised == 0, "Already initialised"); initialised.increment(1); // Required: set MPC response key for verification of RespondBidirectionalEvents mpcResponseKey = disclose(responseKey); }
Each interaction with your contract that executes a transaction on a foreign chain runs these 5 steps.
Steps 1 and 5 are circuits on your contract, and steps 2 to 4 are off-chain client code built on the utilities in @sig-net/midnight.
The off-chain steps share one SignetRequestResponseReader over your contract / Signet singleton pair, and the expected signer of the requested transaction (the key the MPC derives for your contract and the request's path, see Derived keys):
import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
import { deriveEvmAddress, SignetRequestResponseReader } from "@sig-net/midnight";
// SignetRequestResponseReader to poll for Signed Transactions and Signed RespondBidirectionalEvents
const reader = new SignetRequestResponseReader({
// Address of YOUR deployed contract
requesterContractAddress: myContractAddress,
// signBidirectionalEventMap's field position (Setup step 3)
requesterRequestsIndexField: 0,
// Address of the Signet singleton contract
signetContractAddress,
// Provider to index Midnight Blockchain
publicDataProvider: indexerPublicDataProvider({
queryURL: indexerUrl,
subscriptionURL: indexerWsUrl
}),
});
const expectedSigner = deriveEvmAddress(mpcRootPublicKey, myContractAddress, "my-path");- Store a signature request and notify the MPC via cross contract call:
// Construct SignBidirectionalEvent signature request and calculate its RequestId
const request = constructSignBidirectionalEvent<EvmType2TxParams<1, 0, 0>, 34, 34>(/* ... */);
const requestId = disclose(calculateRequestId<EvmType2TxParams<1, 0, 0>, 34, 34>(request));
// Store the signature request in your signBidirectionalEventMap for MPC to discover
signBidirectionalEventMap.insert(requestId, disclose(request));
// Notify the MPC of the SignBidirectionalEvent and the location of your signBidirectionalEventMap.
// The location is 0 here based on the position of the declaration in Setup step 3.
signetSigner.signBidirectional(
requestId,
constructSignBidirectionalEventNotificationV1(kernel.self(), 0 as Uint<8>),
);
NOTE: requestId should be returned from the above circuit call so that it may be used in subsequent steps (or compute it off-chain with the calculateRequestId TS twin).
-
Poll the Signet singleton for the MPC's signature response. The response log is unauthenticated (anyone can post), so use the verifying getter: it only returns a post whose signature recovers to
expectedSignerover the requested transaction's signing hash:const { verified } = await reader.getVerifiedSignatureRespondedEvent(requestId, expectedSigner); // verified === undefined: no valid response posted yet, poll again.
-
Construct the signed transaction and submit it to the foreign chain. The reader rebuilds the transaction from the request record on your ledger and attaches the verified MPC signature:
import { JsonRpcProvider } from "ethers"; const signedTx = await reader.getSignedEvmTransaction(requestId, expectedSigner); await new JsonRpcProvider(foreignChainRpcUrl).broadcastTransaction(signedTx.serialized);
-
Poll the Signet singleton for the MPC's signed remote execution output (posted once the MPC observes the transaction execute on the foreign chain). Posts are stored unverified, so treat them as candidates: the authoritative check is your contract's verify circuit in step 5:
const [respondBidirectionalEvent] = await reader.getRespondBidirectionalEvents(requestId); // Empty array: not posted yet, poll again.
-
Deliver the response to your contract, which verifies it in-circuit against the response key pinned in Setup step 4 and consumes the request:
assert( verifyRespondBidirectionalEvent(requestId, respondBidirectionalEvent, mpcResponseKey), "Invalid attestation signature" ); signBidirectionalEventMap.remove(requestId);
For full integration examples (such as an ERC20 cross chain vault) see the sig-net/midnight-examples repository.
Get set up for contributing by getting both test suites green: the offline unit tests, then the generic end to end integration suite.
The contract packages carry simulator-level unit tests that need no docker stack at all:
yarn compile # generates each contract package's src/managed/ (skip-zk)
yarn compile:signet-contract:zk # signet-contract's build gates on its prover keys
yarn build && yarn test # typecheck + unit tests (simulator-only, offline)The generic end to end integration suite drives the smallest possible client (the test caller contract) through the protocol: submit a signature request, get discovered via the notification registry, receive the MPC signature, and verify it in-circuit. Get it running locally:
- Ensure you have all of the prerequisites installed.
- From the repository root, install workspace dependencies and select the required Compact toolchain explicitly:
corepack enable yarn install compact update 0.33.0-rc.2 # Exact version required. # `compact update` installs/downgrades # to stable.
- Start the local stack (Midnight node, indexer, proof server, anvil EVM) with
docker compose up -d. The fakenet MPC responder is started automatically by the test setup once the signet contract is deployed. - Run the suite and watch it go. The first run can take ~10–25 minutes (it generates zk proving keys for both contracts, deploys them and hands off to the fakenet responder, all automatically, no
.envinserts needed):Green looks likeyarn test:integration-tests
Tests 4 passed (4). Afterwards, save the printedMIDNIGHT_CALLER_CONTRACT_ADDRESSinto.envso the next run skips compile and deploy (~2 minutes; the signet contract address is appended to.envautomatically).
TIP: If you are using Claude Code you can ask it to do all of this for you using this skill, for example:
Use your /e2e skill to get the integration suite running for me, from fresh clone to green. Recover the run yourself if anything fails along the way.
NOTE: The most common reason that a run fails is the proof server hanging or crashing when it exhausts memory on a proving leg. This most often presents as the test failing with connect ECONNREFUSED 127.0.0.1:6300, with docker ps -a showing the proof server container as Exited (137), i.e. OOM-killed. If this happens, restart the proof server and rerun; with the contract addresses kept in .env the rerun skips straight to the flow.
| Prerequisite | Version | Check With | Where to Get It |
|---|---|---|---|
| Node | ≥ 20 (22+ recommended) | node --version |
nodejs.org or your version manager (nvm, fnm, …) |
| Yarn 4 (via Corepack) | 4.x | corepack enable && yarn --version |
Corepack ships with Node; the repo's packageManager field pins the Yarn version |
| Compact toolchain | compiler 0.33.0-rc.2, invoked with --feature-zkir-v3 (see note) |
compact compile --version → 0.33.0 |
Install the compact launcher per Midnight's docs, then compact update 0.33.0-rc.2 (compiler builds live at LFDT-Minokawa/compact releases). If the launcher refuses the rc version, use the direct-download recipe in .github/workflows/ci.yml |
| A docker environment | any recent engine | docker --version |
Docker Desktop (macOS/Windows) or your distro's engine, with ≥ 16 GB RAM allocated (see note) |
| Docker Compose v2 | ≥ 2.x | docker compose version |
Included with Docker Desktop; plugin package on Linux |
NOTE: every compact compile against this stack must pass the --feature-zkir-v3 flag: it is part of the pinned ledger-9 matched set (compiler, node, indexer, proof server), and output compiled without it is not compatible with that stack. This repository's compile scripts already pass it. Integrators compiling their own contracts must pass it themselves (as shown in the Integrator Guide).
NOTE: the midnight proof server is quite heavy. It is recommended that you allocate at least 16 GB of RAM to your docker environment, otherwise expect to have to restart the tests as the proof server hangs.
These versions move together. Bumping one alone produces a stack that compiles but does not interoperate, and the failure is usually silent rather than loud: a responder that does not recognise a request simply never answers it.
| Component | Version | Pinned in |
|---|---|---|
@sig-net/* npm packages |
0.11.0 | packages/*/package.json |
| fakenet MPC responder | ghcr.io/sig-net/fakenet:latest |
docker-compose.yaml |
| Compact compiler | 0.33.0-rc.2, invoked with --feature-zkir-v3 |
.github/workflows/ci.yml, .github/workflows/publish.yml |
| Midnight node | 2.0.0-rc.4 | docker-compose.yaml |
| Midnight indexer | 4.4.0-pre-alpha.16 (l91r3-n2r3 build) |
docker-compose.yaml |
| Midnight proof server | 9.0.0-rc.5_experimental | docker-compose.yaml |
@midnightntwrk/ledger-v9 |
1.0.0-rc.3 | package.json resolutions |
NOTE: the fakenet responder is the one member of this set that is not pinned to an exact version. It tracks :latest, so the responder you get depends on when you pulled rather than on what this repository records. Each fakenet release names the @sig-net version it was built against (fakenet-v* tags); fakenet-v0.6.0 is built against 0.10.0.
| Package | npm | What it is |
|---|---|---|
packages/signet-midnight |
@sig-net/midnight |
Client-agnostic signet protocol library: shared Compact modules, TS twins of the wire structs, state readers, request feed/resolver, crypto (epsilon derivation, secp256k1 ECDSA attestations) |
packages/signet-contract |
@sig-net/midnight-contract |
The central singleton contract: unverified counted response logs + request-notification registry |
packages/signet-contract-deploy |
@sig-net/midnight-contract-deploy |
Deploy tooling for the singleton + the generic deploy/wallet plumbing |
packages/test-caller-contract |
repo-private | Integration-testing caller contract: submit a signature request, verify the response, the smallest thing that drives the protocol. Testing only, not an integration example |
packages/test-caller-contract-20-field |
repo-private | Integration-testing caller contract: the 20-field lockstep fixture proving the raw ledger readers resolve field numbers through the compiler's chunked (>15-field) state layout. Testing only |
packages/integration-tests |
repo-private | The generic e2e suite: submit → notification → MPC signature → in-circuit verify, against the local docker stack (docker-compose.yaml: midnight node/indexer/proof server + anvil EVM + fakenet MPC responder) |
packages/lib |
repo-private | Shared midnight-js provider adapters |