From 2bb9d0810d186f75d527ad8aeabcf264fa51ace6 Mon Sep 17 00:00:00 2001 From: gbengaeben Date: Thu, 27 Aug 2026 16:42:34 +0000 Subject: [PATCH] feat(governance): emit PublicKeyRegistered event on public-key registration register_public_key previously overwrote a signer's stored ECDSA public key without any on-chain traceability. Emit a PublicKeyRegistered event (signer, public key, timestamp) on both registration and overwrite so signer key rotations are observable to auditors and off-chain indexers. Add a unit test asserting the event fires and carries the expected signer, key and timestamp. Closes #1031 --- contracts/governance/src/lib.rs | 17 ++++++++++++++++ contracts/governance/src/tests.rs | 32 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index 4308065f9..2aee3b9a5 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -156,6 +156,15 @@ pub mod governance { pub proposer: AccountId, } + /// Emitted when a signer registers (or overwrites) their ECDSA public key. + #[ink(event)] + pub struct PublicKeyRegistered { + #[ink(topic)] + pub signer: AccountId, + pub public_key: [u8; 33], + pub timestamp: u64, + } + // ========================================================================= // Storage // ========================================================================= @@ -575,11 +584,19 @@ pub mod governance { } /// Register an ECDSA public key for cryptographic signature verification. + /// + /// Emits a `PublicKeyRegistered` event (also on overwrite) so that signer + /// key rotations remain observable to auditors and off-chain indexers. #[ink(message)] pub fn register_public_key(&mut self, public_key: [u8; 33]) -> Result<(), Error> { let caller = self.env().caller(); self.ensure_signer(caller)?; self.signer_public_keys.insert(caller, &public_key); + self.env().emit_event(PublicKeyRegistered { + signer: caller, + public_key, + timestamp: self.env().block_timestamp(), + }); Ok(()) } diff --git a/contracts/governance/src/tests.rs b/contracts/governance/src/tests.rs index e586d465d..7c914960f 100644 --- a/contracts/governance/src/tests.rs +++ b/contracts/governance/src/tests.rs @@ -470,6 +470,38 @@ mod tests { ); } + #[ink::test] + fn register_public_key_emits_public_key_registered_event() { + use scale::Decode as _; + + let mut gov = create_governance(); + let accounts = default_accounts(); + ink::env::test::set_block_timestamp::(42); + set_caller(accounts.alice); + + gov.register_public_key([0x03; 33]).unwrap(); + + let events = ink::env::test::recorded_events().collect::>(); + let decoded = + PublicKeyRegistered::decode(&mut &events[0].data[..]).expect("decode event"); + assert_eq!(decoded.signer, accounts.alice); + assert_eq!(decoded.public_key, [0x03; 33]); + assert_eq!(decoded.timestamp, 42); + + // Overwriting an existing key is a rotation and must also be traceable. + ink::env::test::set_block_timestamp::(99); + set_caller(accounts.bob); + gov.register_public_key([0x04; 33]).unwrap(); + gov.register_public_key([0x05; 33]).unwrap(); + + let events = ink::env::test::recorded_events().collect::>(); + assert_eq!(events.len(), 3); + let decoded = PublicKeyRegistered::decode(&mut &events[2].data[..]).expect("decode event"); + assert_eq!(decoded.signer, accounts.bob); + assert_eq!(decoded.public_key, [0x05; 33]); + assert_eq!(decoded.timestamp, 99); + } + /// Brute-force recount helper mirroring the pre-#972 scan semantics: /// tallies each proposal by its *final* status and computes the /// participation average over Executed/Rejected proposals.