Migrate to sighash based method dispatch (#64)

This commit is contained in:
Armani Ferrante 2021-02-06 16:28:33 +08:00 committed by GitHub
parent 170e6f18d4
commit 48b27e6943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 344 additions and 155 deletions

View File

@ -11,6 +11,8 @@ incremented for features.
## [Unreleased] ## [Unreleased]
* lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64).
## [0.1.0] - 2021-01-31 ## [0.1.0] - 2021-01-31
Initial release. Initial release.

3
Cargo.lock generated
View File

@ -176,12 +176,15 @@ name = "anchor-syn"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bs58",
"heck", "heck",
"proc-macro2 1.0.24", "proc-macro2 1.0.24",
"quote 1.0.8", "quote 1.0.8",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.9.3",
"syn 1.0.57", "syn 1.0.57",
"thiserror",
] ]
[[package]] [[package]]

View File

@ -6,12 +6,12 @@ use anchor_client::solana_sdk::sysvar;
use anchor_client::Client; use anchor_client::Client;
use anyhow::Result; use anyhow::Result;
// The `accounts` and `instructions` modules are generated by the framework. // The `accounts` and `instructions` modules are generated by the framework.
use basic_2::accounts::CreateAuthor; use basic_2::accounts as basic_2_accounts;
use basic_2::instruction::Basic2Instruction; use basic_2::instruction as basic_2_instruction;
use basic_2::Author; use basic_2::Counter;
// The `accounts` and `instructions` modules are generated by the framework. // The `accounts` and `instructions` modules are generated by the framework.
use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize}; use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize};
use composite::instruction::CompositeInstruction; use composite::instruction as composite_instruction;
use composite::{DummyA, DummyB}; use composite::{DummyA, DummyB};
use rand::rngs::OsRng; use rand::rngs::OsRng;
@ -38,7 +38,7 @@ fn main() -> Result<()> {
// Make sure to run a localnet with the program deploy to run this example. // Make sure to run a localnet with the program deploy to run this example.
fn composite(client: &Client) -> Result<()> { fn composite(client: &Client) -> Result<()> {
// Deployed program to execute. // Deployed program to execute.
let pid = "75TykCe6b1oBa8JWVvfkXsFbZydgqi3QfRjgBEJJwy2g" let pid = "CD4y4hpiqB9N3vo2bAmZofsZuFmCnScqDPXejZSTeCV9"
.parse() .parse()
.unwrap(); .unwrap();
@ -73,7 +73,7 @@ fn composite(client: &Client) -> Result<()> {
dummy_b: dummy_b.pubkey(), dummy_b: dummy_b.pubkey(),
rent: sysvar::rent::ID, rent: sysvar::rent::ID,
}) })
.args(CompositeInstruction::Initialize) .args(composite_instruction::Initialize)
.send()?; .send()?;
// Assert the transaction worked. // Assert the transaction worked.
@ -93,7 +93,7 @@ fn composite(client: &Client) -> Result<()> {
dummy_b: dummy_b.pubkey(), dummy_b: dummy_b.pubkey(),
}, },
}) })
.args(CompositeInstruction::CompositeUpdate { .args(composite_instruction::CompositeUpdate {
dummy_a: 1234, dummy_a: 1234,
dummy_b: 4321, dummy_b: 4321,
}) })
@ -115,14 +115,14 @@ fn composite(client: &Client) -> Result<()> {
// Make sure to run a localnet with the program deploy to run this example. // Make sure to run a localnet with the program deploy to run this example.
fn basic_2(client: &Client) -> Result<()> { fn basic_2(client: &Client) -> Result<()> {
// Deployed program to execute. // Deployed program to execute.
let program_id = "FU3yvTEGTFUdMa6qAjVyKfNcDU6hb4yXbPhz8f5iFyvE" let program_id = "DXfgYBD7A3DvFDJoCTcS81EnyxfwXyeYadH5VdKMhVEx"
.parse() .parse()
.unwrap(); .unwrap();
let program = client.program(program_id); let program = client.program(program_id);
// `CreateAuthor` parameters. // `Create` parameters.
let author = Keypair::generate(&mut OsRng); let counter = Keypair::generate(&mut OsRng);
let authority = program.payer(); let authority = program.payer();
// Build and send a transaction. // Build and send a transaction.
@ -130,26 +130,23 @@ fn basic_2(client: &Client) -> Result<()> {
.request() .request()
.instruction(system_instruction::create_account( .instruction(system_instruction::create_account(
&authority, &authority,
&author.pubkey(), &counter.pubkey(),
program.rpc().get_minimum_balance_for_rent_exemption(500)?, program.rpc().get_minimum_balance_for_rent_exemption(500)?,
500, 500,
&program_id, &program_id,
)) ))
.signer(&author) .signer(&counter)
.accounts(CreateAuthor { .accounts(basic_2_accounts::Create {
author: author.pubkey(), counter: counter.pubkey(),
rent: sysvar::rent::ID, rent: sysvar::rent::ID,
}) })
.args(Basic2Instruction::CreateAuthor { .args(basic_2_instruction::Create { authority })
authority,
name: "My Book Name".to_string(),
})
.send()?; .send()?;
let author_account: Author = program.account(author.pubkey())?; let counter_account: Counter = program.account(counter.pubkey())?;
assert_eq!(author_account.authority, authority); assert_eq!(counter_account.authority, authority);
assert_eq!(author_account.name, "My Book Name".to_string()); assert_eq!(counter_account.count, 0);
println!("Success!"); println!("Success!");

View File

@ -4,7 +4,7 @@
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::solana_program::program_error::ProgramError; use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::pubkey::Pubkey; use anchor_lang::solana_program::pubkey::Pubkey;
use anchor_lang::{AccountDeserialize, AnchorSerialize, ToAccountMetas}; use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas};
use solana_client::client_error::ClientError as SolanaClientError; use solana_client::client_error::ClientError as SolanaClientError;
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::commitment_config::CommitmentConfig;
@ -185,9 +185,8 @@ impl<'a> RequestBuilder<'a> {
self self
} }
pub fn args(mut self, args: impl AnchorSerialize) -> Self { pub fn args(mut self, args: impl InstructionData) -> Self {
let data = args.try_to_vec().expect("Should always serialize"); self.instruction_data = Some(args.data());
self.instruction_data = Some(data);
self self
} }

View File

@ -7,7 +7,7 @@ describe("multisig", () => {
const program = anchor.workspace.Multisig; const program = anchor.workspace.Multisig;
it("Is initialized!", async () => { it("Tests the multisig program", async () => {
const multisig = new anchor.web3.Account(); const multisig = new anchor.web3.Account();
const [ const [
multisigSigner, multisigSigner,
@ -58,10 +58,8 @@ describe("multisig", () => {
}, },
]; ];
const newOwners = [ownerA.publicKey, ownerB.publicKey]; const newOwners = [ownerA.publicKey, ownerB.publicKey];
const data = program.coder.instruction.encode({ const data = program.coder.instruction.encode('set_owners', {
setOwners: {
owners: newOwners, owners: newOwners,
},
}); });
const transaction = new anchor.web3.Account(); const transaction = new anchor.web3.Account();

View File

@ -159,6 +159,14 @@ pub trait AccountDeserialize: Sized {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>; fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>;
} }
/// Calculates the data for an instruction invocation, where the data is
/// `Sha256(<namespace>::<method_name>)[..8] || BorshSerialize(args)`.
/// `args` is a borsh serialized struct of named fields for each argument given
/// to an instruction.
pub trait InstructionData: AnchorSerialize {
fn data(&self) -> Vec<u8>;
}
/// The prelude contains all commonly used components of the crate. /// The prelude contains all commonly used components of the crate.
/// All programs should include it via `anchor_lang::prelude::*;`. /// All programs should include it via `anchor_lang::prelude::*;`.
pub mod prelude { pub mod prelude {

View File

@ -3,13 +3,19 @@ use crate::{Program, RpcArg, State};
use heck::{CamelCase, SnakeCase}; use heck::{CamelCase, SnakeCase};
use quote::quote; use quote::quote;
// Namespace for calculating state instruction sighash signatures.
const SIGHASH_STATE_NAMESPACE: &'static str = "state";
// Namespace for calculating instruction sighash signatures for any instruction
// not affecting program state.
const SIGHASH_GLOBAL_NAMESPACE: &'static str = "global";
pub fn generate(program: Program) -> proc_macro2::TokenStream { pub fn generate(program: Program) -> proc_macro2::TokenStream {
let mod_name = &program.name; let mod_name = &program.name;
let instruction_name = instruction_enum_name(&program);
let dispatch = generate_dispatch(&program); let dispatch = generate_dispatch(&program);
let handlers_non_inlined = generate_non_inlined_handlers(&program); let handlers_non_inlined = generate_non_inlined_handlers(&program);
let methods = generate_methods(&program); let methods = generate_methods(&program);
let instruction = generate_instruction(&program); let instructions = generate_instructions(&program);
let cpi = generate_cpi(&program); let cpi = generate_cpi(&program);
let accounts = generate_accounts(&program); let accounts = generate_accounts(&program);
@ -17,21 +23,27 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
// TODO: remove once we allow segmented paths in `Accounts` structs. // TODO: remove once we allow segmented paths in `Accounts` structs.
use #mod_name::*; use #mod_name::*;
#[cfg(not(feature = "no-entrypoint"))] #[cfg(not(feature = "no-entrypoint"))]
anchor_lang::solana_program::entrypoint!(entry); anchor_lang::solana_program::entrypoint!(entry);
#[cfg(not(feature = "no-entrypoint"))] #[cfg(not(feature = "no-entrypoint"))]
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
if instruction_data.len() < 8 {
return Err(ProgramError::Custom(99));
}
let mut instruction_data: &[u8] = instruction_data;
let sighash: [u8; 8] = {
let mut sighash: [u8; 8] = [0; 8];
sighash.copy_from_slice(&instruction_data[..8]);
instruction_data = &instruction_data[8..];
sighash
};
if cfg!(not(feature = "no-idl")) { if cfg!(not(feature = "no-idl")) {
if instruction_data.len() >= 8 { if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] { return __private::__idl(program_id, accounts, &instruction_data[8..]);
return __private::__idl(program_id, accounts, &instruction_data[8..]);
}
} }
} }
let mut data: &[u8] = instruction_data;
let ix = instruction::#instruction_name::deserialize(&mut data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
#dispatch #dispatch
} }
@ -45,7 +57,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
#accounts #accounts
#instruction #instructions
#methods #methods
@ -57,10 +69,19 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
let ctor_state_dispatch_arm = match &program.state { let ctor_state_dispatch_arm = match &program.state {
None => quote! { /* no-op */ }, None => quote! { /* no-op */ },
Some(state) => { Some(state) => {
let variant_arm = generate_ctor_variant(program, state); let variant_arm = generate_ctor_variant(state);
let ctor_args = generate_ctor_args(state); let ctor_args = generate_ctor_args(state);
let ix_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap();
let sighash_arr = sighash_ctor();
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! { quote! {
instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*), #sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::__ctor(program_id, accounts, #(#ctor_args),*)
}
} }
} }
}; };
@ -72,18 +93,19 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.map(|rpc: &crate::StateRpc| { .map(|rpc: &crate::StateRpc| {
let rpc_arg_names: Vec<&syn::Ident> = let rpc_arg_names: Vec<&syn::Ident> =
rpc.args.iter().map(|arg| &arg.name).collect(); rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm: proc_macro2::TokenStream = generate_ix_variant( let name = &rpc.raw_method.sig.ident.to_string();
program, let rpc_name: proc_macro2::TokenStream = { format!("__{}", name).parse().unwrap() };
rpc.raw_method.sig.ident.to_string(), let variant_arm =
&rpc.args, generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, true);
true, let ix_name = generate_ix_variant_name(rpc.raw_method.sig.ident.to_string(), true);
); let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name);
let rpc_name: proc_macro2::TokenStream = { let sighash_tts: proc_macro2::TokenStream =
let name = &rpc.raw_method.sig.ident.to_string(); format!("{:?}", sighash_arr).parse().unwrap();
format!("__{}", name).parse().unwrap()
};
quote! { quote! {
instruction::#variant_arm => { #sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*) __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
} }
} }
@ -95,15 +117,18 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.iter() .iter()
.map(|rpc| { .map(|rpc| {
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect(); let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let rpc_name = &rpc.raw_method.sig.ident; let rpc_name = &rpc.raw_method.sig.ident;
let ix_name = generate_ix_variant_name(rpc.raw_method.sig.ident.to_string(), false);
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &rpc_name.to_string());
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
let variant_arm =
generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, false);
quote! { quote! {
instruction::#variant_arm => { #sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*) __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
} }
} }
@ -111,10 +136,14 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.collect(); .collect();
quote! { quote! {
match ix { match sighash {
#ctor_state_dispatch_arm #ctor_state_dispatch_arm
#(#state_dispatch_arms),* #(#state_dispatch_arms)*
#(#dispatch_arms),* #(#dispatch_arms)*
_ => {
msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
Err(ProgramError::Custom(99))
}
} }
} }
} }
@ -426,36 +455,42 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
} }
} }
pub fn generate_ctor_variant(program: &Program, state: &State) -> proc_macro2::TokenStream { pub fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let ctor_args = generate_ctor_args(state); let ctor_args = generate_ctor_args(state);
let ctor_variant_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap();
if ctor_args.len() == 0 { if ctor_args.len() == 0 {
quote! { quote! {
#enum_name::__Ctor #ctor_variant_name
} }
} else { } else {
quote! { quote! {
#enum_name::__Ctor { #ctor_variant_name {
#(#ctor_args),* #(#ctor_args),*
} }
} }
} }
} }
pub fn generate_ctor_typed_variant_with_comma(program: &Program) -> proc_macro2::TokenStream { pub fn generate_ctor_variant_name() -> String {
"__Ctor".to_string()
}
pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::TokenStream {
match &program.state { match &program.state {
None => quote! {}, None => quote! {},
Some(state) => { Some(state) => {
let ctor_args = generate_ctor_typed_args(state); let ctor_args = generate_ctor_typed_args(state);
if ctor_args.len() == 0 { if ctor_args.len() == 0 {
quote! { quote! {
__Ctor, #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct __Ctor;
} }
} else { } else {
quote! { quote! {
__Ctor { #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct __Ctor {
#(#ctor_args),* #(#ctor_args),*
}, };
} }
} }
} }
@ -503,12 +538,10 @@ fn generate_ctor_args(state: &State) -> Vec<Box<syn::Pat>> {
} }
pub fn generate_ix_variant( pub fn generate_ix_variant(
program: &Program,
name: String, name: String,
args: &[RpcArg], args: &[RpcArg],
underscore: bool, underscore: bool,
) -> proc_macro2::TokenStream { ) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let rpc_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect(); let rpc_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect();
let rpc_name_camel: proc_macro2::TokenStream = { let rpc_name_camel: proc_macro2::TokenStream = {
let n = name.to_camel_case(); let n = name.to_camel_case();
@ -521,17 +554,26 @@ pub fn generate_ix_variant(
if args.len() == 0 { if args.len() == 0 {
quote! { quote! {
#enum_name::#rpc_name_camel #rpc_name_camel
} }
} else { } else {
quote! { quote! {
#enum_name::#rpc_name_camel { #rpc_name_camel {
#(#rpc_arg_names),* #(#rpc_arg_names),*
} }
} }
} }
} }
pub fn generate_ix_variant_name(name: String, underscore: bool) -> proc_macro2::TokenStream {
let n = name.to_camel_case();
if underscore {
format!("__{}", n).parse().unwrap()
} else {
n.parse().unwrap()
}
}
pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream { pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
let program_mod = &program.program_mod; let program_mod = &program.program_mod;
quote! { quote! {
@ -539,9 +581,8 @@ pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
} }
} }
pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream { pub fn generate_instructions(program: &Program) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program); let ctor_variant = generate_ctor_typed_variant_with_semi(program);
let ctor_variant = generate_ctor_typed_variant_with_comma(program);
let state_method_variants: Vec<proc_macro2::TokenStream> = match &program.state { let state_method_variants: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![], None => vec![],
Some(state) => state Some(state) => state
@ -555,18 +596,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
); );
name.parse().unwrap() name.parse().unwrap()
}; };
let raw_args: Vec<&syn::PatType> = let raw_args: Vec<proc_macro2::TokenStream> = method
method.args.iter().map(|arg| &arg.raw_arg).collect(); .args
.iter()
.map(|arg| {
format!("pub {}", parser::tts_to_string(&arg.raw_arg))
.parse()
.unwrap()
})
.collect();
let ix_data_trait = {
let name = method.raw_method.sig.ident.to_string();
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
impl anchor_lang::InstructionData for #rpc_name_camel {
fn data(&self) -> Vec<u8> {
let mut d = #sighash_tts.to_vec();
d.append(&mut self.try_to_vec().expect("Should always serialize"));
d
}
}
}
};
// If no args, output a "unit" variant instead of a struct variant. // If no args, output a "unit" variant instead of a struct variant.
if method.args.len() == 0 { if method.args.len() == 0 {
quote! { quote! {
#rpc_name_camel, #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel;
#ix_data_trait
} }
} else { } else {
quote! { quote! {
#rpc_name_camel { #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel {
#(#raw_args),* #(#raw_args),*
}, }
#ix_data_trait
} }
} }
}) })
@ -576,21 +647,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
.rpcs .rpcs
.iter() .iter()
.map(|rpc| { .map(|rpc| {
let rpc_name_camel = proc_macro2::Ident::new( let name = &rpc.raw_method.sig.ident.to_string();
&rpc.raw_method.sig.ident.to_string().to_camel_case(), let rpc_name_camel =
rpc.raw_method.sig.ident.span(), proc_macro2::Ident::new(&name.to_camel_case(), rpc.raw_method.sig.ident.span());
); let raw_args: Vec<proc_macro2::TokenStream> = rpc
let raw_args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect(); .args
.iter()
.map(|arg| {
format!("pub {}", parser::tts_to_string(&arg.raw_arg))
.parse()
.unwrap()
})
.collect();
let ix_data_trait = {
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
impl anchor_lang::InstructionData for #rpc_name_camel {
fn data(&self) -> Vec<u8> {
let mut d = #sighash_tts.to_vec();
d.append(&mut self.try_to_vec().expect("Should always serialize"));
d
}
}
}
};
// If no args, output a "unit" variant instead of a struct variant. // If no args, output a "unit" variant instead of a struct variant.
if rpc.args.len() == 0 { if rpc.args.len() == 0 {
quote! { quote! {
#rpc_name_camel #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel;
#ix_data_trait
} }
} else { } else {
quote! { quote! {
#rpc_name_camel { #[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel {
#(#raw_args),* #(#raw_args),*
} }
#ix_data_trait
} }
} }
}) })
@ -603,23 +701,14 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
/// specifying instructions on a client. /// specifying instructions on a client.
pub mod instruction { pub mod instruction {
use super::*; use super::*;
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum #enum_name { #ctor_variant
#ctor_variant #(#state_method_variants)*
#(#state_method_variants)* #(#variants)*
#(#variants),*
}
} }
} }
} }
fn instruction_enum_name(program: &Program) -> proc_macro2::Ident {
proc_macro2::Ident::new(
&format!("{}Instruction", program.name.to_string().to_camel_case()),
program.name.span(),
)
}
fn generate_accounts(program: &Program) -> proc_macro2::TokenStream { fn generate_accounts(program: &Program) -> proc_macro2::TokenStream {
let mut accounts = std::collections::HashSet::new(); let mut accounts = std::collections::HashSet::new();
@ -678,14 +767,14 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
.map(|rpc| { .map(|rpc| {
let accounts_ident = &rpc.anchor_ident; let accounts_ident = &rpc.anchor_ident;
let cpi_method = { let cpi_method = {
let ix_variant = generate_ix_variant( let ix_variant =
program, generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, false);
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let method_name = &rpc.ident; let method_name = &rpc.ident;
let args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect(); let args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
let name = &rpc.raw_method.sig.ident.to_string();
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! { quote! {
pub fn #method_name<'a, 'b, 'c, 'info>( pub fn #method_name<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>, ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
@ -693,8 +782,10 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
) -> ProgramResult { ) -> ProgramResult {
let ix = { let ix = {
let ix = instruction::#ix_variant; let ix = instruction::#ix_variant;
let data = AnchorSerialize::try_to_vec(&ix) let mut ix_data = AnchorSerialize::try_to_vec(&ix)
.map_err(|_| ProgramError::InvalidInstructionData)?; .map_err(|_| ProgramError::InvalidInstructionData)?;
let mut data = #sighash_tts.to_vec();
data.append(&mut ix_data);
let accounts = ctx.accounts.to_account_metas(None); let accounts = ctx.accounts.to_account_metas(None);
anchor_lang::solana_program::instruction::Instruction { anchor_lang::solana_program::instruction::Instruction {
program_id: *ctx.program.key, program_id: *ctx.program.key,
@ -725,3 +816,24 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
} }
} }
} }
// We don't technically use sighash, because the input arguments aren't given.
// Rust doesn't have method overloading so no need to use the arguments.
// However, we do namespace methods in the preeimage so that we can use
// different traits with the same method name.
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
let preimage = format!("{}::{}", namespace, name);
let mut sighash = [0u8; 8];
sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]);
sighash
}
fn sighash_ctor() -> [u8; 8] {
let namespace = SIGHASH_STATE_NAMESPACE;
let preimage = format!("{}::new", namespace);
let mut sighash = [0u8; 8];
sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]);
sighash
}

View File

@ -11,6 +11,8 @@ use std::collections::HashMap;
pub mod codegen; pub mod codegen;
#[cfg(feature = "hash")] #[cfg(feature = "hash")]
pub mod hash; pub mod hash;
#[cfg(not(feature = "hash"))]
pub(crate) mod hash;
#[cfg(feature = "idl")] #[cfg(feature = "idl")]
pub mod idl; pub mod idl;
pub mod parser; pub mod parser;

View File

@ -32,10 +32,11 @@
"bs58": "^4.0.1", "bs58": "^4.0.1",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"camelcase": "^5.3.1", "camelcase": "^5.3.1",
"crypto-hash": "^1.3.0",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"find": "^0.3.0", "find": "^0.3.0",
"pako": "^2.0.3" "js-sha256": "^0.9.0",
"pako": "^2.0.3",
"snake-case": "^3.0.4"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^11.0.0", "@commitlint/cli": "^11.0.0",

View File

@ -1,6 +1,7 @@
import camelCase from "camelcase"; import camelCase from "camelcase";
import { snakeCase } from "snake-case";
import { Layout } from "buffer-layout"; import { Layout } from "buffer-layout";
import { sha256 } from "crypto-hash"; import * as sha256 from "js-sha256";
import * as borsh from "@project-serum/borsh"; import * as borsh from "@project-serum/borsh";
import { import {
Idl, Idl,
@ -16,6 +17,15 @@ import { IdlError } from "./error";
* Number of bytes of the account discriminator. * Number of bytes of the account discriminator.
*/ */
export const ACCOUNT_DISCRIMINATOR_SIZE = 8; export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
/**
* Namespace for state method function signatures.
*/
export const SIGHASH_STATE_NAMESPACE = "state";
/**
* Namespace for global instruction function signatures (i.e. functions
* that aren't namespaced by the state or any of its trait implementations).
*/
export const SIGHASH_GLOBAL_NAMESPACE = "global";
/** /**
* Coder provides a facade for encoding and decoding all IDL related objects. * Coder provides a facade for encoding and decoding all IDL related objects.
@ -54,35 +64,48 @@ export default class Coder {
/** /**
* Encodes and decodes program instructions. * Encodes and decodes program instructions.
*/ */
class InstructionCoder<T = any> { class InstructionCoder {
/** /**
* Instruction enum layout. * Instruction args layout. Maps namespaced method
*/ */
private ixLayout: Layout; private ixLayout: Map<string, Layout>;
public constructor(idl: Idl) { public constructor(idl: Idl) {
this.ixLayout = InstructionCoder.parseIxLayout(idl); this.ixLayout = InstructionCoder.parseIxLayout(idl);
} }
public encode(ix: T): Buffer { /**
* Encodes a program instruction.
*/
public encode(ixName: string, ix: any) {
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
}
/**
* Encodes a program state instruction.
*/
public encodeState(ixName: string, ix: any) {
return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
}
public _encode(nameSpace: string, ixName: string, ix: any): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer. const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const len = this.ixLayout.encode(ix, buffer); const methodName = camelCase(ixName);
return buffer.slice(0, len); const len = this.ixLayout.get(methodName).encode(ix, buffer);
const data = buffer.slice(0, len);
return Buffer.concat([sighash(nameSpace, ixName), data]);
} }
public decode(ix: Buffer): T { private static parseIxLayout(idl: Idl): Map<string, Layout> {
return this.ixLayout.decode(ix); const stateMethods = idl.state ? idl.state.methods : [];
}
private static parseIxLayout(idl: Idl): Layout { const ixLayouts = stateMethods
let stateMethods = idl.state ? idl.state.methods : [];
let ixLayouts = stateMethods
.map((m: IdlStateMethod) => { .map((m: IdlStateMethod) => {
let fieldLayouts = m.args.map((arg: IdlField) => let fieldLayouts = m.args.map((arg: IdlField) => {
IdlCoder.fieldLayout(arg, idl.types) return IdlCoder.fieldLayout(arg, idl.types);
); });
const name = camelCase(m.name); const name = camelCase(m.name);
return borsh.struct(fieldLayouts, name); return [name, borsh.struct(fieldLayouts, name)];
}) })
.concat( .concat(
idl.instructions.map((ix) => { idl.instructions.map((ix) => {
@ -90,10 +113,11 @@ class InstructionCoder<T = any> {
IdlCoder.fieldLayout(arg, idl.types) IdlCoder.fieldLayout(arg, idl.types)
); );
const name = camelCase(ix.name); const name = camelCase(ix.name);
return borsh.struct(fieldLayouts, name); return [name, borsh.struct(fieldLayouts, name)];
}) })
); );
return borsh.rustEnum(ixLayouts); // @ts-ignore
return new Map(ixLayouts);
} }
} }
@ -320,24 +344,14 @@ class IdlCoder {
// Calculates unique 8 byte discriminator prepended to all anchor accounts. // Calculates unique 8 byte discriminator prepended to all anchor accounts.
export async function accountDiscriminator(name: string): Promise<Buffer> { export async function accountDiscriminator(name: string): Promise<Buffer> {
return Buffer.from( // @ts-ignore
( return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
await sha256(`account:${name}`, {
outputFormat: "buffer",
})
).slice(0, 8)
);
} }
// Calculates unique 8 byte discriminator prepended to all anchor state accounts. // Calculates unique 8 byte discriminator prepended to all anchor state accounts.
export async function stateDiscriminator(name: string): Promise<Buffer> { export async function stateDiscriminator(name: string): Promise<Buffer> {
return Buffer.from( // @ts-ignore
( return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
await sha256(`account:${name}`, {
outputFormat: "buffer",
})
).slice(0, 8)
);
} }
// Returns the size of the type in bytes. For variable length types, just return // Returns the size of the type in bytes. For variable length types, just return
@ -424,3 +438,12 @@ export function accountSize(
.map((f) => typeSize(idl, f.type)) .map((f) => typeSize(idl, f.type))
.reduce((a, b) => a + b); .reduce((a, b) => a + b);
} }
// Not technically sighash, since we don't include the arguments, as Rust
// doesn't allow function overloading.
function sighash(nameSpace: string, ixName: string): Buffer {
let name = snakeCase(ixName);
let preimage = `${nameSpace}::${name}`;
// @ts-ignore
return Buffer.from(sha256.digest(preimage)).slice(0, 8);
}

View File

@ -24,6 +24,8 @@ import {
import { IdlError, ProgramError } from "./error"; import { IdlError, ProgramError } from "./error";
import Coder, { import Coder, {
ACCOUNT_DISCRIMINATOR_SIZE, ACCOUNT_DISCRIMINATOR_SIZE,
SIGHASH_STATE_NAMESPACE,
SIGHASH_GLOBAL_NAMESPACE,
accountDiscriminator, accountDiscriminator,
stateDiscriminator, stateDiscriminator,
accountSize, accountSize,
@ -229,7 +231,10 @@ export class RpcFactory {
RpcFactory.accountsArray(ctx.accounts, m.accounts) RpcFactory.accountsArray(ctx.accounts, m.accounts)
), ),
programId, programId,
data: coder.instruction.encode(toInstruction(m, ...ixArgs)), data: coder.instruction.encodeState(
m.name,
toInstruction(m, ...ixArgs)
),
}) })
); );
try { try {
@ -316,12 +321,15 @@ export class RpcFactory {
} }
if (ctx.__private && ctx.__private.logAccounts) { if (ctx.__private && ctx.__private.logAccounts) {
console.log("Outoing account metas:", keys); console.log("Outgoing account metas:", keys);
} }
return new TransactionInstruction({ return new TransactionInstruction({
keys, keys,
programId, programId,
data: coder.instruction.encode(toInstruction(idlIx, ...ixArgs)), data: coder.instruction.encode(
idlIx.name,
toInstruction(idlIx, ...ixArgs)
),
}); });
}; };
@ -609,12 +617,7 @@ function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
idx += 1; idx += 1;
}); });
// JavaScript representation of the rust enum variant. return ix;
const name = camelCase(idlIx.name);
const ixVariant: { [key: string]: any } = {};
ixVariant[name] = ix;
return ixVariant;
} }
// Throws error if any account required for the `ix` is not given. // Throws error if any account required for the `ix` is not given.

View File

@ -1687,7 +1687,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
crypto-hash@^1.2.2, crypto-hash@^1.3.0: crypto-hash@^1.2.2:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
@ -1923,6 +1923,14 @@ domutils@^1.5.1:
dom-serializer "0" dom-serializer "0"
domelementtype "1" domelementtype "1"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
dot-prop@^5.1.0: dot-prop@^5.1.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@ -3552,6 +3560,11 @@ jest@26.6.0:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^26.6.0" jest-cli "^26.6.0"
js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
js-tokens@^4.0.0: js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -3923,6 +3936,13 @@ log-update@^4.0.0:
slice-ansi "^4.0.0" slice-ansi "^4.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
dependencies:
tslib "^2.0.3"
lru-cache@^6.0.0: lru-cache@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -4142,6 +4162,14 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
dependencies:
lower-case "^2.0.2"
tslib "^2.0.3"
node-addon-api@^2.0.0: node-addon-api@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
@ -5006,6 +5034,14 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0" astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
snapdragon-node@^2.0.1: snapdragon-node@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -5502,6 +5538,11 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tsutils@^3.17.1: tsutils@^3.17.1:
version "3.17.1" version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"