Skip to content

Commit d20099a

Browse files
authored
Guibescos/executor (#283)
* Start anchor program * pythnet folder * Renames * Implement processor * Comments * More comments * Bump anchor * Use new version of the wormhole package * Get Solana chain id from wormhole core * Pythnet bridge address * Remove comment * Fix borsh headers
1 parent 2ad991f commit d20099a

File tree

8 files changed

+587
-136
lines changed

8 files changed

+587
-136
lines changed

pythnet/remote-executor/Cargo.lock

Lines changed: 343 additions & 133 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pythnet/remote-executor/programs/remote-executor/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ default = []
1919
overflow-checks = true
2020

2121
[dependencies]
22-
anchor-lang = "0.24.2"
22+
anchor-lang = {version = "0.25.0", features = ["init-if-needed"]}
23+
wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "gbescos/sdk-solana"}
24+
wormhole-core = { git = "https://github.com/guibescos/wormhole", branch = "gbescos/sdk-solana"}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
use anchor_lang::prelude::*;
3+
4+
#[error_code]
5+
pub enum ExecutorError {
6+
EmitterChainNotSolana,
7+
NonIncreasingSequence,
8+
GovernanceHeaderInvalidMagicNumber,
9+
GovernanceHeaderInvalidModule,
10+
GovernanceHeaderInvalidAction,
11+
GovernanceHeaderInvalidReceiverChain
12+
}
Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,61 @@
1-
use anchor_lang::prelude::*;
1+
#![deny(warnings)]
2+
3+
use anchor_lang::{prelude::*, solana_program::borsh::get_packed_len};
4+
use state::{claim_record::ClaimRecord, posted_vaa::AnchorVaa};
5+
6+
mod state;
7+
mod error;
28

39
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
410

511
#[program]
612
pub mod remote_executor {
13+
use anchor_lang::solana_program::{program::invoke_signed, instruction::Instruction};
14+
use wormhole::Chain::{Solana, self};
15+
16+
use crate::{state::{governance_payload::ExecutorPayload}, error::ExecutorError};
17+
718
use super::*;
819

920
pub fn execute_posted_vaa(ctx: Context<ExecutePostedVaa>) -> Result<()> {
21+
let posted_vaa = &ctx.accounts.posted_vaa;
22+
let claim_record = &mut ctx.accounts.claim_record;
23+
assert_or_err(Chain::from(posted_vaa.emitter_chain) == Solana, err!(ExecutorError::EmitterChainNotSolana))?;
24+
assert_or_err(posted_vaa.sequence > claim_record.sequence, err!(ExecutorError::NonIncreasingSequence))?;
25+
claim_record.sequence = posted_vaa.sequence;
26+
27+
let payload = ExecutorPayload::try_from_slice(&posted_vaa.payload)?;
28+
payload.check_header()?;
29+
30+
for instruction in payload.instructions.iter().map(Instruction::from) {
31+
// TO DO: We currently pass `remaining_accounts` down to the CPIs, is there a more efficient way to do it?
32+
invoke_signed(&instruction, ctx.remaining_accounts, &[&[EXECUTOR_KEY_SEED.as_bytes(), &posted_vaa.emitter_address, &[*ctx.bumps.get("executor_key").unwrap()]]])?;
33+
}
1034
Ok(())
1135
}
1236
}
1337

38+
const EXECUTOR_KEY_SEED : &str = "EXECUTOR_KEY";
39+
const CLAIM_RECORD_SEED : &str = "CLAIM_RECORD";
40+
41+
1442
#[derive(Accounts)]
15-
pub struct ExecutePostedVaa {}
43+
pub struct ExecutePostedVaa<'info> {
44+
#[account(mut)]
45+
pub payer : Signer<'info>,
46+
pub posted_vaa : Account<'info, AnchorVaa>,
47+
#[account(seeds = [EXECUTOR_KEY_SEED.as_bytes(), &posted_vaa.emitter_address], bump)]
48+
pub executor_key : UncheckedAccount<'info>,
49+
/// The reason claim record is separated from executor_key is that executor key might need to pay in the CPI, so we want it to be a wallet
50+
#[account(init_if_needed, space = 8 + get_packed_len::<ClaimRecord>(), payer=payer, seeds = [CLAIM_RECORD_SEED.as_bytes(), &posted_vaa.emitter_address], bump)]
51+
pub claim_record : Account<'info, ClaimRecord>,
52+
pub system_program: Program<'info, System>
53+
}
54+
55+
pub fn assert_or_err(condition : bool, error : Result<()>) -> Result<()>{
56+
if !condition {
57+
error
58+
} else {
59+
Result::Ok(())
60+
}
61+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_lang::account;
3+
use anchor_lang::prelude::borsh::BorshSchema;
4+
5+
#[account]
6+
#[derive(Default, BorshSchema)]
7+
/// This struct records
8+
pub struct ClaimRecord{
9+
pub sequence : u64
10+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::{ops::Deref, mem::size_of, io::ErrorKind};
2+
3+
use anchor_lang::{prelude::*, solana_program::instruction::Instruction};
4+
use wormhole::Chain;
5+
6+
use crate::{assert_or_err, error::ExecutorError};
7+
8+
pub const MAGIC_NUMBER : u32 = 0x4d475450; // Reverse order of the solidity contract because borsh uses little endian numbers
9+
10+
#[derive(AnchorDeserialize, AnchorSerialize)]
11+
pub struct ExecutorPayload{
12+
pub header : GovernanceHeader,
13+
pub instructions: Vec<InstructionData>,
14+
15+
}
16+
17+
#[derive(AnchorDeserialize, AnchorSerialize, PartialEq)]
18+
pub enum Module {
19+
Executor = 0,
20+
Target
21+
}
22+
23+
#[derive(AnchorDeserialize, AnchorSerialize, PartialEq)]
24+
pub enum Action {
25+
ExecutePostedVaa = 0,
26+
}
27+
28+
29+
/// The Governance Header format for pyth governance messages is the following:
30+
/// - A 4 byte magic number `['P','T','G','M']`
31+
/// - A one byte module variant (0 for Executor and 1 for Target contracts)
32+
/// - A one byte action variant (for Executor only 0 is currently valid)
33+
/// - A bigendian 2 bytes u16 chain id
34+
#[derive(AnchorDeserialize, AnchorSerialize)]
35+
pub struct GovernanceHeader {
36+
pub magic_number : u32,
37+
pub module : Module,
38+
pub action : Action,
39+
pub chain : BigEndianU16
40+
}
41+
42+
/// Hack to get Borsh to deserialize, serialize this number with big endian order
43+
pub struct BigEndianU16{
44+
pub value : u16
45+
}
46+
47+
impl AnchorDeserialize for BigEndianU16 {
48+
fn deserialize(buf: &mut &[u8]) -> std::result::Result<BigEndianU16, std::io::Error> {
49+
if buf.len() < size_of::<u16>() {
50+
return Err(std::io::Error::new(
51+
ErrorKind::InvalidInput,
52+
"Unexpected length of input",
53+
));
54+
}
55+
let res = u16::from_be_bytes(buf[..size_of::<u16>()].try_into().unwrap());
56+
*buf = &buf[size_of::<u16>()..];
57+
Ok(BigEndianU16{ value:res})
58+
}
59+
}
60+
61+
impl AnchorSerialize for BigEndianU16 {
62+
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
63+
writer.write_all(&self.to_be_bytes())
64+
}
65+
}
66+
67+
impl Deref for BigEndianU16 {
68+
type Target = u16;
69+
70+
fn deref(&self) -> &Self::Target {
71+
&self.value
72+
}
73+
}
74+
75+
/// InstructionData wrapper. It can be removed once Borsh serialization for Instruction is supported in the SDK
76+
#[derive(Clone, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
77+
pub struct InstructionData {
78+
/// Pubkey of the instruction processor that executes this instruction
79+
pub program_id: Pubkey,
80+
/// Metadata for what accounts should be passed to the instruction processor
81+
pub accounts: Vec<AccountMetaData>,
82+
/// Opaque data passed to the instruction processor
83+
pub data: Vec<u8>,
84+
}
85+
86+
/// Account metadata used to define Instructions
87+
#[derive(Clone, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
88+
pub struct AccountMetaData {
89+
/// An account's public key
90+
pub pubkey: Pubkey,
91+
/// True if an Instruction requires a Transaction signature matching `pubkey`.
92+
pub is_signer: bool,
93+
/// True if the `pubkey` can be loaded as a read-write account.
94+
pub is_writable: bool,
95+
}
96+
97+
impl From<&InstructionData> for Instruction {
98+
fn from(instruction: &InstructionData) -> Self {
99+
Instruction {
100+
program_id: instruction.program_id,
101+
accounts: instruction
102+
.accounts
103+
.iter()
104+
.map(|a| AccountMeta {
105+
pubkey: a.pubkey,
106+
is_signer: a.is_signer,
107+
is_writable: a.is_writable,
108+
})
109+
.collect(),
110+
data: instruction.data.clone(),
111+
}
112+
}
113+
}
114+
115+
impl ExecutorPayload {
116+
const MODULE : Module = Module::Executor;
117+
const ACTION : Action = Action::ExecutePostedVaa;
118+
119+
pub fn check_header(&self) -> Result<()>{
120+
assert_or_err(self.header.magic_number == MAGIC_NUMBER, err!(ExecutorError::GovernanceHeaderInvalidMagicNumber))?;
121+
assert_or_err(self.header.module == ExecutorPayload::MODULE, err!(ExecutorError::GovernanceHeaderInvalidMagicNumber))?;
122+
assert_or_err(self.header.action == ExecutorPayload::ACTION, err!(ExecutorError::GovernanceHeaderInvalidMagicNumber))?;
123+
assert_or_err(Chain::from(self.header.chain.value) == Chain::Pythnet, err!(ExecutorError::GovernanceHeaderInvalidMagicNumber))
124+
}
125+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod claim_record;
2+
pub mod posted_vaa;
3+
pub mod governance_payload;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use std::{io::Write, str::FromStr, ops::Deref};
2+
use anchor_lang::prelude::*;
3+
use wormhole_solana::VAA;
4+
5+
impl Owner for AnchorVaa {
6+
fn owner() -> Pubkey{
7+
Pubkey::from_str("H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU").unwrap() // Pythnet bridge address
8+
}
9+
}
10+
11+
impl AccountDeserialize for AnchorVaa {
12+
// Manual implementation because this account does not have an anchor discriminator
13+
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
14+
Self::try_deserialize_unchecked(buf)
15+
}
16+
17+
// Manual implementation because this account does not have an anchor discriminator
18+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
19+
AnchorDeserialize::deserialize(buf)
20+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
21+
}
22+
}
23+
24+
impl AccountSerialize for AnchorVaa {
25+
// Make this fail, this is readonly VAA it should never be serialized by this program
26+
fn try_serialize<W: Write>(&self, _writer: &mut W) -> Result<()> {
27+
return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
28+
}
29+
}
30+
31+
impl Deref for AnchorVaa {
32+
type Target = VAA;
33+
34+
fn deref(&self) -> &Self::Target {
35+
&self.vaa
36+
}
37+
}
38+
39+
40+
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
41+
pub struct AnchorVaa{
42+
pub vaa : VAA
43+
}

0 commit comments

Comments
 (0)