diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9a747d19..3624527bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. - RFCs - RFC-27: IP Ownership Verification Service for user connection +- Serviceability + - `GlobalState` carries `ip_verifier_authority_pk`, the RFC-27 trust root for IP ownership proof validation, which `SetAuthority` and `doublezero global-config authority set --ip-verifier-authority ` rotate without a program upgrade. (#4196) - Utility crates - New `doublezero-ip-proof` crate defines the RFC-27 `IpOwnershipProof` and the exact bytes the verifier signs, in one place the serviceability program, the CLI, and the verification service all share. Nothing consumes it yet. (#4195, #4206) diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock index 1987fa6a12..89be6948ab 100644 --- a/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/Cargo.lock @@ -323,7 +323,7 @@ dependencies = [ [[package]] name = "doublezero-program-common" -version = "0.31.0" +version = "0.36.0" dependencies = [ "borsh", "byteorder", @@ -335,7 +335,7 @@ dependencies = [ [[package]] name = "doublezero-serviceability" -version = "0.31.0" +version = "0.36.0" dependencies = [ "bitflags", "borsh", @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "doublezero-serviceability-instruction" -version = "0.31.0" +version = "0.36.0" dependencies = [ "doublezero-serviceability", "solana-compute-budget-interface", diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs index 9772f13780..adc769f152 100644 --- a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs @@ -379,6 +379,7 @@ fn generate_global_state(dir: &Path) { let health_oracle_pk = pubkey_from_byte(0x04); let qa_pk = pubkey_from_byte(0x05); let feed_authority_pk = pubkey_from_byte(0x06); + let ip_verifier_authority_pk = pubkey_from_byte(0x07); let val = GlobalState { account_type: AccountType::GlobalState, @@ -395,6 +396,7 @@ fn generate_global_state(dir: &Path) { qa_allowlist: vec![qa_pk], feature_flags: 1, feed_authority_pk: feed_authority_pk, + ip_verifier_authority_pk, }; let data = borsh::to_vec(&val).unwrap(); @@ -419,6 +421,7 @@ fn generate_global_state(dir: &Path) { FieldValue { name: "QaAllowlist0".into(), value: pubkey_bs58(&qa_pk), typ: "pubkey".into() }, FieldValue { name: "FeatureFlags".into(), value: "1".into(), typ: "u128".into() }, FieldValue { name: "FeedAuthorityPk".into(), value: pubkey_bs58(&feed_authority_pk), typ: "pubkey".into() }, + FieldValue { name: "IpVerifierAuthorityPk".into(), value: pubkey_bs58(&ip_verifier_authority_pk), typ: "pubkey".into() }, ], }; diff --git a/sdk/serviceability/testdata/fixtures/global_state.bin b/sdk/serviceability/testdata/fixtures/global_state.bin index e99010f866..e94da35280 100644 Binary files a/sdk/serviceability/testdata/fixtures/global_state.bin and b/sdk/serviceability/testdata/fixtures/global_state.bin differ diff --git a/sdk/serviceability/testdata/fixtures/global_state.json b/sdk/serviceability/testdata/fixtures/global_state.json index 3a8267b310..9a8c2825a9 100644 --- a/sdk/serviceability/testdata/fixtures/global_state.json +++ b/sdk/serviceability/testdata/fixtures/global_state.json @@ -81,6 +81,11 @@ "name": "FeedAuthorityPk", "value": "QRSsyMWN1yHT9ir42bgNZUNZ4PdEhcSWCrL2AryKpy5", "typ": "pubkey" + }, + { + "name": "IpVerifierAuthorityPk", + "value": "UKrXU5bFrTzrqqpZXs8GVDbp4xPweiM65ADXNAy3ddR", + "typ": "pubkey" } ] } \ No newline at end of file diff --git a/smartcontract/cli/src/globalconfig/airdrop/get.rs b/smartcontract/cli/src/globalconfig/airdrop/get.rs index 3143549aef..67f970e753 100644 --- a/smartcontract/cli/src/globalconfig/airdrop/get.rs +++ b/smartcontract/cli/src/globalconfig/airdrop/get.rs @@ -80,6 +80,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client diff --git a/smartcontract/cli/src/globalconfig/authority/get.rs b/smartcontract/cli/src/globalconfig/authority/get.rs index 68a6d93e5b..b910f2b128 100644 --- a/smartcontract/cli/src/globalconfig/authority/get.rs +++ b/smartcontract/cli/src/globalconfig/authority/get.rs @@ -25,6 +25,8 @@ pub struct AuthorityDisplay { pub feed_authority: Pubkey, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] pub health_oracle: Pubkey, + #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] + pub ip_verifier_authority: Pubkey, } impl GetAuthorityCliCommand { @@ -41,6 +43,7 @@ impl GetAuthorityCliCommand { access_authority: gstate.sentinel_authority_pk, feed_authority: gstate.feed_authority_pk, health_oracle: gstate.health_oracle_pk, + ip_verifier_authority: gstate.ip_verifier_authority_pk, }; if self.json { @@ -79,6 +82,7 @@ mod tests { let sentinel_authority = Pubkey::new_unique(); let feed_authority = Pubkey::new_unique(); let health_oracle = Pubkey::new_unique(); + let ip_verifier_authority = Pubkey::new_unique(); let globalstate = GlobalState { account_type: AccountType::GlobalState, bump_seed: 0, @@ -94,6 +98,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: feed_authority, + ip_verifier_authority_pk: ip_verifier_authority, }; client @@ -129,6 +134,10 @@ mod tests { has_row("health_oracle", &health_oracle.to_string()), "health_oracle row should contain value" ); + assert!( + has_row("ip_verifier_authority", &ip_verifier_authority.to_string()), + "ip_verifier_authority row should contain value" + ); // JSON output let mut output = Vec::new(); @@ -153,5 +162,9 @@ mod tests { json["health_oracle"].as_str().unwrap(), health_oracle.to_string() ); + assert_eq!( + json["ip_verifier_authority"].as_str().unwrap(), + ip_verifier_authority.to_string() + ); } } diff --git a/smartcontract/cli/src/globalconfig/authority/set.rs b/smartcontract/cli/src/globalconfig/authority/set.rs index ef09cb2b5c..b9f5701b1a 100644 --- a/smartcontract/cli/src/globalconfig/authority/set.rs +++ b/smartcontract/cli/src/globalconfig/authority/set.rs @@ -25,6 +25,10 @@ pub struct SetAuthorityCliCommand { /// New feed authority public key #[arg(long)] pub feed_authority: Option, + + /// New IP ownership verifier authority public key (RFC-27) + #[arg(long)] + pub ip_verifier_authority: Option, } impl SetAuthorityCliCommand { @@ -82,11 +86,24 @@ impl SetAuthorityCliCommand { } }; + let ip_verifier_authority_pk = { + if let Some(ip_verifier_authority) = &self.ip_verifier_authority { + if ip_verifier_authority.eq_ignore_ascii_case("me") { + Some(client.get_payer()) + } else { + Some(Pubkey::from_str(ip_verifier_authority)?) + } + } else { + None + } + }; + let signature = client.set_authority(SetAuthorityCommand { activator_authority_pk, sentinel_authority_pk, health_oracle_pk, feed_authority_pk, + ip_verifier_authority_pk, })?; writeln!(out, "Signature: {signature}",)?; @@ -122,6 +139,7 @@ mod tests { let sentinel_authority_pk = Pubkey::new_unique(); let health_oracle_pk = Pubkey::new_unique(); let feed_authority_pk = Pubkey::new_unique(); + let ip_verifier_authority_pk = Pubkey::new_unique(); client .expect_check_requirements() @@ -134,6 +152,7 @@ mod tests { sentinel_authority_pk: Some(sentinel_authority_pk), health_oracle_pk: Some(health_oracle_pk), feed_authority_pk: Some(feed_authority_pk), + ip_verifier_authority_pk: Some(ip_verifier_authority_pk), })) .returning(move |_| Ok(signature)); @@ -147,6 +166,7 @@ mod tests { sentinel_authority: Some(sentinel_authority_pk.to_string()), health_oracle: Some(health_oracle_pk.to_string()), feed_authority: Some(feed_authority_pk.to_string()), + ip_verifier_authority: Some(ip_verifier_authority_pk.to_string()), } .execute(&ctx, &client, &mut output1), ); diff --git a/smartcontract/cli/src/globalconfig/featureflags/get.rs b/smartcontract/cli/src/globalconfig/featureflags/get.rs index af005dd3fe..af5220d05d 100644 --- a/smartcontract/cli/src/globalconfig/featureflags/get.rs +++ b/smartcontract/cli/src/globalconfig/featureflags/get.rs @@ -77,6 +77,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 1, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client @@ -114,6 +115,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client @@ -151,6 +153,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 1, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client diff --git a/smartcontract/cli/src/globalconfig/featureflags/set.rs b/smartcontract/cli/src/globalconfig/featureflags/set.rs index b2427059ab..0d1fc9908d 100644 --- a/smartcontract/cli/src/globalconfig/featureflags/set.rs +++ b/smartcontract/cli/src/globalconfig/featureflags/set.rs @@ -99,6 +99,7 @@ mod tests { qa_allowlist: vec![], feature_flags, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), } } diff --git a/smartcontract/programs/doublezero-geolocation/tests/test_helpers.rs b/smartcontract/programs/doublezero-geolocation/tests/test_helpers.rs index eca2df2a5c..b674305836 100644 --- a/smartcontract/programs/doublezero-geolocation/tests/test_helpers.rs +++ b/smartcontract/programs/doublezero-geolocation/tests/test_helpers.rs @@ -83,6 +83,7 @@ pub fn create_mock_globalstate_account_shared( qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::new_unique(), + ip_verifier_authority_pk: Pubkey::new_unique(), }; let data = borsh::to_vec(&globalstate).unwrap(); diff --git a/smartcontract/programs/doublezero-serviceability/src/authorize.rs b/smartcontract/programs/doublezero-serviceability/src/authorize.rs index e222f5ab71..b444f34130 100644 --- a/smartcontract/programs/doublezero-serviceability/src/authorize.rs +++ b/smartcontract/programs/doublezero-serviceability/src/authorize.rs @@ -498,6 +498,7 @@ mod tests { fn gs_with_feed(authority: &Pubkey) -> GlobalState { GlobalState { feed_authority_pk: *authority, + ip_verifier_authority_pk: Pubkey::default(), ..GlobalState::default() } } @@ -523,6 +524,7 @@ mod tests { sentinel_authority_pk: sentinel, health_oracle_pk: health_oracle, feed_authority_pk: feed, + ip_verifier_authority_pk: Pubkey::default(), ..GlobalState::default() }; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/initialize.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/initialize.rs index 49f73beffd..cd0ba59ff8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/initialize.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/initialize.rs @@ -108,6 +108,7 @@ pub fn initialize_global_state(program_id: &Pubkey, accounts: &[AccountInfo]) -> qa_allowlist: vec![*payer_account.key], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; try_acc_create( diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs index 5e404c8379..b2c96904bc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs @@ -21,14 +21,19 @@ pub struct SetAuthorityArgs { pub sentinel_authority_pk: Option, pub health_oracle_pk: Option, pub feed_authority_pk: Option, + pub ip_verifier_authority_pk: Option, } impl fmt::Debug for SetAuthorityArgs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "activator_authority_pk: {:?}, sentinel_authority_pk: {:?}, health_oracle_pk: {:?}, feed_authority_pk: {:?}", - self.activator_authority_pk, self.sentinel_authority_pk, self.health_oracle_pk, self.feed_authority_pk + "activator_authority_pk: {:?}, sentinel_authority_pk: {:?}, health_oracle_pk: {:?}, feed_authority_pk: {:?}, ip_verifier_authority_pk: {:?}", + self.activator_authority_pk, + self.sentinel_authority_pk, + self.health_oracle_pk, + self.feed_authority_pk, + self.ip_verifier_authority_pk ) } } @@ -95,6 +100,9 @@ pub fn process_set_authority( if let Some(feed_authority_pk) = value.feed_authority_pk { globalstate.feed_authority_pk = feed_authority_pk; } + if let Some(ip_verifier_authority_pk) = value.ip_verifier_authority_pk { + globalstate.ip_verifier_authority_pk = ip_verifier_authority_pk; + } try_acc_write(&globalstate, globalstate_account, payer_account, accounts)?; @@ -103,3 +111,37 @@ pub fn process_set_authority( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Transactions built before `ip_verifier_authority_pk` existed encode only the + /// first four options. `BorshDeserializeIncremental` must still decode them, + /// leaving the new field as `None` (i.e. "leave it alone"). + #[test] + fn test_setauthority_args_decodes_pre_ip_verifier_encoding() { + let feed = Pubkey::new_unique(); + + let mut data = Vec::new(); + None::.serialize(&mut data).unwrap(); + None::.serialize(&mut data).unwrap(); + None::.serialize(&mut data).unwrap(); + Some(feed).serialize(&mut data).unwrap(); + + let args = SetAuthorityArgs::try_from(&data[..]).unwrap(); + assert_eq!(args.feed_authority_pk, Some(feed)); + assert_eq!(args.ip_verifier_authority_pk, None); + } + + #[test] + fn test_setauthority_args_roundtrips_ip_verifier() { + let args = SetAuthorityArgs { + ip_verifier_authority_pk: Some(Pubkey::new_unique()), + ..Default::default() + }; + + let data = borsh::to_vec(&args).unwrap(); + assert_eq!(SetAuthorityArgs::try_from(&data[..]).unwrap(), args); + } +} diff --git a/smartcontract/programs/doublezero-serviceability/src/state/globalstate.rs b/smartcontract/programs/doublezero-serviceability/src/state/globalstate.rs index f5a56d9f7c..f06a5c3fe8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/globalstate.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/globalstate.rs @@ -30,6 +30,10 @@ pub struct GlobalState { pub qa_allowlist: Vec, // 4 + 32 * len pub feature_flags: u128, // 16 pub feed_authority_pk: Pubkey, // 32 + /// Trust root for IP ownership proof validation (RFC-27). `Pubkey::default()` + /// means no verifier is configured, which enforcement must treat as a hard + /// reject rather than "any signature passes". + pub ip_verifier_authority_pk: Pubkey, // 32 } impl Default for GlobalState { @@ -49,6 +53,7 @@ impl Default for GlobalState { qa_allowlist: Vec::new(), feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), } } } @@ -78,7 +83,11 @@ health_oracle_pk: {:?}", self.user_airdrop_lamports, self.health_oracle_pk, )?; - write!(f, ", feature_flags: {}", self.feature_flags) + write!( + f, + ", feature_flags: {}, feed_authority_pk: {:?}, ip_verifier_authority_pk: {:?}", + self.feature_flags, self.feed_authority_pk, self.ip_verifier_authority_pk + ) } } @@ -102,6 +111,7 @@ impl TryFrom<&[u8]> for GlobalState { qa_allowlist: deserialize_vec_with_capacity(&mut data).unwrap_or_default(), feature_flags: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), feed_authority_pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + ip_verifier_authority_pk: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), }; if out.account_type != AccountType::GlobalState { @@ -147,6 +157,11 @@ impl Validate for GlobalState { #[cfg(test)] mod tests { use super::*; + use base64::{engine::general_purpose, Engine as _}; + + /// A mainnet GlobalState account captured before `ip_verifier_authority_pk` + /// existed, i.e. its serialized bytes end at `feed_authority_pk`. + const LEGACY_GLOBALSTATE_VECTOR: &str = "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"; #[test] fn test_state_compatibility_globalstate() { @@ -155,11 +170,57 @@ mod tests { solana account 5cNB1387r3wt3aBAT1uoTge7y1LAadWkC9DFQ3F89Dt6 --output json -u https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16 */ - let versions = ["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"]; + let versions = [ + LEGACY_GLOBALSTATE_VECTOR, + &globalstate_vector_with_ip_verifier(IP_VERIFIER_VECTOR_PK), + ]; crate::helper::base_tests::test_parsing::(&versions).unwrap(); } + /// The pubkey written into the vector below (32 bytes of 0x11). + const IP_VERIFIER_VECTOR_PK: Pubkey = Pubkey::new_from_array([0x11; 32]); + + /// The legacy account re-serialized under the current layout with + /// `ip_verifier_authority_pk` set — i.e. what the account looks like after a + /// SetAuthority rotation writes it back. + fn globalstate_vector_with_ip_verifier(pk: Pubkey) -> String { + let bytes = general_purpose::STANDARD + .decode(LEGACY_GLOBALSTATE_VECTOR) + .unwrap(); + + let mut parsed = GlobalState::try_from(&bytes[..]).unwrap(); + parsed.ip_verifier_authority_pk = pk; + + general_purpose::STANDARD.encode(borsh::to_vec(&parsed).unwrap()) + } + + #[test] + fn test_state_globalstate_ip_verifier_authority_backwards_compatible() { + // An account written before the field existed reads back as "unset". + let legacy = general_purpose::STANDARD + .decode(LEGACY_GLOBALSTATE_VECTOR) + .unwrap(); + let old = GlobalState::try_from(&legacy[..]).unwrap(); + assert_eq!(old.ip_verifier_authority_pk, Pubkey::default()); + + // Once the field is written, it round-trips. + let extended = general_purpose::STANDARD + .decode(globalstate_vector_with_ip_verifier(IP_VERIFIER_VECTOR_PK)) + .unwrap(); + let new = GlobalState::try_from(&extended[..]).unwrap(); + assert_eq!(new.ip_verifier_authority_pk, IP_VERIFIER_VECTOR_PK); + + // Nothing ahead of the new field shifted. + assert_eq!( + GlobalState { + ip_verifier_authority_pk: Pubkey::default(), + ..new + }, + old + ); + } + #[test] fn test_state_globalstate_try_from_defaults() { let data = [AccountType::GlobalState as u8]; @@ -176,6 +237,7 @@ mod tests { assert_eq!(val.user_airdrop_lamports, 0); assert_eq!(val.feature_flags, 0); assert_eq!(val.feed_authority_pk, Pubkey::default()); + assert_eq!(val.ip_verifier_authority_pk, Pubkey::default()); } #[test] @@ -195,6 +257,7 @@ mod tests { qa_allowlist: vec![Pubkey::new_unique(), Pubkey::new_unique()], feature_flags: 1, feed_authority_pk: Pubkey::new_unique(), + ip_verifier_authority_pk: Pubkey::new_unique(), }; let data = borsh::to_vec(&val).unwrap(); @@ -225,6 +288,7 @@ mod tests { assert_eq!(val.user_airdrop_lamports, val2.user_airdrop_lamports); assert_eq!(val.feature_flags, val2.feature_flags); assert_eq!(val.feed_authority_pk, val2.feed_authority_pk); + assert_eq!(val.ip_verifier_authority_pk, val2.ip_verifier_authority_pk); } #[test] @@ -244,6 +308,7 @@ mod tests { qa_allowlist: vec![Pubkey::new_unique(), Pubkey::new_unique()], feature_flags: 0, feed_authority_pk: Pubkey::new_unique(), + ip_verifier_authority_pk: Pubkey::new_unique(), }; let err = val.validate(); assert!(err.is_err()); diff --git a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs index 71acd66f78..72e94a91aa 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/create_subscribe_user_test.rs @@ -1216,6 +1216,7 @@ async fn test_create_subscribe_user_sentinel_owner_override() { sentinel_authority_pk: Some(sentinel.pubkey()), health_oracle_pk: None, feed_authority_pk: None, + ip_verifier_authority_pk: None, }), vec![AccountMeta::new(globalstate_pubkey, false)], &payer, diff --git a/smartcontract/programs/doublezero-serviceability/tests/setauthority_test.rs b/smartcontract/programs/doublezero-serviceability/tests/setauthority_test.rs new file mode 100644 index 0000000000..b605d8ebcd --- /dev/null +++ b/smartcontract/programs/doublezero-serviceability/tests/setauthority_test.rs @@ -0,0 +1,151 @@ +//! SetAuthority coverage for `ip_verifier_authority_pk` (RFC-27, issue #4196). +//! +//! The verifier public key is the trust root for IP ownership proof validation, so +//! it has to be rotatable onchain without a program upgrade, and rotating it must +//! not disturb the other authorities. + +use doublezero_serviceability::{ + instructions::DoubleZeroInstruction, pda::*, + processors::globalstate::setauthority::SetAuthorityArgs, +}; +use solana_program_test::*; +use solana_sdk::{ + instruction::AccountMeta, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +mod test_helpers; +use test_helpers::*; + +/// Bring up a program instance with an initialized GlobalState. +async fn init_globalstate() -> ( + BanksClient, + Pubkey, + Keypair, + solana_program::hash::Hash, + Pubkey, +) { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + ( + banks_client, + program_id, + payer, + recent_blockhash, + globalstate_pubkey, + ) +} + +#[tokio::test] +async fn test_setauthority_rotates_ip_verifier_authority() { + let (mut banks_client, program_id, payer, recent_blockhash, globalstate_pubkey) = + init_globalstate().await; + + // A freshly initialized GlobalState has no verifier configured. Enforcement + // must read this as "reject", never as "any signature passes". + let before = get_globalstate(&mut banks_client, globalstate_pubkey).await; + assert_eq!(before.ip_verifier_authority_pk, Pubkey::default()); + + let verifier = Keypair::new(); + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { + ip_verifier_authority_pk: Some(verifier.pubkey()), + ..Default::default() + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + let after = get_globalstate(&mut banks_client, globalstate_pubkey).await; + assert_eq!(after.ip_verifier_authority_pk, verifier.pubkey()); + + // Every other authority is untouched. + assert_eq!(after.activator_authority_pk, before.activator_authority_pk); + assert_eq!(after.sentinel_authority_pk, before.sentinel_authority_pk); + assert_eq!(after.health_oracle_pk, before.health_oracle_pk); + assert_eq!(after.feed_authority_pk, before.feed_authority_pk); + assert_eq!(after.foundation_allowlist, before.foundation_allowlist); + assert_eq!(after.feature_flags, before.feature_flags); + + // Rotating again replaces the previous key. + let rotated = Keypair::new(); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { + ip_verifier_authority_pk: Some(rotated.pubkey()), + ..Default::default() + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + let after = get_globalstate(&mut banks_client, globalstate_pubkey).await; + assert_eq!(after.ip_verifier_authority_pk, rotated.pubkey()); +} + +#[tokio::test] +async fn test_setauthority_none_leaves_ip_verifier_authority_unchanged() { + let (mut banks_client, program_id, payer, recent_blockhash, globalstate_pubkey) = + init_globalstate().await; + + let verifier = Keypair::new(); + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { + ip_verifier_authority_pk: Some(verifier.pubkey()), + ..Default::default() + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + // A later SetAuthority that only touches another authority must not clear the + // verifier key. + let sentinel = Keypair::new(); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { + sentinel_authority_pk: Some(sentinel.pubkey()), + ip_verifier_authority_pk: None, + ..Default::default() + }), + vec![AccountMeta::new(globalstate_pubkey, false)], + &payer, + ) + .await; + + let after = get_globalstate(&mut banks_client, globalstate_pubkey).await; + assert_eq!(after.sentinel_authority_pk, sentinel.pubkey()); + assert_eq!(after.ip_verifier_authority_pk, verifier.pubkey()); +} diff --git a/smartcontract/sdk/rs/src/commands/device/delete.rs b/smartcontract/sdk/rs/src/commands/device/delete.rs index 885525e5d1..474c490105 100644 --- a/smartcontract/sdk/rs/src/commands/device/delete.rs +++ b/smartcontract/sdk/rs/src/commands/device/delete.rs @@ -193,6 +193,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client .expect_get() diff --git a/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs b/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs index ddbc6c16b8..a1462555db 100644 --- a/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs +++ b/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs @@ -9,6 +9,7 @@ pub struct SetAuthorityCommand { pub sentinel_authority_pk: Option, pub health_oracle_pk: Option, pub feed_authority_pk: Option, + pub ip_verifier_authority_pk: Option, } impl SetAuthorityCommand { @@ -21,6 +22,7 @@ impl SetAuthorityCommand { sentinel_authority_pk: self.sentinel_authority_pk, health_oracle_pk: self.health_oracle_pk, feed_authority_pk: self.feed_authority_pk, + ip_verifier_authority_pk: self.ip_verifier_authority_pk, }, )) } @@ -48,6 +50,7 @@ mod tests { let sentinel_authority_pk = Pubkey::new_unique(); let health_oracle_pk = Pubkey::new_unique(); let feed_authority_pk = Pubkey::new_unique(); + let ip_verifier_authority_pk = Pubkey::new_unique(); let expected = set_authority( &program_id, @@ -57,6 +60,7 @@ mod tests { sentinel_authority_pk: Some(sentinel_authority_pk), health_oracle_pk: Some(health_oracle_pk), feed_authority_pk: Some(feed_authority_pk), + ip_verifier_authority_pk: Some(ip_verifier_authority_pk), }, ); client @@ -69,6 +73,7 @@ mod tests { sentinel_authority_pk: Some(sentinel_authority_pk), health_oracle_pk: Some(health_oracle_pk), feed_authority_pk: Some(feed_authority_pk), + ip_verifier_authority_pk: Some(ip_verifier_authority_pk), } .execute(&client); assert!(res.is_ok()); diff --git a/smartcontract/sdk/rs/src/commands/user/delete.rs b/smartcontract/sdk/rs/src/commands/user/delete.rs index 61740dca8a..553b6bf673 100644 --- a/smartcontract/sdk/rs/src/commands/user/delete.rs +++ b/smartcontract/sdk/rs/src/commands/user/delete.rs @@ -571,6 +571,7 @@ mod tests { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client .expect_get() diff --git a/smartcontract/sdk/rs/src/tests.rs b/smartcontract/sdk/rs/src/tests.rs index 4c40de8062..81b09e5fed 100644 --- a/smartcontract/sdk/rs/src/tests.rs +++ b/smartcontract/sdk/rs/src/tests.rs @@ -40,6 +40,7 @@ pub mod utils { qa_allowlist: vec![], feature_flags: 0, feed_authority_pk: Pubkey::default(), + ip_verifier_authority_pk: Pubkey::default(), }; client .expect_get()