diff --git a/target_chains/starknet/contracts/src/wormhole.cairo b/target_chains/starknet/contracts/src/wormhole.cairo index 5f02c1a86a..a8462a950b 100644 --- a/target_chains/starknet/contracts/src/wormhole.cairo +++ b/target_chains/starknet/contracts/src/wormhole.cairo @@ -12,6 +12,7 @@ pub use interface::{ }; pub use wormhole::{Event, GuardianSetAdded}; +/// Implementation of the Wormhole contract. #[starknet::contract] mod wormhole { use pyth::util::UnwrapWithFelt252; @@ -30,36 +31,56 @@ mod wormhole { use core::panic_with_felt252; use pyth::util::{UNEXPECTED_OVERFLOW}; + /// Events emitted by the contract. #[event] #[derive(Drop, PartialEq, starknet::Event)] pub enum Event { GuardianSetAdded: GuardianSetAdded, } + /// Emitted when a new guardian set is added. #[derive(Drop, PartialEq, starknet::Event)] pub struct GuardianSetAdded { + /// Index of the new guardian set. pub index: u32, } + /// Guardian set storage. #[derive(Drop, Debug, Clone, Serde, starknet::Store)] struct GuardianSet { + /// Number of guardians in this guardian set. + /// Guardian keys are stored separately. num_guardians: usize, + /// Timestamp of expiry, or 0 if there is no expiration time. // XXX: storage doesn't work if we use Option here. expiration_time: u64, } #[storage] struct Storage { + /// ID of the chain the contract is deployed on. chain_id: u16, + /// ID of the chain containing the Wormhole governance contract. governance_chain_id: u16, + /// Address of the Wormhole governance contract. governance_contract: u256, + /// Index of the last added set. current_guardian_set_index: u32, + /// For every executed governance actions, contains an entry with + /// key = hash of the message and value = true. consumed_governance_actions: LegacyMap, + /// All known guardian sets. guardian_sets: LegacyMap, - // (guardian_set_index, guardian_index) => guardian_address + /// Public keys of guardians in all known guardian sets. + /// Key = (guardian_set_index, guardian_index). guardian_keys: LegacyMap<(u32, u8), EthAddress>, } + /// Initializes the contract. + /// `initial_guardians` is the list of public keys of guardians. + /// `chain_id` is the ID of the chain the contract is being deployed on. + /// `governance_chain_id` is the ID of the chain containing the Wormhole governance contract. + /// `governance_contract` is the address of the Wormhole governance contract. #[constructor] fn constructor( ref self: ContractState, @@ -136,7 +157,14 @@ mod wormhole { keys.append(self.guardian_keys.read((index, i))); i += 1; }; - super::GuardianSet { keys, expiration_time: set.expiration_time } + super::GuardianSet { + keys, + expiration_time: if set.expiration_time == 0 { + Option::None + } else { + Option::Some(set.expiration_time) + } + } } fn get_current_guardian_set_index(self: @ContractState) -> u32 { self.current_guardian_set_index.read() @@ -182,6 +210,8 @@ mod wormhole { #[generate_trait] impl PrivateImpl of PrivateImplTrait { + /// Validates the new guardian set and writes it to the storage. + /// `SubmitNewGuardianSetError` enumerates possible panic payloads. fn store_guardian_set( ref self: ContractState, set_index: u32, guardians: @Array ) { @@ -214,12 +244,15 @@ mod wormhole { self.current_guardian_set_index.write(set_index); } + /// Marks the specified guardian set to expire in 24 hours. fn expire_guardian_set(ref self: ContractState, set_index: u32, now: u64) { let mut set = self.guardian_sets.read(set_index); set.expiration_time = now + 86400; self.guardian_sets.write(set_index, set); } + /// Checks required properties of the governance instruction. + /// `GovernanceError` enumerates possible panic payloads. fn verify_governance_vm(self: @ContractState, vm: @VerifiedVM) { if self.current_guardian_set_index.read() != *vm.guardian_set_index { panic_with_felt252(GovernanceError::NotCurrentGuardianSet.into()); diff --git a/target_chains/starknet/contracts/src/wormhole/errors.cairo b/target_chains/starknet/contracts/src/wormhole/errors.cairo index bbd4aa905a..523266864c 100644 --- a/target_chains/starknet/contracts/src/wormhole/errors.cairo +++ b/target_chains/starknet/contracts/src/wormhole/errors.cairo @@ -1,3 +1,4 @@ +/// Possible panic payloads for methods that execute governance instructions. #[derive(Copy, Drop, Debug, Serde, PartialEq)] pub enum GovernanceError { InvalidModule, @@ -25,6 +26,7 @@ impl GovernanceErrorIntoFelt252 of Into { } } +/// Possible panic payloads of `IWormhole::submit_new_guardian_set`. #[derive(Copy, Drop, Debug, Serde, PartialEq)] pub enum SubmitNewGuardianSetError { Governance: GovernanceError, @@ -33,7 +35,6 @@ pub enum SubmitNewGuardianSetError { InvalidGuardianKey, // guardian set index must increase in steps of 1 InvalidGuardianSetSequence, - AccessDenied, } impl SubmitNewGuardianSetErrorIntoFelt252 of Into { @@ -44,12 +45,11 @@ impl SubmitNewGuardianSetErrorIntoFelt252 of Into 'too many guardians', SubmitNewGuardianSetError::InvalidGuardianKey => 'invalid guardian key', SubmitNewGuardianSetError::InvalidGuardianSetSequence => 'invalid guardian set sequence', - SubmitNewGuardianSetError::AccessDenied => 'access denied', } } } - +/// Possible panic payloads of `IWormhole::parse_and_verify_vm`. #[derive(Copy, Drop, Debug, Serde, PartialEq)] pub enum ParseAndVerifyVmError { Reader: pyth::reader::Error, @@ -77,6 +77,7 @@ impl ErrorIntoFelt252 of Into { } } +/// Possible panic payloads of `IWormhole::get_guardian_set`. #[derive(Copy, Drop, Debug, Serde, PartialEq)] pub enum GetGuardianSetError { InvalidIndex, diff --git a/target_chains/starknet/contracts/src/wormhole/governance.cairo b/target_chains/starknet/contracts/src/wormhole/governance.cairo index cf850d3612..f981affb79 100644 --- a/target_chains/starknet/contracts/src/wormhole/governance.cairo +++ b/target_chains/starknet/contracts/src/wormhole/governance.cairo @@ -31,19 +31,29 @@ impl U8TryIntoAction of TryInto { } } +/// Parsed header of a governance message. #[derive(Drop, Debug, Clone)] pub struct Header { + /// The destination module of this instruction. pub module: u256, + /// Type of action. pub action: Action, + /// The destination chain ID of this instruction, + /// or 0 if it's applicable to all chains. pub chain_id: u16, } +/// Payload of `GuardianSetUpgrade` instruction. #[derive(Drop, Debug, Clone)] pub struct NewGuardianSet { + /// Index of the new set. pub set_index: u32, + /// Public keys of guardians, in order. pub keys: Array, } +/// Parses the header of a governance instruction and verifies the module. +/// `GovernanceError` enumerates possible panic payloads. pub fn parse_header(ref reader: Reader) -> Header { let module = reader.read_u256(); if module != MODULE { @@ -55,6 +65,8 @@ pub fn parse_header(ref reader: Reader) -> Header { Header { module, action, chain_id } } +/// Parses the payload of `GuardianSetUpgrade` instruction. +/// `GovernanceError` enumerates possible panic payloads. pub fn parse_new_guardian_set(ref reader: Reader) -> NewGuardianSet { let set_index = reader.read_u32(); let num_guardians = reader.read_u8(); diff --git a/target_chains/starknet/contracts/src/wormhole/interface.cairo b/target_chains/starknet/contracts/src/wormhole/interface.cairo index 73adb36e72..dee61c8b8f 100644 --- a/target_chains/starknet/contracts/src/wormhole/interface.cairo +++ b/target_chains/starknet/contracts/src/wormhole/interface.cairo @@ -3,41 +3,84 @@ use pyth::byte_array::ByteArray; use core::starknet::secp256_trait::Signature; use core::starknet::EthAddress; +/// Wormhole provides a secure means for communication between multiple chains. +/// This contract allows users to parse and verify a Wormhole message that informs +/// them about a message that was produced by a contract on a Wormhole-supported chain. +/// +/// Note that this implementation does not support creating Wormhole messages. #[starknet::interface] pub trait IWormhole { + /// Parses and returns the contents of the message. Panics if there was a + /// parsing error or if signature verification failed. + /// `ParseAndVerifyVmError` enumerates possible panic payloads. fn parse_and_verify_vm(self: @T, encoded_vm: ByteArray) -> VerifiedVM; + /// Returns the list of guardians at the specified index. + /// `GetGuardianSetError` enumerates possible panic payloads. fn get_guardian_set(self: @T, index: u32) -> GuardianSet; + + /// Returns the index of the latest guardian set. Guardian sets with + /// lower indexes may still be supported unless they have already expired. fn get_current_guardian_set_index(self: @T) -> u32; + + /// Checks whether the governance action with the specified hash has already + /// been consumed by this contract. Actions that have been consumed cannot + /// be executed again. fn governance_action_is_consumed(self: @T, hash: u256) -> bool; + + /// Returns the ID of the chain on which the contract has been deployed. fn chain_id(self: @T) -> u16; + + /// Returns the ID of the chain containing the Wormhole governance contract. fn governance_chain_id(self: @T) -> u16; + + /// Returns the address of the Wormhole governance contract. fn governance_contract(self: @T) -> u256; // We don't need to implement other governance actions for now. // Instead of upgrading the Wormhole contract, we can switch to another Wormhole address // in the Pyth contract. + + /// Executes a governance instruction to add a new guardian set. The new set becomes + /// active immediately. The previous guardian set will be available for 24 hours and then expire. + /// `SubmitNewGuardianSetError` enumerates possible panic payloads. fn submit_new_guardian_set(ref self: T, encoded_vm: ByteArray); } +/// Information about a guardian's signature within a message. #[derive(Drop, Debug, Clone, Serde)] pub struct GuardianSignature { + /// Index of this guardian within the guardian set. pub guardian_index: u8, + /// The guardian's signature of the message. pub signature: Signature, } +/// A verified Wormhole message. #[derive(Drop, Debug, Clone, Serde)] pub struct VerifiedVM { + /// Version of the encoding format. pub version: u8, + /// Index of the guardian set that signed this message. pub guardian_set_index: u32, + /// Signatures of guardians. pub signatures: Array, + /// Creation time of the message. pub timestamp: u32, + /// Unique nonce of the message. pub nonce: u32, + /// ID of the chain on which the message was sent. pub emitter_chain_id: u16, + /// Address of the contract that sent the message. pub emitter_address: u256, + /// Sequence number of the message. pub sequence: u64, + /// Observed consistency level (specific to the sender's chain). pub consistency_level: u8, + /// The data attached to the message. pub payload: ByteArray, + /// Double keccak256 hash of all fields of the message, excluding `version`, + /// `guardian_set_index` and `signatures`. pub hash: u256, } @@ -47,8 +90,13 @@ pub fn quorum(num_guardians: usize) -> usize { ((num_guardians * 2) / 3) + 1 } +/// Information about a guardian set. #[derive(Drop, Debug, Clone, Serde)] pub struct GuardianSet { + /// Public keys of guardians, in order. pub keys: Array, - pub expiration_time: u64, + /// Timestamp of expiration, if any. The contract will not verify messages signed by + /// this guardian set if the guardian set has expired. The latest guardian set + /// does not have an expiration time. + pub expiration_time: Option, } diff --git a/target_chains/starknet/contracts/src/wormhole/parse_vm.cairo b/target_chains/starknet/contracts/src/wormhole/parse_vm.cairo index e99faae1b9..d2b2bf4d28 100644 --- a/target_chains/starknet/contracts/src/wormhole/parse_vm.cairo +++ b/target_chains/starknet/contracts/src/wormhole/parse_vm.cairo @@ -5,6 +5,8 @@ use core::starknet::secp256_trait::Signature; use pyth::byte_array::ByteArray; use core::panic_with_felt252; +/// Parses information about a guardian signature within a Wormhole message. +/// `pyth::reader::Error` enumerates possible panic payloads. fn parse_signature(ref reader: Reader) -> GuardianSignature { let guardian_index = reader.read_u8(); let r = reader.read_u256(); @@ -14,6 +16,8 @@ fn parse_signature(ref reader: Reader) -> GuardianSignature { GuardianSignature { guardian_index, signature: Signature { r, s, y_parity } } } +/// Parses a Wormhole message. +/// `ParseAndVerifyVmError` enumerates possible panic payloads. pub fn parse_vm(encoded_vm: ByteArray) -> VerifiedVM { let mut reader = ReaderImpl::new(encoded_vm); let version = reader.read_u8(); diff --git a/target_chains/starknet/contracts/tests/wormhole.cairo b/target_chains/starknet/contracts/tests/wormhole.cairo index 435dcbbfc8..34968ebbb7 100644 --- a/target_chains/starknet/contracts/tests/wormhole.cairo +++ b/target_chains/starknet/contracts/tests/wormhole.cairo @@ -156,38 +156,38 @@ fn test_get_guardian_set_works() { let set0 = dispatcher.get_guardian_set(0); assert!(set0.keys == guardian_set0()); - assert!(set0.expiration_time == 0); + assert!(set0.expiration_time.is_none()); assert!(dispatcher.get_current_guardian_set_index() == 0); dispatcher.submit_new_guardian_set(data::mainnet_guardian_set_upgrade1()); let set0 = dispatcher.get_guardian_set(0); assert!(set0.keys == guardian_set0()); - assert!(set0.expiration_time != 0); + assert!(set0.expiration_time.is_some()); let set1 = dispatcher.get_guardian_set(1); assert!(set1.keys == guardian_set1()); - assert!(set1.expiration_time == 0); + assert!(set1.expiration_time.is_none()); assert!(dispatcher.get_current_guardian_set_index() == 1); dispatcher.submit_new_guardian_set(data::mainnet_guardian_set_upgrade2()); let set0 = dispatcher.get_guardian_set(0); assert!(set0.keys == guardian_set0()); - assert!(set0.expiration_time != 0); + assert!(set0.expiration_time.is_some()); let set1 = dispatcher.get_guardian_set(1); assert!(set1.keys == guardian_set1()); - assert!(set1.expiration_time != 0); + assert!(set1.expiration_time.is_some()); let set2 = dispatcher.get_guardian_set(2); assert!(set2.keys == guardian_set2()); - assert!(set2.expiration_time == 0); + assert!(set2.expiration_time.is_none()); assert!(dispatcher.get_current_guardian_set_index() == 2); dispatcher.submit_new_guardian_set(data::mainnet_guardian_set_upgrade3()); dispatcher.submit_new_guardian_set(data::mainnet_guardian_set_upgrade4()); let set3 = dispatcher.get_guardian_set(3); assert!(set3.keys == guardian_set3()); - assert!(set3.expiration_time != 0); + assert!(set3.expiration_time.is_some()); let set4 = dispatcher.get_guardian_set(4); assert!(set4.keys == guardian_set4()); - assert!(set4.expiration_time == 0); + assert!(set4.expiration_time.is_none()); assert!(dispatcher.get_current_guardian_set_index() == 4); }