examples: add solana sdk examples

Change-Id: I2f0cc982c880ba1830ad1e1c4a4a409876c4ad66
This commit is contained in:
Reisen 2021-12-08 14:33:52 +00:00 committed by Reisen
parent 7f1c7ec62f
commit ca1f0264b0
10 changed files with 4305 additions and 0 deletions

View File

@ -0,0 +1,14 @@
[package]
name = "wormhole-messenger-common"
version = "0.1.0"
edition = "2018"
[lib]
name = "messenger_common"
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "0.8.1"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -0,0 +1,13 @@
use borsh::{
BorshDeserialize,
BorshSerialize,
};
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)]
pub struct Message {
/// Messenger/DM username.
pub nick: String,
/// Message text to be output on the target networks node logs.
pub text: String,
}

View File

@ -0,0 +1,21 @@
# Uses 2018 for maximum compatibiilty with Solana.
edition = "2018"
# Merge similar crates together to avoid multiple use statements.
imports_granularity = "Module"
# Consistency in formatting makes tool based searching/editing better.
empty_item_single_line = false
# Easier editing when arbitrary mixed use statements do not collapse.
imports_layout = "Vertical"
# Default rustfmt formatting of match arms with branches is awful.
match_arm_leading_pipes = "Preserve"
# Align Fields
enum_discrim_align_threshold = 80
struct_field_align_threshold = 80
# Allow up to two blank lines for grouping.
blank_lines_upper_bound = 2

3602
examples/messenger/solana/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
[package]
name = "wormhole-messenger-solana"
version = "0.1.0"
edition = "2018"
[lib]
name = "messenger"
crate-type = ["cdylib", "lib"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
borsh = { version="=0.8.1" }
solana-program = { version="=1.7.0" }
nom = { version="7", default-features=false, features=["alloc"] }
wormhole-sdk = { path = "../../../sdk/rust/sdk", features = ["devnet", "solana"] }
wormhole-messenger-common = { path = "../common" }
[dev-dependencies]
borsh = "=0.8.1"
solana-program-test = "=1.7.0"
solana-sdk = "=1.7.0"
rand = "0.7.3"
libsecp256k1 = "0.3.5"
hex = "*"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

Binary file not shown.

View File

@ -0,0 +1,108 @@
use borsh::BorshSerialize;
use solana_program::instruction::{
AccountMeta,
Instruction,
};
use solana_program::pubkey::Pubkey;
use solana_program::system_program;
use solana_program::sysvar::rent;
use solana_program::sysvar::clock;
use wormhole_sdk::{
id,
config,
fee_collector,
sequence,
};
use messenger_common::Message;
use crate::Instruction::{
RecvMessage,
SendMessage,
};
/// Create a RecvMessage instruction.
pub fn recv_message(program_id: Pubkey, payer: Pubkey, vaa: Pubkey) -> Instruction {
Instruction {
program_id,
data: RecvMessage.try_to_vec().unwrap(),
accounts: vec![
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(vaa, false),
],
}
}
/// Create a SendMessage instruction.
pub fn send_message(
program_id: Pubkey,
payer: Pubkey,
emitter: Pubkey,
message: Pubkey,
payload: Message,
nonce: u32,
) -> Instruction {
let wormhole = id();
let config = config(&wormhole);
let fee_collector = fee_collector(&wormhole);
let sequence = sequence(&wormhole, &emitter);
// Note that accounts are passed in in order of useful-ness. The payer and message accounts are
// used to invoke Wormhole. Many of the example send_message* instruction handlers will only
// pop off as many accounts as required.
Instruction {
program_id,
accounts: vec![
AccountMeta::new(payer, true),
AccountMeta::new(message, true),
AccountMeta::new(fee_collector, false),
AccountMeta::new(config, false),
AccountMeta::new_readonly(emitter, false),
AccountMeta::new(sequence, false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(wormhole, false),
],
data: SendMessage(payload, nonce).try_to_vec().unwrap(),
}
}
/// Create a SendMessageRaw instruction. This does the same as SendMessage however the instruction
/// handler does not use the Wormhole SDK helper API.
pub fn send_message_raw(
program_id: Pubkey,
payer: Pubkey,
emitter: Pubkey,
message: Pubkey,
payload: Message,
nonce: u32,
) -> Instruction {
let wormhole = id();
let config = config(&wormhole);
let fee_collector = fee_collector(&wormhole);
let sequence = sequence(&wormhole, &emitter);
// Note that accounts are passed in in order of useful-ness. The payer and message accounts are
// used to invoke Wormhole. Many of the example send_message* instruction handlers will only
// pop off as many accounts as required.
Instruction {
program_id,
accounts: vec![
AccountMeta::new(payer, true),
AccountMeta::new(message, true),
AccountMeta::new(fee_collector, false),
AccountMeta::new(config, false),
AccountMeta::new_readonly(emitter, false),
AccountMeta::new(sequence, false),
AccountMeta::new_readonly(clock::id(), false),
AccountMeta::new_readonly(rent::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(wormhole, false),
],
data: SendMessage(payload, nonce).try_to_vec().unwrap(),
}
}

View File

@ -0,0 +1,176 @@
#![deny(unused_must_use)]
// A common serialization library used in the blockchain space, which we'll use to serialize our
// cross chain message payloads.
use borsh::{
BorshDeserialize,
BorshSerialize,
};
// Solana SDK imports to interact with the solana runtime.
use solana_program::account_info::{
next_account_info,
AccountInfo,
};
use solana_program::entrypoint::ProgramResult;
use solana_program::program::invoke_signed;
use solana_program::pubkey::Pubkey;
use solana_program::{
entrypoint,
msg,
};
// Import Solana Wormhole SDK.
use wormhole_sdk::{
instructions::post_message,
ConsistencyLevel,
VAA,
};
// Our Payload, defined in a common library.
pub use messenger_common::Message;
pub mod instruction;
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub enum Instruction {
/// This instruction is used to send a message to another chain by emitting it as a wormhole
/// message targetting another users key.
///
/// 0: Payer [Signer]
/// 1: Message [Signer]
/// 2: Worm Config [PDA]
/// 3: Worm Fee [PDA]
/// 4: Worm Sequence [PDA]
/// 5: Emitter [PDA, Signer]
/// 6: Clock [Program] -- Needed for wormhole to take block times.
/// 7: Rent [Program] -- Needed for wormhole fee calculation on the message account.
/// 8: System [Program] -- Needed for wormhole to take fees.
/// 9: Wormhole [Program] -- Needed for wormhole invoke_signed.
SendMessage(Message, u32),
/// This is the same as the above message, but the example handler is more low level.
SendMessageRaw(Message, u32),
/// This instruction receives a message by processing an incoming VAA containing a message
/// intended for a receiver on Solana. Note that the simple existence of the VAA account is
/// enough to verify it as the account is only created by the bridge if the guardians had
/// successfull signed it.
///
/// 0: VAA [PDA]
RecvMessage,
}
entrypoint!(process_instruction);
/// The Solana entrypoint, here we deserialize our Borsh encoded Instruction and dispatch to our
/// program handlers.
pub fn process_instruction(id: &Pubkey, accs: &[AccountInfo], data: &[u8]) -> ProgramResult {
match BorshDeserialize::try_from_slice(data).unwrap() {
// Send Message Variants. Check the source of each to see various ways to invoke Wormhole.
Instruction::SendMessage(msg, nonce) => send_message(id, accs, msg, nonce),
Instruction::SendMessageRaw(msg, nonce) => send_message_raw(id, accs, msg, nonce),
// RecvMessage shows an example of safely processing a VAA.
Instruction::RecvMessage => recv_message(id, accs),
}?;
Ok(())
}
/// Send a Message from this chain to a user on a remote target chain.
///
/// This method is a reference example of emitting messages via Wormhole using the ergonomic API
/// methods. This is the easiest way to use Wormhole.
fn send_message(id: &Pubkey, accounts: &[AccountInfo], payload: Message, nonce: u32) -> ProgramResult {
let iter = &mut accounts.iter();
let payer = next_account_info(iter)?;
let message = next_account_info(iter)?;
// This helper method will take care of all of the following for you:
//
// - Derives a reasonable emitter PDA for your program.
// - Pays the Bridge (Payer Key)
// - Emits a Message
wormhole_sdk::post_message(
*id,
*payer.key,
*message.key,
payload.try_to_vec()?,
ConsistencyLevel::Finalized,
None,
accounts,
nonce,
)?;
Ok(())
}
/// Send a Message from this chain to a user on a remote target chain.
///
/// This method is a reference example of emitting messages via Wormhole using the most low level
/// interface provided by the SDK. You must handle the emitter, payment, and invoking yourself.
fn send_message_raw(id: &Pubkey, accs: &[AccountInfo], payload: Message, nonce: u32) -> ProgramResult {
let accounts = &mut accs.iter();
let payer = next_account_info(accounts)?;
let message = next_account_info(accounts)?;
let fee_collector = next_account_info(accounts)?;
let config = next_account_info(accounts)?;
// Deserialize Bridge Config, used to figure out what the fee is so we can pay the bridge
// programatically.
let config = wormhole_sdk::read_config(config).unwrap();
// Pay Fee to the Wormhole.
invoke_signed(
&solana_program::system_instruction::transfer(payer.key, fee_collector.key, config.fee),
accs,
&[],
)?;
// Create an Emitter to emit messages from, this helper method is producing the emitter from
// the _current_ program's ID.
let (emitter, mut seeds, bump) = wormhole_sdk::emitter(id);
let bump = &[bump];
seeds.push(bump);
// Invoke the Wormhole post_message endpoint to create an on-chain message.
invoke_signed(
&post_message(
wormhole_sdk::id(),
*payer.key,
emitter,
*message.key,
nonce,
payload.try_to_vec()?,
ConsistencyLevel::Finalized,
)
.unwrap(),
accs,
&[&seeds],
)?;
Ok(())
}
/// Receives a VAA containing a message from a foreign chain, and parses/verifies the VAA to
/// validate the message has been safely attested by the guardian set. Prints the message in
/// validator logs.
fn recv_message(id: &Pubkey, accs: &[AccountInfo]) -> ProgramResult {
// We must verify the VAA is legitimately signed by the guardians. We do this by deriving the
// expected PDA derived by the bridge, as long as we produce the same account we can trust the
// contents of the VAA.
let accounts = &mut accs.iter();
let payer = next_account_info(accounts)?;
let vaa = next_account_info(accounts)?;
// If we want to avoid processing a message twice we need to track whether we have already
// processed a VAA manually. There are several ways to do this in Solana but in this example
// we will simply reprocess VAA's.
let vaa = wormhole_sdk::read_vaa(vaa).unwrap();
let msg = Message::try_from_slice(&vaa.payload)?;
msg!("{}: {}", msg.nick, msg.text);
Ok(())
}

View File

@ -0,0 +1,339 @@
//! Test the Messenger program.
//!
//! You will need a `bridge.so` somewhere in the path of the tests. The easiest way to do this is
//! to build the Wormhole bridge and copy the `.so` into the current directory:
//!
//! ```
//! $ pushd <BRIDGE>
//! $ EMITTER_ADDRESS="0000000000000000000000000000000000000000000000000000000000000004" cargo build-bpf
//! $ popd
//! $ cp <BRIDGE>/target/deploy/bridge.so .
//! ```
//!
//! This will give you a BPF ELF that the ProgramTest framework can deploy, if you don't do this
//! ProgramTest will try and compile the processor in native mode which cannot currently handle
//! accounts that change size, which Wormhole relies on.
use std::convert::TryInto;
use std::str::FromStr;
// Solana Requirements
use solana_program::pubkey::Pubkey;
use solana_program_test::{
processor,
tokio,
ProgramTest,
ProgramTestContext,
};
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_sdk::transaction::Transaction;
use solana_sdk::secp256k1_instruction::new_secp256k1_instruction;
// Import necessary components from the Messenger Program so we can test them.
use messenger::Message;
use messenger::process_instruction;
use messenger::instruction::{
send_message,
recv_message,
send_message_raw,
};
// We utilise the bridge_endpoint, which is re-exposed by the SDK, to run instructions against
// within the Solana program test framework.
use wormhole_sdk::Chain;
use wormhole_sdk::MessageData;
use wormhole_sdk::VAA;
use wormhole_sdk::PostVAAData;
use wormhole_sdk::VerifySignaturesData;
use wormhole_sdk::bridge_entrypoint;
// Borsh to deserialise the Wormhole Message account for asserting data.
use borsh::BorshDeserialize;
// Secp256k1 so we can produce a Guardian secret key to test with.
use secp256k1::SecretKey;
/// We need an address to deploy our Messenger program at, we just hardcode one for use throughout
/// the tests.
const ID: Pubkey = Pubkey::new_from_array([2u8; 32]);
#[tokio::test]
pub async fn test_publish_message() {
// Guardian
let guardian = hex::decode("B7f0900393F869eE15E00e01Dc71E7ba8590E51f").unwrap();
let guardian = &guardian.try_into().unwrap();
// Initialize Test Environment with instruction processors. This lets us load the wormhole
// processor into scope so we can inspect whether the messages it emits are in fact as we
// expect them to be.
let mut context = {
let mut test = ProgramTest::default();
test.add_program("bridge", wormhole_sdk::id(), processor!(bridge_entrypoint));
test.add_program("messenger", ID, processor!(process_instruction));
test.start_with_context().await
};
// Initialize Wormhole
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[wormhole_sdk::instructions::initialize(
wormhole_sdk::id(),
context.payer.pubkey(),
50,
2_000_000_000,
&[*guardian],
).unwrap()],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
))
.await
.unwrap();
// Message & Emitter Account Keys
let message = Keypair::new();
let emitter = wormhole_sdk::emitter(&ID);
// Submit a cross-chain message via Wormhole.
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[send_message(
ID,
context.payer.pubkey(),
emitter.0,
message.pubkey(),
Message {
nick: "Alice".to_string(),
text: "Hello from Bob!".to_string(),
},
0,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &message],
context.last_blockhash,
))
.await
.unwrap();
// We should now be able to find a Message on chain emitted by our contract. Let's verify it
// contains the expected data.
let message = MessageData::try_from_slice(
&context
.banks_client
.get_account(message.pubkey())
.await
.unwrap()
.unwrap()
.data[3..]
).unwrap();
assert_eq!(message.vaa_version, 0);
assert_eq!(message.consistency_level, 32);
assert_eq!(message.vaa_time, 0);
assert_eq!(message.nonce, 0);
assert_eq!(message.emitter_chain, 1);
assert_eq!(message.emitter_address, emitter.0.to_bytes());
assert_eq!(
Message::try_from_slice(&message.payload).unwrap(),
Message {
nick: "Alice".to_string(),
text: "Hello from Bob!".to_string(),
}
);
// Simulate Guardian behaviour: detecting message, signing, posting VAA.
let vaa = simulate_guardians(&mut context, &message).await;
// We can now test the recv_message endpoint by submitting the signed VAA.
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[recv_message(
ID,
context.payer.pubkey(),
vaa,
)],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
))
.await
.unwrap();
}
#[tokio::test]
pub async fn test_publish_message_raw() {
// Guardian
let guardian = hex::decode("966745cb54d907a93272dd154e1d1bb94b38c69b").unwrap();
let guardian = &guardian.try_into().unwrap();
// Initialize Test Environment with instruction processors. This lets us load the wormhole
// processor into scope so we can inspect whether the messages it emits are in fact as we
// expect them to be.
let mut context = {
let mut test = ProgramTest::default();
test.add_program("bridge", wormhole_sdk::id(), processor!(bridge_entrypoint));
test.add_program("messenger", ID, processor!(process_instruction));
test.start_with_context().await
};
// Initialize Wormhole
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[wormhole_sdk::instructions::initialize(
wormhole_sdk::id(),
context.payer.pubkey(),
50,
2_000_000_000,
&[*guardian],
).unwrap()],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
))
.await
.unwrap();
// Message & Emitter Account Keys
let message = Keypair::new();
let emitter = wormhole_sdk::emitter(&ID);
// Submit a cross-chain message via Wormhole.
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[send_message_raw(
ID,
context.payer.pubkey(),
emitter.0,
message.pubkey(),
Message {
nick: "Alice".to_string(),
text: "Hello from Bob!".to_string(),
},
1,
)],
Some(&context.payer.pubkey()),
&[&context.payer, &message],
context.last_blockhash,
))
.await
.unwrap();
// Check Resulting Message on Chain
let message = MessageData::try_from_slice(
&context
.banks_client
.get_account(message.pubkey())
.await
.unwrap()
.unwrap()
.data[3..]
).unwrap();
assert_eq!(message.vaa_version, 0);
assert_eq!(message.consistency_level, 32);
assert_eq!(message.vaa_time, 0);
assert_eq!(message.nonce, 1);
assert_eq!(message.emitter_chain, 1);
assert_eq!(message.emitter_address, emitter.0.to_bytes());
assert_eq!(
Message::try_from_slice(&message.payload).unwrap(),
Message {
nick: "Alice".to_string(),
text: "Hello from Bob!".to_string(),
}
);
}
pub async fn simulate_guardians(context: &mut ProgramTestContext, message: &MessageData) -> Pubkey {
// Emulate Guardian signatures by signing manually. First we produce a VAA.
let vaa = {
let mut vaa = VAA::default();
vaa.timestamp = message.submission_time;
vaa.nonce = message.nonce;
vaa.emitter_chain = Chain::Solana;
vaa.emitter_address = message.emitter_address;
vaa.sequence = message.sequence;
vaa.consistency_level = message.consistency_level;
vaa.payload = message.payload.clone();
vaa
};
// Hash the body to produce our message to sign.
let body = vaa.digest().unwrap();
// Place to store Signatures on Solana.
let signatures = Keypair::new();
let mut signers = [-1; 19];
signers[0] = 0;
// Verify Signatures
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[
new_secp256k1_instruction(
&SecretKey::parse(
(&*hex::decode("ff2f9d893e5c12618c442b34a98cfa3f646c402bf5e2a180ce761a7d8a43d452")
.unwrap())
.try_into()
.unwrap()
).unwrap(),
&body
),
wormhole_sdk::instructions::verify_signatures(
wormhole_sdk::id(),
context.payer.pubkey(),
0,
signatures.pubkey(),
VerifySignaturesData { signers },
).unwrap(),
],
Some(&context.payer.pubkey()),
&[&context.payer, &signatures],
context.last_blockhash,
))
.await
.unwrap();
context
.banks_client
.process_transaction(Transaction::new_signed_with_payer(
&[
wormhole_sdk::instructions::post_vaa(
wormhole_sdk::id(),
context.payer.pubkey(),
signatures.pubkey(),
PostVAAData {
version: 0,
guardian_set_index: 0,
timestamp: vaa.timestamp,
nonce: vaa.nonce,
emitter_chain: vaa.emitter_chain as u16,
emitter_address: vaa.emitter_address,
sequence: vaa.sequence,
consistency_level: vaa.consistency_level,
payload: vaa.payload,
},
),
],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
))
.await
.unwrap();
// Derive VAA Destination.
Pubkey::find_program_address(
&[b"PostedVAA", &body],
&wormhole_sdk::id(),
).0
}