Skip to content

Commit 6276352

Browse files
authored
feat: CON-1646 Make ECDSA/Schnorr request queue size dynamic with available pre-signatures (#9059)
We can now store significantly more pre-signatures in advance. The rate at which these pre-signatures may be consumed is currently determined by the registry's `max_queue_size` config parameter: If there are _many_ pre-signatures available, increasing this parameter allows the replica to accept more signature requests that will be paired with these pre-signatures. This means pre-signatures will be consumed at a higher rate, leading to better throughput. However, if there are _no_ pre-signatures available (i.e. because the stash was depleted), then increasing the queue size implies a higher worst case latency for requests sitting at the end of the queue. These requests have to wait until enough pre-signatures were generated for all the requests sitting in front. Therefore, the `max_queue_size` parameter currently represents a trade-off between maximum throughput and worst case latency. The solution in this PR is to make the queue size dynamic: As long as there are still enough pre-signatures available, we accept more signature requests. If there are no pre-signatures available, then we only accept at most `max_queue_size` requests, as before. Since we don't want to accept too many requests, even if there are more pre-signatures available, the dynamic queue size is additionally capped by a new constant, that is currently set to 100 requests. For reference, on mainnet, we currently store 100 pre-signatures per key. The `max_queue_size` is set to 20 for ECDSA and 30 for Schnorr.
1 parent 89cc1c2 commit 6276352

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

rs/execution_environment/src/execution_environment.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ use ic_error_types::{ErrorCode, RejectCode, UserError};
3333
use ic_interfaces::execution_environment::{
3434
ExecutionMode, IngressHistoryWriter, RegistryExecutionSettings, SubnetAvailableMemory,
3535
};
36-
use ic_limits::{LOG_CANISTER_OPERATION_CYCLES_THRESHOLD, SMALL_APP_SUBNET_MAX_SIZE};
36+
use ic_limits::{
37+
LOG_CANISTER_OPERATION_CYCLES_THRESHOLD, MAX_PAIRED_PRE_SIGNATURES, SMALL_APP_SUBNET_MAX_SIZE,
38+
};
3739
use ic_logger::{ReplicaLogger, error, info, warn};
3840
use ic_management_canister_types_private::{
3941
CanisterChangeOrigin, CanisterHttpRequestArgs, CanisterIdRecord, CanisterInfoRequest,
@@ -61,16 +63,17 @@ use ic_replicated_state::{
6163
system_state::{CyclesUseCase, PausedExecutionId},
6264
},
6365
metadata_state::subnet_call_context_manager::{
64-
EcdsaArguments, InstallCodeCall, InstallCodeCallId, ReshareChainKeyContext,
65-
SchnorrArguments, SetupInitialDkgContext, SignWithThresholdContext, StopCanisterCall,
66-
SubnetCallContext, ThresholdArguments, VetKdArguments,
66+
EcdsaArguments, InstallCodeCall, InstallCodeCallId, PreSignatureStash,
67+
ReshareChainKeyContext, SchnorrArguments, SetupInitialDkgContext, SignWithThresholdContext,
68+
StopCanisterCall, SubnetCallContext, ThresholdArguments, VetKdArguments,
6769
},
6870
};
6971
use ic_types::{
7072
CanisterId, Cycles, ExecutionRound, Height, NumBytes, NumInstructions, RegistryVersion,
7173
ReplicaVersion, SubnetId, Time,
7274
batch::{CanisterCyclesCostSchedule, ChainKeyData},
7375
canister_http::{CanisterHttpRequestContext, MAX_CANISTER_HTTP_RESPONSE_BYTES},
76+
consensus::idkg::IDkgMasterPublicKeyId,
7477
crypto::{
7578
ExtendedDerivationPath,
7679
canister_threshold_sig::{MasterPublicKey, PublicKey},
@@ -3547,7 +3550,7 @@ impl ExecutionEnvironment {
35473550
mut request: Request,
35483551
args: ThresholdArguments,
35493552
derivation_path: Vec<Vec<u8>>,
3550-
max_queue_size: u32,
3553+
max_queue_size_registry: u32,
35513554
state: &mut ReplicatedState,
35523555
rng: &mut dyn RngCore,
35533556
subnet_size: usize,
@@ -3624,12 +3627,18 @@ impl ExecutionEnvironment {
36243627
));
36253628
}
36263629

3630+
let dynamic_queue_size = get_dynamic_signature_queue_size(
3631+
state.pre_signature_stashes(),
3632+
max_queue_size_registry,
3633+
&threshold_key,
3634+
);
3635+
36273636
// Check if the queue is full.
36283637
if state
36293638
.metadata
36303639
.subnet_call_context_manager
36313640
.sign_with_threshold_contexts_count(&threshold_key)
3632-
>= max_queue_size as usize
3641+
>= dynamic_queue_size
36333642
{
36343643
return Err(UserError::new(
36353644
ErrorCode::CanisterRejectedMessage,
@@ -4792,3 +4801,29 @@ fn get_master_public_key<'a>(
47924801
Some(master_key) => Ok(master_key),
47934802
}
47944803
}
4804+
4805+
/// Returns the dynamic signature request queue size for the given key.
4806+
/// In case the stash contains many pre-signatures for the requested key, then we increase
4807+
/// the queue size dynamically up to the maximum number of paired pre-signatures.
4808+
/// This is because a signature is much easier to produce if there is already a pre-signature
4809+
/// available.
4810+
pub(crate) fn get_dynamic_signature_queue_size(
4811+
stashes: &BTreeMap<IDkgMasterPublicKeyId, PreSignatureStash>,
4812+
max_queue_size_registry: u32,
4813+
key_id: &MasterPublicKeyId,
4814+
) -> usize {
4815+
if let Ok(key_id) = IDkgMasterPublicKeyId::try_from(key_id.clone()) {
4816+
// If this key uses pre-signatures, we can accept more requests if there are unpaired
4817+
// pre-signatures available in the stash.
4818+
let stash_size = stashes
4819+
.get(&key_id)
4820+
.map(|stash| stash.pre_signatures.len())
4821+
.unwrap_or_default();
4822+
// We never want to allow more requests than the maximum number of paired pre-signatures.
4823+
let max_queue_size = MAX_PAIRED_PRE_SIGNATURES.min(max_queue_size_registry as usize);
4824+
stash_size.clamp(max_queue_size, MAX_PAIRED_PRE_SIGNATURES)
4825+
} else {
4826+
// If this key doesn't use pre-signatures, we use the registry's max queue size.
4827+
max_queue_size_registry as usize
4828+
}
4829+
}

rs/execution_environment/src/execution_environment/tests.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use candid::{Decode, Encode};
33
use ic_base_types::{NumBytes, NumSeconds};
44
use ic_btc_interface::NetworkInRequest;
55
use ic_error_types::{ErrorCode, RejectCode, UserError};
6+
use ic_limits::MAX_PAIRED_PRE_SIGNATURES;
67
use ic_management_canister_types_private::{
78
self as ic00, BitcoinGetUtxosArgs, BoundedHttpHeaders, CanisterChange, CanisterHttpRequestArgs,
89
CanisterIdRecord, CanisterMetadataRequest, CanisterMetadataResponse, CanisterStatusResultV2,
@@ -19,10 +20,12 @@ use ic_replicated_state::{
1920
canister_state::{
2021
DEFAULT_QUEUE_CAPACITY, WASM_PAGE_SIZE_IN_BYTES, system_state::CyclesUseCase,
2122
},
23+
metadata_state::subnet_call_context_manager::PreSignatureStash,
2224
metadata_state::testing::NetworkTopologyTesting,
2325
testing::{CanisterQueuesTesting, SystemStateTesting},
2426
};
2527
use ic_test_utilities::assert_utils::assert_balance_equals;
28+
use ic_test_utilities_consensus::idkg::fake_pre_signature_stash;
2629
use ic_test_utilities_execution_environment::{
2730
ExecutionTest, ExecutionTestBuilder, check_ingress_status, expect_canister_did_not_reply,
2831
get_reject, get_reply,
@@ -32,6 +35,7 @@ use ic_types::{
3235
CanisterId, CountBytes, Cycles, PrincipalId, RegistryVersion,
3336
batch::CanisterCyclesCostSchedule,
3437
canister_http::{CanisterHttpMethod, Transform},
38+
consensus::idkg::IDkgMasterPublicKeyId,
3539
ingress::{IngressState, IngressStatus, WasmResult},
3640
messages::{
3741
CallbackId, MAX_RESPONSE_COUNT_BYTES, NO_DEADLINE, Payload, RejectContext,
@@ -44,6 +48,7 @@ use ic_types_test_utils::ids::{canister_test_id, node_test_id, subnet_test_id, u
4448
use ic_universal_canister::{CallArgs, UNIVERSAL_CANISTER_WASM, call_args, wasm};
4549
use maplit::btreemap;
4650
use more_asserts::{assert_ge, assert_gt, assert_le, assert_lt};
51+
use std::collections::BTreeMap;
4752
use std::mem::size_of;
4853

4954
#[cfg(test)]
@@ -4605,3 +4610,85 @@ fn cannot_accept_cycles_after_replying() {
46054610
initial_cycles + (transferred_cycles / 2u64)
46064611
);
46074612
}
4613+
4614+
#[test]
4615+
fn get_dynamic_signature_queue_size_vetkd_returns_registry_max() {
4616+
// Keys that don't use pre-signatures (e.g. VetKd) get the registry max queue size.
4617+
let key_id = make_vetkd_key("vetkd_key");
4618+
let stashes: BTreeMap<IDkgMasterPublicKeyId, PreSignatureStash> = BTreeMap::new();
4619+
assert_eq!(
4620+
super::get_dynamic_signature_queue_size(&stashes, 50, &key_id),
4621+
50
4622+
);
4623+
assert_eq!(
4624+
super::get_dynamic_signature_queue_size(&stashes, 200, &key_id),
4625+
200
4626+
);
4627+
}
4628+
4629+
#[test]
4630+
fn get_dynamic_signature_queue_size_idkg_empty_stash_returns_min_of_registry_and_cap() {
4631+
// Ecdsa/Schnorr key with no stash: queue size is min(registry, MAX_PAIRED_PRE_SIGNATURES).
4632+
let key_id = make_ecdsa_key("ecdsa_test_key");
4633+
let stashes: BTreeMap<IDkgMasterPublicKeyId, PreSignatureStash> = BTreeMap::new();
4634+
4635+
// Registry < constant → result is registry limit.
4636+
let max_queue_size_registry = MAX_PAIRED_PRE_SIGNATURES as u32 - 50;
4637+
assert_eq!(
4638+
super::get_dynamic_signature_queue_size(&stashes, max_queue_size_registry, &key_id),
4639+
max_queue_size_registry as usize
4640+
);
4641+
// Registry > contant → result is the constant (capped).
4642+
let max_queue_size_registry = MAX_PAIRED_PRE_SIGNATURES as u32 + 50;
4643+
assert_eq!(
4644+
super::get_dynamic_signature_queue_size(&stashes, max_queue_size_registry, &key_id),
4645+
MAX_PAIRED_PRE_SIGNATURES
4646+
);
4647+
}
4648+
4649+
#[test]
4650+
fn get_dynamic_signature_queue_size_idkg_stash_size_within_range() {
4651+
// Stash size in [max_queue_size, MAX_PAIRED_PRE_SIGNATURES] is returned as-is.
4652+
let ecdsa_key_id = IDkgMasterPublicKeyId::try_from(make_ecdsa_key("ecdsa_stash_test")).unwrap();
4653+
let key_id = ecdsa_key_id.inner();
4654+
let pre_signature_count = 30;
4655+
let stash = fake_pre_signature_stash(&ecdsa_key_id, pre_signature_count);
4656+
let stashes = BTreeMap::from([(ecdsa_key_id.clone(), stash)]);
4657+
4658+
// Registry 20, stash 30 → max_queue_size=20, result = clamp(30, 20, 100) = 30.
4659+
assert_eq!(
4660+
super::get_dynamic_signature_queue_size(&stashes, 20, key_id),
4661+
pre_signature_count as usize
4662+
);
4663+
}
4664+
4665+
#[test]
4666+
fn get_dynamic_signature_queue_size_idkg_stash_size_capped_at_max_paired() {
4667+
// Stash size > MAX_PAIRED_PRE_SIGNATURES is capped.
4668+
let ecdsa_key_id =
4669+
IDkgMasterPublicKeyId::try_from(make_ecdsa_key("ecdsa_large_stash")).unwrap();
4670+
let key_id = ecdsa_key_id.inner();
4671+
let stash = fake_pre_signature_stash(&ecdsa_key_id, 150);
4672+
let stashes = BTreeMap::from([(ecdsa_key_id.clone(), stash)]);
4673+
4674+
assert_eq!(
4675+
super::get_dynamic_signature_queue_size(&stashes, 20, key_id),
4676+
MAX_PAIRED_PRE_SIGNATURES
4677+
);
4678+
}
4679+
4680+
#[test]
4681+
fn get_dynamic_signature_queue_size_idkg_stash_below_floor_returns_floor() {
4682+
// Stash size < max_queue_size → result is max_queue_size (floor).
4683+
let ecdsa_key_id =
4684+
IDkgMasterPublicKeyId::try_from(make_ecdsa_key("ecdsa_small_stash")).unwrap();
4685+
let key_id = ecdsa_key_id.inner();
4686+
let stash = fake_pre_signature_stash(&ecdsa_key_id, 5);
4687+
let stashes = BTreeMap::from([(ecdsa_key_id.clone(), stash)]);
4688+
4689+
// Registry 50, stash 5 → max_queue_size=50, result = clamp(5, 50, 100) = 50.
4690+
assert_eq!(
4691+
super::get_dynamic_signature_queue_size(&stashes, 50, key_id),
4692+
50
4693+
);
4694+
}

rs/limits/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ pub const DKG_DEALINGS_PER_BLOCK: usize = 1;
9696
/// Please note that we put fairly big number mainly for perfomance reasons so either side of a channel doesn't await.
9797
/// The replica code should be designed in such a way that if we put a channel of size 1, the protocol should still work.
9898
pub const MAX_P2P_IO_CHANNEL_SIZE: usize = 100_000;
99+
100+
/// The maximum number of pre-signatures that may be paired with signature requests,
101+
/// per key ID.
102+
pub const MAX_PAIRED_PRE_SIGNATURES: usize = 100;

0 commit comments

Comments
 (0)