Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce mpc contract latency #168

Merged
merged 13 commits into from
Feb 6, 2025
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ jobs:

url="https://s3.us-west-1.amazonaws.com/build.nearprotocol.com/nearcore/${os_and_arch}/${branch_name}/${commit_hash}/neard"

mkdir -p target/debug
if ! curl -o target/debug/neard "${url}"; then
mkdir -p target/release
if ! curl -o target/release/neard "${url}"; then
echo "curl failed with URL: ${url}"
exit 1
fi
chmod +x target/debug/neard
chmod +x target/release/neard

- name: Build near core as fallback
if: steps.download-neard.outcome != 'success'
run: |
cd libs/nearcore
cargo build -p neard
cargo build -p neard --release

- name: Build mpc node
run: cargo build -p mpc-node --release
Expand All @@ -97,8 +97,8 @@ jobs:
- name: Run pytest
run: |
source pytest/venv/bin/activate
cd pytest
pytest -s
cd pytest
pytest -m "not ci_excluded" -s

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/pytest/venv/
/pytest/*/__pycache__/
/pytest/nearcore_pytest/nearcore_pytest.egg-info/
/pytest/tests/test_contracts/parallel/target/

/devnet/generate_keys/target/
/libs/chain-signatures/res/
Expand Down
Binary file not shown.
Binary file added libs/chain-signatures/compiled-contracts/v1.wasm
Binary file not shown.
86 changes: 86 additions & 0 deletions libs/chain-signatures/contract/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# MPC Contract

This folder contains the code for the **MPC Contract**, which is deployed on the NEAR blockchain.

The contract aims to reflect the current state of the MPC-Network and allows users to submit signature requests via the `sign` [endpoint](#user-api).

The currently deployed version of the contract is `V0`, with `V1` expected to be deployed in Q1 of 2025. Contract `V1` will introduce several efficiency improvements:

- **Lower gas costs**: Signature requests in `V1` will consume approximately half the gas compared to `V0`, mainly due to optimizations in state handling and reducing the number of receipts required per request. T
- **Removal of the signature request limit**: `V0` imposed a hard limit on the number of signature requests, which `V1` removes. This limit was necessary for [previous MPC nodes](https://github.com/near/mpc/releases/tag/1.0.0-rc.5), but is no longer required due to performance improvements by the [current release](https://github.com/Near-One/mpc/releases/tag/testnet-upgrade) (currently on testnet).

**Benchmarks:**

| Contract | avg. receipts | avg. gas [Tgas] |
| ------------- | ------------- | ------------- |
| V0 | 8 |11.30479597562405|
| V1 | 4 |6.131075775468398 |

**Migration Considerations:** Migration from `V0` to `V1` will not affect how users interact with the contract.

## `V1` Contract Details
### State and Lifecycle

The contract state tracks pending signature requests, the current configuartion of the contract as well as any updates to the contract that are proposed by Participants of the MPC-Network via the `update` [endpoint](#participants-api).

```Rust
pub struct MpcContractV1 {
protocol_state: ProtocolContractState,
pending_requests: LookupMap<SignatureRequest, YieldIndex>,
request_by_block_height: Vector<(u64, SignatureRequest)>,
proposed_updates: ProposedUpdates,
config: ConfigV1,
}
```

The **Protocol State** of the contract should reflect the state of the MPC-Network:
```mermaid
stateDiagram-v2
[*] --> NotInitialized : deploy
NotInitialized --> Initializing : init
Initializing --> Running : vote_pk
Running --> Resharing : vote_join
Resharing --> Running : vote_reshared
Running --> Resharing : vote_leave
```

### Contract API
#### User API

| Function | Behavior | Return Value | Gas requirement | Effective Gas Cost |
| ---- | --- | --- | --- | --- |
| `remove_timed_out_requests(max_num_to_remove: Option<u32>)` | Removes at most `max_num_to_remove` timed out signature requests from the contract state (defaulting to the value defined in the config). | `32`: number of signature requests that have been removed from the state. | - | `~0.4 Tgas` per removed request |
| `sign(request: SignRequest)` | Submits a signature request to the contract. | deferred to promise | `10 Tgas` | `~6 Tgas` |
| `public_key()` | | the aggregated public key used by all participants in the network. | `Result<PublicKey, Error>` | |
| `derived_public_key(path: String, predecessor: Option<AccountId>)` | Generates a derived public key for a given path and account. | `Result<PublicKey, Error>` | |
| `experimental_signature_deposit()` | | `U128`: the required deposit for a signature request | | |


#### Participants API
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>` | TBD | TBD |
| `join(url: String, cipher_pk: primitives::hpke::PublicKey, sign_pk: PublicKey)` | Allows a node to join the network by submitting necessary public keys and a URL. | `Result<(), Error>` | TBD | TBD |
| `vote_join(candidate: AccountId)` | Votes to accept a candidate node into the network. If the threshold is met, the candidate is added as a participant. | `Result<bool, Error>` | TBD | TBD |
| `vote_leave(kick: AccountId)` | Votes to remove a participant from the network. If the threshold is met, the participant is removed. | `Result<bool, Error>` | TBD | TBD |
| `vote_pk(public_key: PublicKey)` | When the protocol is in `Initializing` state:<br> - Stores votes to establish a new public key for the network to the protocol state. <br> - Changes the protocol state from `Initializing` to `Running` when a public key receives `threshold` votes. The participant set of the new protocol state will be the current candidate set. <br><br> When the protocol is in `Running` or `Resharing` state: <br> - Allows the participant to verify if `public_key` matches the public key stored in the protocol state. | `Result<bool, Error>` | TBD | TBD |
| `vote_reshared(epoch: u64)` | Votes to complete the key resharing process for a new epoch. | `Result<bool, 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 |


#### Developer API

| Function | Behavior | Return Value | Gas Requirement | Effective Gas Cost |
| ---- | --- | --- | --- | --- |
| `init(threshold: usize, candidates: BTreeMap<AccountId, CandidateInfo>, init_config: Option<InitConfigV1>)` | Initializes the contract with a threshold, candidate participants, and config values. Can only be called once. This sets the contract state to `Initializing`, where it will stay until `threshold` number of candidates have agreed on a public key by submitting their vote to `vote_pk`. | `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 |
| `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 |



32 changes: 31 additions & 1 deletion libs/chain-signatures/contract/src/config/impls.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use borsh::{self, BorshDeserialize, BorshSerialize};

use super::{
Config, DynamicValue, PresignatureConfig, ProtocolConfig, SignatureConfig, TripleConfig,
Config, ConfigV1, DynamicValue, InitConfigV1, PresignatureConfig, ProtocolConfig,
SignatureConfig, TripleConfig,
};

/// This is maximum expected participants we aim to support right now. This can be different
Expand All @@ -12,6 +13,35 @@ const MAX_EXPECTED_PARTICIPANTS: u32 = 32;
/// that should be in the network.
const NETWORK_MULTIPLIER: u32 = 128;

// Default delay of 200 blocks. After that, request is removed from the contract state
const DEFAULT_REQUEST_TIMEOUT_BLOCKS: u64 = 200;

// The maximum number of requests to remove during a call
const MAX_NUM_REQUESTS_TO_REMOVE: u32 = 1;

impl Default for ConfigV1 {
fn default() -> Self {
ConfigV1 {
max_num_requests_to_remove: MAX_NUM_REQUESTS_TO_REMOVE,
request_timeout_blocks: DEFAULT_REQUEST_TIMEOUT_BLOCKS,
}
}
}
impl From<Option<InitConfigV1>> for ConfigV1 {
fn from(value: Option<InitConfigV1>) -> Self {
match value {
None => ConfigV1::default(),
Some(init_config) => ConfigV1 {
max_num_requests_to_remove: init_config
.max_num_requests_to_remove
.unwrap_or(MAX_NUM_REQUESTS_TO_REMOVE),
request_timeout_blocks: init_config
.request_timeout_blocks
.unwrap_or(DEFAULT_REQUEST_TIMEOUT_BLOCKS),
},
}
}
}
impl Config {
pub fn get(&self, key: &str) -> Option<serde_json::Value> {
match key {
Expand Down
12 changes: 12 additions & 0 deletions libs/chain-signatures/contract/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ use near_sdk::serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct DynamicValue(serde_json::Value);

#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
pub struct ConfigV1 {
pub max_num_requests_to_remove: u32,
pub request_timeout_blocks: u64,
}

#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
pub struct InitConfigV1 {
pub max_num_requests_to_remove: Option<u32>,
pub request_timeout_blocks: Option<u64>,
}

#[derive(
Clone, Default, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq,
)]
Expand Down
Loading