Skip to content

Commit 853ea8f

Browse files
gusin13Copilot
andauthored
feat: admin handler to update config (#98)
## Description Adds update config admin handler <!-- Brief description of changes --> ## Checklist - [ ] I have updated the [docs/SPEC.md](https://github.com/babylonlabs-io/rollup-bsn-contracts/blob/main/docs/SPEC.md) file if this change affects the specification - [ ] I have updated the schema by running `cargo gen-schema` Note - I will run cargo schema and update spec after all prs are merged, otherwise it will create git conflict --------- Co-authored-by: Copilot <[email protected]>
1 parent 435d3b1 commit 853ea8f

File tree

8 files changed

+483
-68
lines changed

8 files changed

+483
-68
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
4646

4747
* [#91](https://github.com/babylonlabs-io/rollup-bsn-contracts/pull/91) feat:
4848
optimize public key handling by using bytes instead of hex
49+
* [#98](https://github.com/babylonlabs-io/rollup-bsn-contracts/pull/98) feat:
50+
admin handler to update config
4951

5052
## v0.1.0
5153

contracts/finality/src/contract.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use babylon_bindings::BabylonQuery;
44

55
use crate::error::ContractError;
66
use crate::exec::allowlist::{handle_add_to_allowlist, handle_remove_from_allowlist};
7+
use crate::exec::config::handle_update_config;
78
use crate::exec::finality::handle_finality_signature;
89
use crate::exec::public_randomness::handle_public_randomness_commit;
910
use crate::msg::BabylonMsg;
@@ -159,6 +160,21 @@ pub fn execute(
159160
ExecuteMsg::RemoveFromAllowlist { fp_pubkey_hex_list } => {
160161
handle_remove_from_allowlist(deps, info, fp_pubkey_hex_list)
161162
}
163+
ExecuteMsg::UpdateConfig {
164+
min_pub_rand,
165+
max_msgs_per_interval,
166+
rate_limiting_interval,
167+
bsn_activation_height,
168+
finality_signature_interval,
169+
} => handle_update_config(
170+
deps,
171+
info,
172+
min_pub_rand,
173+
max_msgs_per_interval,
174+
rate_limiting_interval,
175+
bsn_activation_height,
176+
finality_signature_interval,
177+
),
162178
}
163179
}
164180

@@ -1028,6 +1044,195 @@ pub(crate) mod tests {
10281044
assert!(result.is_err());
10291045
}
10301046

1047+
#[test]
1048+
fn test_update_config() {
1049+
let mut deps = mock_deps_babylon();
1050+
let admin = deps.api.addr_make(INIT_ADMIN);
1051+
let non_admin = deps.api.addr_make("non_admin");
1052+
let bsn_id = "op-stack-l2-11155420".to_string();
1053+
let min_pub_rand = 100;
1054+
let bsn_activation_height = 1000;
1055+
let finality_signature_interval = 100;
1056+
1057+
// Initialize contract
1058+
let instantiate_msg = InstantiateMsg {
1059+
admin: admin.to_string(),
1060+
bsn_id: bsn_id.clone(),
1061+
min_pub_rand,
1062+
max_msgs_per_interval: MAX_MSGS_PER_INTERVAL,
1063+
rate_limiting_interval: RATE_LIMITING_INTERVAL,
1064+
bsn_activation_height,
1065+
finality_signature_interval,
1066+
allowed_finality_providers: None,
1067+
};
1068+
let info = message_info(&deps.api.addr_make(CREATOR), &[]);
1069+
instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
1070+
1071+
// Test 1: Admin can update individual fields
1072+
let update_msg = ExecuteMsg::UpdateConfig {
1073+
min_pub_rand: Some(200),
1074+
max_msgs_per_interval: None,
1075+
rate_limiting_interval: None,
1076+
bsn_activation_height: None,
1077+
finality_signature_interval: None,
1078+
};
1079+
let admin_info = message_info(&admin, &[]);
1080+
let res = execute(deps.as_mut(), mock_env(), admin_info.clone(), update_msg).unwrap();
1081+
1082+
assert_eq!(res.attributes.len(), 1);
1083+
assert_eq!(res.attributes[0].key, "action");
1084+
assert_eq!(res.attributes[0].value, "update_config");
1085+
1086+
// Verify config was updated
1087+
let config_query = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap();
1088+
let config: Config = from_json(config_query).unwrap();
1089+
assert_eq!(config.bsn_id, bsn_id); // unchanged
1090+
assert_eq!(config.min_pub_rand, 200); // updated
1091+
assert_eq!(config.bsn_activation_height, bsn_activation_height); // unchanged
1092+
assert_eq!(
1093+
config.finality_signature_interval,
1094+
finality_signature_interval
1095+
); // unchanged
1096+
1097+
// Test 2: Update multiple fields at once
1098+
let update_msg = ExecuteMsg::UpdateConfig {
1099+
min_pub_rand: Some(300),
1100+
max_msgs_per_interval: Some(150),
1101+
rate_limiting_interval: Some(15000),
1102+
bsn_activation_height: Some(2000),
1103+
finality_signature_interval: Some(200),
1104+
};
1105+
let res = execute(deps.as_mut(), mock_env(), admin_info.clone(), update_msg).unwrap();
1106+
1107+
assert_eq!(res.attributes.len(), 1);
1108+
assert_eq!(res.attributes[0].key, "action");
1109+
assert_eq!(res.attributes[0].value, "update_config");
1110+
1111+
// Verify all fields were updated
1112+
let config_query = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap();
1113+
let config: Config = from_json(config_query).unwrap();
1114+
assert_eq!(config.bsn_id, bsn_id); // unchanged
1115+
assert_eq!(config.min_pub_rand, 300);
1116+
assert_eq!(config.rate_limiting.max_msgs_per_interval, 150);
1117+
assert_eq!(config.rate_limiting.block_interval, 15000);
1118+
assert_eq!(config.bsn_activation_height, 2000);
1119+
assert_eq!(config.finality_signature_interval, 200);
1120+
1121+
// Test 3: Non-admin cannot update config
1122+
let non_admin_info = message_info(&non_admin, &[]);
1123+
let update_msg = ExecuteMsg::UpdateConfig {
1124+
min_pub_rand: Some(999),
1125+
max_msgs_per_interval: None,
1126+
rate_limiting_interval: None,
1127+
bsn_activation_height: None,
1128+
finality_signature_interval: None,
1129+
};
1130+
let err = execute(deps.as_mut(), mock_env(), non_admin_info, update_msg).unwrap_err();
1131+
assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {}));
1132+
1133+
// Test 4: Empty update should fail
1134+
let empty_update_msg = ExecuteMsg::UpdateConfig {
1135+
min_pub_rand: None,
1136+
max_msgs_per_interval: None,
1137+
rate_limiting_interval: None,
1138+
bsn_activation_height: None,
1139+
finality_signature_interval: None,
1140+
};
1141+
let err = execute(deps.as_mut(), mock_env(), admin_info, empty_update_msg).unwrap_err();
1142+
assert_eq!(err, ContractError::NoConfigFieldsToUpdate);
1143+
}
1144+
1145+
#[test]
1146+
fn test_update_config_validation() {
1147+
let mut deps = mock_deps_babylon();
1148+
let admin = deps.api.addr_make(INIT_ADMIN);
1149+
let bsn_id = "op-stack-l2-11155420".to_string();
1150+
1151+
// Initialize contract
1152+
let instantiate_msg = InstantiateMsg {
1153+
admin: admin.to_string(),
1154+
bsn_id,
1155+
min_pub_rand: 100,
1156+
max_msgs_per_interval: MAX_MSGS_PER_INTERVAL,
1157+
rate_limiting_interval: RATE_LIMITING_INTERVAL,
1158+
bsn_activation_height: 1000,
1159+
finality_signature_interval: 100,
1160+
allowed_finality_providers: None,
1161+
};
1162+
let info = message_info(&deps.api.addr_make(CREATOR), &[]);
1163+
instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap();
1164+
1165+
let admin_info = message_info(&admin, &[]);
1166+
1167+
// Test invalid min_pub_rand
1168+
let invalid_min_pub_rand_update = ExecuteMsg::UpdateConfig {
1169+
min_pub_rand: Some(0), // invalid: must be > 0
1170+
max_msgs_per_interval: None,
1171+
rate_limiting_interval: None,
1172+
bsn_activation_height: None,
1173+
finality_signature_interval: None,
1174+
};
1175+
let err = execute(
1176+
deps.as_mut(),
1177+
mock_env(),
1178+
admin_info.clone(),
1179+
invalid_min_pub_rand_update,
1180+
)
1181+
.unwrap_err();
1182+
assert_eq!(err, ContractError::InvalidMinPubRand(0));
1183+
1184+
// Test invalid max_msgs_per_interval
1185+
let invalid_max_msgs_update = ExecuteMsg::UpdateConfig {
1186+
min_pub_rand: None,
1187+
max_msgs_per_interval: Some(0), // invalid: must be > 0
1188+
rate_limiting_interval: None,
1189+
bsn_activation_height: None,
1190+
finality_signature_interval: None,
1191+
};
1192+
let err = execute(
1193+
deps.as_mut(),
1194+
mock_env(),
1195+
admin_info.clone(),
1196+
invalid_max_msgs_update,
1197+
)
1198+
.unwrap_err();
1199+
assert_eq!(err, ContractError::InvalidMaxMsgsPerInterval(0));
1200+
1201+
// Test invalid rate_limiting_interval
1202+
let invalid_rate_interval_update = ExecuteMsg::UpdateConfig {
1203+
min_pub_rand: None,
1204+
max_msgs_per_interval: None,
1205+
rate_limiting_interval: Some(0), // invalid: must be > 0
1206+
bsn_activation_height: None,
1207+
finality_signature_interval: None,
1208+
};
1209+
let err = execute(
1210+
deps.as_mut(),
1211+
mock_env(),
1212+
admin_info.clone(),
1213+
invalid_rate_interval_update,
1214+
)
1215+
.unwrap_err();
1216+
assert_eq!(err, ContractError::InvalidRateLimitingInterval(0));
1217+
1218+
// Test invalid finality_signature_interval
1219+
let invalid_finality_interval_update = ExecuteMsg::UpdateConfig {
1220+
min_pub_rand: None,
1221+
max_msgs_per_interval: None,
1222+
rate_limiting_interval: None,
1223+
bsn_activation_height: None,
1224+
finality_signature_interval: Some(0), // invalid: must be > 0
1225+
};
1226+
let err = execute(
1227+
deps.as_mut(),
1228+
mock_env(),
1229+
admin_info,
1230+
invalid_finality_interval_update,
1231+
)
1232+
.unwrap_err();
1233+
assert_eq!(err, ContractError::InvalidFinalitySignatureInterval(0));
1234+
}
1235+
10311236
#[test]
10321237
fn test_allowlist_management() {
10331238
use rand::rngs::StdRng;

contracts/finality/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ pub enum ContractError {
105105
RateLimitExceeded { fp_btc_pk: String, limit: u32 },
106106
#[error("Finality provider {0} is not in the allowlist")]
107107
FinalityProviderNotAllowed(String),
108+
#[error("No configuration fields were provided to update")]
109+
NoConfigFieldsToUpdate,
108110
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use cosmwasm_std::{DepsMut, MessageInfo, Response};
2+
3+
use crate::error::ContractError;
4+
use crate::msg::BabylonMsg;
5+
use crate::state::config::{get_config, set_config, ADMIN};
6+
use crate::validation::{
7+
validate_finality_signature_interval, validate_max_msgs_per_interval, validate_min_pub_rand,
8+
validate_rate_limiting_interval,
9+
};
10+
use babylon_bindings::BabylonQuery;
11+
12+
/// Handle updating the contract configuration
13+
pub fn handle_update_config(
14+
deps: DepsMut<BabylonQuery>,
15+
info: MessageInfo,
16+
min_pub_rand: Option<u64>,
17+
max_msgs_per_interval: Option<u32>,
18+
rate_limiting_interval: Option<u64>,
19+
bsn_activation_height: Option<u64>,
20+
finality_signature_interval: Option<u64>,
21+
) -> Result<Response<BabylonMsg>, ContractError> {
22+
// Only admin can update config
23+
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
24+
25+
// Get current config
26+
let mut config = get_config(deps.storage)?;
27+
let mut has_updates = false;
28+
29+
// Update min_pub_rand if provided
30+
if let Some(new_min_pub_rand) = min_pub_rand {
31+
validate_min_pub_rand(new_min_pub_rand)?;
32+
config.min_pub_rand = new_min_pub_rand;
33+
has_updates = true;
34+
}
35+
36+
// Update rate limiting config if any rate limiting fields are provided
37+
if let Some(new_max_msgs) = max_msgs_per_interval {
38+
validate_max_msgs_per_interval(new_max_msgs)?;
39+
config.rate_limiting.max_msgs_per_interval = new_max_msgs;
40+
has_updates = true;
41+
}
42+
43+
if let Some(new_interval) = rate_limiting_interval {
44+
validate_rate_limiting_interval(new_interval)?;
45+
config.rate_limiting.block_interval = new_interval;
46+
has_updates = true;
47+
}
48+
49+
// Update bsn_activation_height if provided (no validation needed - any u64 is valid)
50+
if let Some(new_activation_height) = bsn_activation_height {
51+
config.bsn_activation_height = new_activation_height;
52+
has_updates = true;
53+
}
54+
55+
// Update finality_signature_interval if provided
56+
if let Some(new_finality_interval) = finality_signature_interval {
57+
validate_finality_signature_interval(new_finality_interval)?;
58+
config.finality_signature_interval = new_finality_interval;
59+
has_updates = true;
60+
}
61+
62+
// Check if any fields were actually updated
63+
if !has_updates {
64+
return Err(ContractError::NoConfigFieldsToUpdate);
65+
}
66+
67+
// Save the updated config
68+
set_config(deps.storage, &config)?;
69+
70+
Ok(Response::new().add_attribute("action", "update_config"))
71+
}

contracts/finality/src/exec/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod allowlist;
2+
pub mod config;
23
pub mod finality;
34
pub mod public_randomness;

contracts/finality/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod msg;
1212
pub mod queries;
1313
pub mod state;
1414
pub mod utils;
15+
pub mod validation;
1516

1617
#[cfg_attr(not(feature = "library"), entry_point)]
1718
pub fn instantiate(

0 commit comments

Comments
 (0)