Skip to content

doc(target_chains/starknet): add wormhole code docs and small API improvements #1647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions target_chains/starknet/contracts/src/wormhole.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<u256, bool>,
/// All known guardian sets.
guardian_sets: LegacyMap<u32, GuardianSet>,
// (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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<EthAddress>
) {
Expand Down Expand Up @@ -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());
Expand Down
7 changes: 4 additions & 3 deletions target_chains/starknet/contracts/src/wormhole/errors.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// Possible panic payloads for methods that execute governance instructions.
#[derive(Copy, Drop, Debug, Serde, PartialEq)]
pub enum GovernanceError {
InvalidModule,
Expand Down Expand Up @@ -25,6 +26,7 @@ impl GovernanceErrorIntoFelt252 of Into<GovernanceError, felt252> {
}
}

/// Possible panic payloads of `IWormhole::submit_new_guardian_set`.
#[derive(Copy, Drop, Debug, Serde, PartialEq)]
pub enum SubmitNewGuardianSetError {
Governance: GovernanceError,
Expand All @@ -33,7 +35,6 @@ pub enum SubmitNewGuardianSetError {
InvalidGuardianKey,
// guardian set index must increase in steps of 1
InvalidGuardianSetSequence,
AccessDenied,
}

impl SubmitNewGuardianSetErrorIntoFelt252 of Into<SubmitNewGuardianSetError, felt252> {
Expand All @@ -44,12 +45,11 @@ impl SubmitNewGuardianSetErrorIntoFelt252 of Into<SubmitNewGuardianSetError, fel
SubmitNewGuardianSetError::TooManyGuardians => '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,
Expand Down Expand Up @@ -77,6 +77,7 @@ impl ErrorIntoFelt252 of Into<ParseAndVerifyVmError, felt252> {
}
}

/// Possible panic payloads of `IWormhole::get_guardian_set`.
#[derive(Copy, Drop, Debug, Serde, PartialEq)]
pub enum GetGuardianSetError {
InvalidIndex,
Expand Down
12 changes: 12 additions & 0 deletions target_chains/starknet/contracts/src/wormhole/governance.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,29 @@ impl U8TryIntoAction of TryInto<u8, Action> {
}
}

/// 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<EthAddress>,
}

/// 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 {
Expand All @@ -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();
Expand Down
50 changes: 49 additions & 1 deletion target_chains/starknet/contracts/src/wormhole/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
/// 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<GuardianSignature>,
/// 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,
}

Expand All @@ -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<EthAddress>,
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<u64>,
}
4 changes: 4 additions & 0 deletions target_chains/starknet/contracts/src/wormhole/parse_vm.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
16 changes: 8 additions & 8 deletions target_chains/starknet/contracts/tests/wormhole.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading