This crate defines the MPC Contract, which governs the MPC network and allows any NEAR account to request signatures.
┌───────┐ ┌─────────────┐ ┌───────────┐
│ User │ │ Participant │ │ MPC Node │
└───────┘ └─────────────┘ └───────────┘
│ │ │
Request signature. │ │
│ Vote on changes. │
│ │ │
└────────┐ │ ┌──Respond to signature requests.
│ │ │
▼ ▼ ▼
┌──────────────┐
│ MPC Contract │
└──────────────┘
The MPC contract is deployed on the NEAR blockchain and on the NEAR testnet.
This contract serves as an interface to the MPC network. Users and contracts can submit signature requests via this contract, and MPC Participants can vote on changes to the MPC network, such as:
- Changing the set of MPC participants.
- Adjusting the cryptographic threshold.
- Generating new distributed keys.
- Updating the contract code.
The contract tracks the following information:
- Pending signature requests.
- Current participant set of the MPC network.
- Node migrations.
- Current set of keys managed by the MPC network (each key is associated to a unique
domain_id). - Metadata related to trusted execution environments.
- Current protocol state of the MPC network (see Protocol State and Lifecycle).
Participants can propose and vote on contract updates (code or configuration changes). When an update receives sufficient votes and is executed (via the vote_update endpoint which calls do_update internally), all pending update proposals and votes are cleared as they are no longer be valid after the contract migration. The update ID counter is preserved across migrations as part of the contract state to avoid race conditions where multiple participants might propose updates with colliding IDs immediately after an upgrade.
Both sign and request_app_private_key require a deposit of at least 1 yoctonear. Any excess deposit is automatically refunded.
The deposit exists to prevent abuse by malicious frontends. On NEAR, a dApp frontend can hold a function-call access key that lets it submit transactions on behalf of a user without prompting for approval each time. By default, however, function-call access keys cannot attach a deposit. Requiring a deposit therefore guarantees that the call was authorised by the user's full-access key (or a function-call key with an explicit deposit allowance), preventing a compromised or malicious frontend from silently submitting signature requests without the user's knowledge.
Users can submit a signature request to the MPC network via the sign endpoint of this contract. A deposit of 1 yoctonear is required (see Deposit requirement).
The sign request takes the following arguments:
path(String): the derivation path (used for key-derivation).payload_v2: either{"Ecdsa": "<hex encoded 32 bytes>"}or{"Eddsa": "<hex encoded between 32 and 1232 bytes>"}
domain_id(integer): identifies the key to use for generating the signature. Note that the payload type must match the associated signature scheme.
Submitting a signature request costs approximately 7 Tgas, but the contract requires that at least 10 Tgas are attached to the transaction.
ECDSA Signature Request
{
"request": {
"payload_v2": {
"Ecdsa": "521da91dc9bddb625bd0679d9e735def558761a34653624f5954f44bce6443a9"
},
"path": "sepolia-1",
"domain_id": 0
}
}EDDSA Signature Request
{
"request": {
"payload_v2": {
"Eddsa": "521da91dc9bddb625bd0679d9e735def558761a34653624f5954f44bce6443a9"
},
"path": "solana-1",
"domain_id": 1
}
}Note that an Ecdsa payload is subsequently represented as a Scalar on curve Secp256k1. This means that the payload must be strictly less than the field size p = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F (see also curve parameters and k256 implementation details).
Users can submit a ckd request to the MPC network via the
request_app_private_key endpoint of this contract. A deposit of 1
yoctonear is required (see Deposit requirement).
The ckd request takes the following arguments:
derivation_path(String): the derivation path (used to derive different keys from the same account).app_public_key: the ephemeral public key for the CKD request. Two formats are supported:- Privately verifiable (legacy): a single G1 point, e.g.
"bls12381g1:<base58>"or{"AppPublicKey": "bls12381g1:<base58>"}. - Publicly verifiable: a pair of points
(pk1, pk2) = (a·G1, a·G2), passed as{"AppPublicKeyPV": {"pk1": "bls12381g1:<base58>", "pk2": "bls12381g2:<base58>"}}. This allows anyone to verify the encrypted result on-chain without the app's secret key.
- Privately verifiable (legacy): a single G1 point, e.g.
domain_id(integer): identifies the master key to use for deriving the ckd, and must correspond to bls12381.
Submitting a ckd request costs approximately 7 Tgas, but the contract requires that at least 10 Tgas are attached to the transaction.
Privately verifiable ckd request (legacy)
{
"request": {
"derivation_path": "mykey",
"app_public_key": "bls12381g1:6KtVVcAAGacrjNGePN8bp3KV6fYGrw1rFsyc7cVJCqR16Zc2ZFg3HX3hSZxSfv1oH6",
"domain_id": 2
}
}Publicly verifiable ckd request
{
"request": {
"derivation_path": "mykey",
"app_public_key": {
"AppPublicKeyPV": {
"pk1": "bls12381g1:6KtVVcAAGacrjNGePN8bp3KV6fYGrw1rFsyc7cVJCqR16Zc2ZFg3HX3hSZxSfv1oH6",
"pk2": "bls12381g2:22AgdyBXAQor5kiToW4frjEksuAhyic1S7CWWX7LFBTXFt1MxjcXwuB73yFCQVQfwMjKQoFFtmxPSUg2fCjhNUNVCFPVdtotAFMkPpoDg9s3QWQSZ2gUfvS3Uw1gaESFCfrw"
}
},
"domain_id": 2
}
}The set of MPC participants can be changed, subject to following restrictions:
- There must at least be
threshold(the current threshold) number of current participants in the prospective participant set. - The prospective threshold must be at least 60% of the number of participants (rounded upwards).
- The set of participants must have at least two participants.
In order for a change to be accepted by the contract, all prospective participants must vote for it using the vote_new_parameters endpoint. Note that any new participants vote will only be accepted after at least threshold (the current threshold) old participants voted for the same participant set.
{
"prospective_epoch_id":1,
"proposal":{
"threshold":3,
"participants":{
"next_id":2,
"participants":[
[
"mpc-participant0.near",
0,
{
"tls_public_key":"ed25519:2XPuwqhg71RXRiTUMKGapd8FYWgXnxVvydYBK9tS1ex2",
"url":"http://mpc-service0.com"
}
],
[
"mpc-participant1.near",
1,
{
"tls_public_key":"ed25519:2XPuwqhg71RXRiTUMKGapd8FYWgXnxVvydYBK9tS1ex2",
"url":"http://mpc-service1.com"
}
]
]
}
}
}To generate a new threshold signature key, all participants must vote for it to be added via vote_add_domains. Only votes from existing participants will be accepted.
{
"domains":[
{
"id":2,
"curve":"Secp256k1",
"protocol":"CaitSith",
"reconstruction_threshold":2,
"purpose":"Sign"
},
{
"id":3,
"curve":"Edwards25519",
"protocol":"Frost",
"reconstruction_threshold":2,
"purpose":"Sign"
},
{
"id":4,
"curve":"Bls12381",
"protocol":"ConfidentialKeyDerivation",
"reconstruction_threshold":2,
"purpose":"CKD"
}
]
}reconstruction_threshold is the per-domain t in t-of-n key reconstruction; it must satisfy 2 <= t <= n against the current participant count. DamgardEtAl domains additionally require the honest-majority bound 2t - 1 <= n.
After deploying the contract, it will first be in an uninitialized state. The owner will need to initialize it via init, providing the set of participants and threshold parameters.
The contract will then switch to running state, where further operations (like initializing keys, or changing the participant set), can be taken.
The following protocol state transitions are allowed.
stateDiagram-v2
direction LR
[*] --> NotInitialized : deploy
NotInitialized --> Running : init
Running --> Initializing : vote_add_domains
Running --> Resharing : vote_new_parameters
Initializing --> Running : vote_pk
Initializing --> Running : vote_cancel_keygen
Resharing --> Running : vote_reshared
Resharing --> Resharing : vote_new_parameters
| Function | Behavior | Return Value | Gas requirement | Effective Gas Cost |
|---|---|---|---|---|
sign(request: SignRequestArgs) |
Submits a signature request to the contract. Requires a deposit of 1 yoctonear. Duplicate submissions of the same request (same caller, domain, path, and payload) while an earlier one is still pending are queued and all receive the same response when the MPC nodes reply; the queue is bounded — concurrent duplicates beyond that bound are rejected with PendingRequestQueueFull. |
deferred to promise | 10 Tgas |
~7 Tgas |
request_app_private_key(request: CKDRequestArgs) |
Submits a confidential key derivation (ckd) request to the contract. Requires a deposit of 1 yoctonear. Duplicate submissions of the same request (same caller, domain, derivation path, and app public key) while an earlier one is still pending are queued and all receive the same response when the MPC nodes reply; the queue is bounded — concurrent duplicates beyond that bound are rejected with PendingRequestQueueFull. |
deferred to promise | 10 Tgas |
~7 Tgas |
verify_foreign_transaction(request: VerifyForeignTransactionRequestArgs) |
Submits a foreign-chain transaction verification request to the contract. Requires a deposit of 1 yoctonear and that the requested foreign chain is in the contract's supported set. Duplicate submissions of the same request (same caller, domain, chain, and payload) while an earlier one is still pending are queued and all receive the same response when the MPC nodes reply; the queue is bounded — concurrent duplicates beyond that bound are rejected with PendingRequestQueueFull. |
deferred to promise | 10 Tgas |
~7 Tgas |
public_key(domain: Option<DomainId>) |
Read-only function; returns the public key used for the given domain (defaulting to first). | Result<PublicKey, Error> |
||
derived_public_key(path: String, predecessor: Option<AccountId>, domain: Option<DomainId>) |
Generates a derived public key for a given path and account, for the given domain (defaulting to first). | Result<PublicKey, Error> |
||
prepay_attestation_storage(account_id: AccountId, grants: u32) |
Prepays attestation-entry storage for account_id. One grant permits one stored attestation. Payable and permissionless — anyone may prepay for any account, which is how an operator funds a node whose function-call access key cannot attach a deposit. Requires an attached deposit of exactly attestation_storage_fee_millinear × grants (see config) and rejects anything else. Nothing is refunded and there is no withdrawal. |
Result<(), Error> |
30Tgas | ~4Tgas |
available_attestation_grants(account_id: AccountId) |
Read-only function; returns the grants account_id has available — bought, minus those currently backing a stored attestation. 0 therefore means either "never prepaid" or "prepaid, and the grant is backing an entry"; cross-reference get_tee_accounts to distinguish them. |
u32 |
The sign request takes the following arguments:
path(String): the derivation path.payload_v2: either{"Ecdsa": "<hex encoded 32 bytes>"}or{"Eddsa": "<hex encoded between 32 and 1232 bytes>"}domain_id(integer): the domain ID that identifies the key and signature scheme to use for signing.
The request_app_private_key request takes the following arguments:
derivation_path(String): the derivation path.app_public_key: the ephemeral public key to encrypt the generated confidential key. Accepts either a plain G1 point string (privately verifiable, legacy) or a tagged enum withAppPublicKey(single G1 point) orAppPublicKeyPV(a{pk1, pk2}pair for public verifiability).domain_id(integer): the domain ID that identifies the key and signature scheme to use to generate the confidential key
- The legacy argument
payloadcan be used in place ofpayload_v2; the format for that is an array of 32 integer bytes. This argument can only be used to pass in an ECDSA payload. - The legacy argument
key_versioncan be used in place ofdomain_idand means the same thing.
These functions require the caller to be a participant or candidate.
| Function | Behavior | Return Value | Gas Requirement | Effective Gas Cost |
|---|---|---|---|---|
respond(request: SignatureRequest, response: SignatureResponse) |
Processes a response to a signature request, verifying its validity and ensuring proper state cleanup. | Result<(), Error> |
10Tgas | ~6Tgas |
respond_ckd(request: CKDRequest, response: CKDResponse) |
Processes a response to a ckd request, ensuring proper state cleanup. | Result<(), Error> |
10Tgas | ~6Tgas |
respond_verify_foreign_tx(request: VerifyForeignTransactionRequest, response: VerifyForeignTransactionResponse) |
Processes a response to a foreign-chain transaction verification request, ensuring proper state cleanup. | Result<(), Error> |
10Tgas | ~6Tgas |
vote_add_domains(domains: Vec<DomainConfig>) |
Votes to add new domains (new keys) to the MPC network; newly proposed domain IDs must start from next_domain_id and be contiguous, and each domain must specify a reconstruction_threshold with 2 <= t <= n. |
Result<(), Error> |
TBD | TBD |
vote_new_parameters(prospective_epoch_id: EpochId, proposal: ThresholdParameters) |
Votes to change the set of participants as well as the new threshold for the network. (Prospective epoch ID must be 1 plus current) | Result<(), Error> |
TBD | TBD |
vote_code_hash(code_hash: CodeHash) |
Votes to add new whitelisted TEE Docker image code hashes. | Result<(), Error> |
TBD | TBD |
vote_add_launcher_hash(launcher_hash: LauncherImageHash) |
Votes to add a launcher image hash to the allowed set. Requires threshold votes. | Result<(), Error> |
TBD | TBD |
vote_remove_launcher_hash(launcher_hash: LauncherImageHash) |
Votes to remove a launcher image hash. Requires ALL participants to vote. | Result<(), Error> |
TBD | TBD |
vote_add_os_measurement(measurement: ContractExpectedMeasurements) |
Votes to add an OS measurement set (MRTD, RTMR0-2, key-provider event digest). Requires threshold votes. | Result<(), Error> |
TBD | TBD |
vote_remove_os_measurement(measurement: ContractExpectedMeasurements) |
Votes to remove an OS measurement set. Requires ALL participants to vote. | Result<(), Error> |
TBD | TBD |
start_keygen_instance(key_event_id: KeyEventId) |
For Initializing state only. Starts a new attempt to generate a key (key_event_id must be the expected one) | Result<(), Error> |
TBD | TBD |
start_reshare_instance(key_event_id: KeyEventId) |
For Resharing state only. Starts a new attempt to reshare a key (key_event_id must be the expected one) | Result<(), Error> |
TBD | TBD |
vote_pk(key_event_id: KeyEventId, public_key: PublicKey) |
For Initializing state only. Votes for the public key for the given generation attempt; if enough votes are collected, transitions to the next domain to generate a key for, or if all domains are completed, transitions into Running. | Result<(), Error> |
TBD | TBD |
vote_reshared(key_event_id: KeyEventId) |
For Resharing state only. Votes for the success of the given resharing attempt; if enough votes are collected, transitions to the next domain to reshare for, or if all domains are completed, transitions into Running. | Result<(), Error> |
TBD | TBD |
vote_cancel_keygen(next_domain_id: u64) |
For Initializing state only. Votes to cancel the key generation (identified by the next_domain_id) and revert to the Running state. | Result<(), Error> |
TBD | TBD |
propose_update(args: ProposeUpdateArgs) |
Proposes an update to the contract, requiring an attached deposit. | Result<UpdateId, Error> |
TBD | TBD |
vote_update(id: UpdateId) |
Votes on a proposed update. If the threshold is met, the update is executed. | Result<bool, Error> |
TBD | TBD |
submit_participant_info(proposed_participant_attestation: Attestation, tls_public_key: Ed25519PublicKey) |
Submits the tee participant info for a potential candidate. c.f. TEE section. Storing a new attestation entry consumes one attestation-storage grant, so the account must have one prepaid via prepay_attestation_storage; re-submitting for a TLS key the account already owns does not. |
Result<(), Error> |
TBD | TBD |
| Function | Behavior | Return Value | Gas Requirement | Effective Gas Cost |
|---|---|---|---|---|
init(parameters: ThresholdParameters, init_config: Option<InitConfig>) |
Initializes the contract with a threshold, candidate participants, and config values. Can only be called once. This sets the contract state to Running with zero domains. vote_add_domains can be called to initialize key generation. |
Result<Self, Error> |
TBD | TBD |
state() |
Returns the current state of the contract. | &ProtocolContractState |
TBD | TBD |
get_pending_request(request: &SignatureRequest) |
Retrieves pending signature requests. | Option<YieldIndex> |
TBD | TBD |
get_pending_ckd_request(request: &CKDRequest) |
Retrieves pending confidential key derivation requests. | Option<YieldIndex> |
TBD | TBD |
config() |
Returns the contract configuration. | &ConfigV1 |
TBD | TBD |
version() |
Returns the contract version. | String |
TBD | TBD |
update_config(config: ConfigV1) |
Updates the contract configuration for V1. |
() |
TBD | TBD |
allowed_docker_image_hashes() |
Returns all currently allowed MPC Docker image hashes with their eviction expiry, newest first. | Vec<AllowedMpcDockerImageHash> |
TBD | TBD |
allowed_launcher_image_hashes() |
Returns the non-expired allowed launcher image hashes (the most-recently-used entry only when all are expired). | Vec<LauncherImageHash> |
TBD | TBD |
allowed_launcher_compose_hashes() |
Returns the non-expired allowed launcher compose hashes (derived from launcher + MPC image pairs; the most-recently-used entry only when all are expired). | Vec<LauncherDockerComposeHash> |
TBD | TBD |
launcher_hash_votes() |
Returns current launcher hash votes, showing each participant's vote. | LauncherHashVotes |
TBD | TBD |
code_hash_votes() |
Returns current code hash votes, showing each participant's vote. | CodeHashesVotes |
TBD | TBD |
allowed_os_measurements() |
Returns all currently allowed OS measurement sets. | Vec<ContractExpectedMeasurements> |
TBD | TBD |
os_measurement_votes() |
Returns current OS measurement votes, showing each participant's vote. | MeasurementVotes |
TBD | TBD |
clean_tee_status() |
Private endpoint. Cleans up TEE information for non-participants after resharing. Only callable by the contract itself via a promise. | Result<(), Error> |
TBD | TBD |
During development, it's recommended to build non-deterministically using cargo-near.
cargo near build non-reproducible-wasm --features abi --manifest-path crates/contract/Cargo.toml --lockedThe contract can also be built deterministically. The released artifact is the
cargo-near reproducible build, which embeds NEP-330 metadata for third-party
verifiers (requires docker):
cargo near build reproducible-wasm --manifest-path crates/contract/Cargo.toml
sha256sum target/near/mpc_contract/mpc_contract.wasmA Nix-based reproducible build is also available. See reproducible-builds.md for the full workflow and the difference between the two.
The MPC nodes will eventually run inside a Trusted Execution Environments (TEE). The network is currently in a transitioning period, where both operation modes (TEE and non-TEE) are supported, however, the TEE support is at least as of June 2025, highly experimental and not stable.
Participants that run their node inside a TEE will have to submit the following TEE related data to the contract:
pub struct DstackAttestation {
/// TEE Remote Attestation Quote that proves the participant's identity.
pub quote: Quote,
/// Supplemental data for the TEE quote, including Intel certificates to verify it came from
/// genuine Intel hardware, along with details about the Trusted Computing Base (TCB)
/// versioning, status, and other relevant info.
pub collateral: Collateral,
/// Dstack event log.
pub tcb_info: TcbInfo,
/// Expected measurements for the TEE quote.
pub expected_measurements: ExpectedMeasurements,
}The prospective node operator can retrieve that data from the web endpoint (:8080/get_public_data).
The process of doing so is as follows:
- The prospective participants set up their MPC inside their TEE environment (see running an MPC node in TDX).
- The prospective participants fetch their TEE related information from their logs.
- The prospective participants add the
near_signer_public_keyfrom the web endpoint (:8080/get_public_data) as an access key to their node operator account, eligible for calling the MPC contract (v1.signeron mainnet orv1.signer-prod.testneton testnet). Participants should provide sufficient funding to this key. - The prospective participants add the
near_responder_public_keysfrom the web endpoint to a different account and provide sufficient funding to it. - The participants submit their data to the contract via
submit_participant_info.