Browse Source

Migrate to sighash based method dispatch (#64)

Armani Ferrante 4 years ago
parent
commit
48b27e6943

+ 2 - 0
CHANGELOG.md

@@ -11,6 +11,8 @@ incremented for features.
 
 ## [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
 
 Initial release.

+ 3 - 0
Cargo.lock

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

+ 18 - 21
client/example/src/main.rs

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

+ 3 - 4
client/src/lib.rs

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

+ 2 - 4
examples/multisig/tests/multisig.js

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

+ 8 - 0
lang/src/lib.rs

@@ -159,6 +159,14 @@ pub trait AccountDeserialize: Sized {
     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.
 /// All programs should include it via `anchor_lang::prelude::*;`.
 pub mod prelude {

+ 193 - 81
lang/syn/src/codegen/program.rs

@@ -3,13 +3,19 @@ use crate::{Program, RpcArg, State};
 use heck::{CamelCase, SnakeCase};
 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 {
     let mod_name = &program.name;
-    let instruction_name = instruction_enum_name(&program);
     let dispatch = generate_dispatch(&program);
     let handlers_non_inlined = generate_non_inlined_handlers(&program);
     let methods = generate_methods(&program);
-    let instruction = generate_instruction(&program);
+    let instructions = generate_instructions(&program);
     let cpi = generate_cpi(&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.
         use #mod_name::*;
 
-
         #[cfg(not(feature = "no-entrypoint"))]
         anchor_lang::solana_program::entrypoint!(entry);
         #[cfg(not(feature = "no-entrypoint"))]
         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 instruction_data.len() >= 8 {
-                    if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] {
-                        return __private::__idl(program_id, accounts, &instruction_data[8..]);
-                    }
+                if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
+                    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
         }
@@ -45,7 +57,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
 
         #accounts
 
-        #instruction
+        #instructions
 
         #methods
 
@@ -57,10 +69,19 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
     let ctor_state_dispatch_arm = match &program.state {
         None => quote! { /* no-op */ },
         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 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! {
-                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| {
                 let rpc_arg_names: Vec<&syn::Ident> =
                     rpc.args.iter().map(|arg| &arg.name).collect();
-                let variant_arm: proc_macro2::TokenStream = generate_ix_variant(
-                    program,
-                    rpc.raw_method.sig.ident.to_string(),
-                    &rpc.args,
-                    true,
-                );
-                let rpc_name: proc_macro2::TokenStream = {
-                    let name = &rpc.raw_method.sig.ident.to_string();
-                    format!("__{}", name).parse().unwrap()
-                };
+                let name = &rpc.raw_method.sig.ident.to_string();
+                let rpc_name: proc_macro2::TokenStream = { format!("__{}", name).parse().unwrap() };
+                let variant_arm =
+                    generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, 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 sighash_tts: proc_macro2::TokenStream =
+                    format!("{:?}", sighash_arr).parse().unwrap();
                 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),*)
                     }
                 }
@@ -95,15 +117,18 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
         .iter()
         .map(|rpc| {
             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 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! {
-                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),*)
                 }
             }
@@ -111,10 +136,14 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
         .collect();
 
     quote! {
-        match ix {
+        match sighash {
             #ctor_state_dispatch_arm
-            #(#state_dispatch_arms),*
-            #(#dispatch_arms),*
+            #(#state_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 {
-    let enum_name = instruction_enum_name(program);
+pub fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream {
     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 {
         quote! {
-            #enum_name::__Ctor
+            #ctor_variant_name
         }
     } else {
         quote! {
-            #enum_name::__Ctor {
+            #ctor_variant_name {
                 #(#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 {
         None => quote! {},
         Some(state) => {
             let ctor_args = generate_ctor_typed_args(state);
             if ctor_args.len() == 0 {
                 quote! {
-                    __Ctor,
+                    #[derive(AnchorSerialize, AnchorDeserialize)]
+                    pub struct __Ctor;
                 }
             } else {
                 quote! {
-                    __Ctor {
+                    #[derive(AnchorSerialize, AnchorDeserialize)]
+                    pub struct __Ctor {
                         #(#ctor_args),*
-                    },
+                    };
                 }
             }
         }
@@ -503,12 +538,10 @@ fn generate_ctor_args(state: &State) -> Vec<Box<syn::Pat>> {
 }
 
 pub fn generate_ix_variant(
-    program: &Program,
     name: String,
     args: &[RpcArg],
     underscore: bool,
 ) -> 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_name_camel: proc_macro2::TokenStream = {
         let n = name.to_camel_case();
@@ -521,17 +554,26 @@ pub fn generate_ix_variant(
 
     if args.len() == 0 {
         quote! {
-            #enum_name::#rpc_name_camel
+            #rpc_name_camel
         }
     } else {
         quote! {
-            #enum_name::#rpc_name_camel {
+            #rpc_name_camel {
                 #(#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 {
     let program_mod = &program.program_mod;
     quote! {
@@ -539,9 +581,8 @@ pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
     }
 }
 
-pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
-    let enum_name = instruction_enum_name(program);
-    let ctor_variant = generate_ctor_typed_variant_with_comma(program);
+pub fn generate_instructions(program: &Program) -> proc_macro2::TokenStream {
+    let ctor_variant = generate_ctor_typed_variant_with_semi(program);
     let state_method_variants: Vec<proc_macro2::TokenStream> = match &program.state {
         None => vec![],
         Some(state) => state
@@ -555,18 +596,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
                     );
                     name.parse().unwrap()
                 };
-                let raw_args: Vec<&syn::PatType> =
-                    method.args.iter().map(|arg| &arg.raw_arg).collect();
+                let raw_args: Vec<proc_macro2::TokenStream> = method
+                    .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 method.args.len() == 0 {
                     quote! {
-                        #rpc_name_camel,
+                        #[derive(AnchorSerialize, AnchorDeserialize)]
+                        pub struct #rpc_name_camel;
+
+                        #ix_data_trait
                     }
                 } else {
                     quote! {
-                        #rpc_name_camel {
+                        #[derive(AnchorSerialize, AnchorDeserialize)]
+                        pub struct #rpc_name_camel {
                             #(#raw_args),*
-                        },
+                        }
+
+                        #ix_data_trait
                     }
                 }
             })
@@ -576,21 +647,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
         .rpcs
         .iter()
         .map(|rpc| {
-            let rpc_name_camel = proc_macro2::Ident::new(
-                &rpc.raw_method.sig.ident.to_string().to_camel_case(),
-                rpc.raw_method.sig.ident.span(),
-            );
-            let raw_args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
+            let name = &rpc.raw_method.sig.ident.to_string();
+            let rpc_name_camel =
+                proc_macro2::Ident::new(&name.to_camel_case(), rpc.raw_method.sig.ident.span());
+            let raw_args: Vec<proc_macro2::TokenStream> = rpc
+                .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 rpc.args.len() == 0 {
                 quote! {
-                    #rpc_name_camel
+                    #[derive(AnchorSerialize, AnchorDeserialize)]
+                    pub struct #rpc_name_camel;
+
+                    #ix_data_trait
                 }
             } else {
                 quote! {
-                    #rpc_name_camel {
+                    #[derive(AnchorSerialize, AnchorDeserialize)]
+                    pub struct #rpc_name_camel {
                         #(#raw_args),*
                     }
+
+                    #ix_data_trait
                 }
             }
         })
@@ -603,23 +701,14 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
         /// specifying instructions on a client.
         pub mod instruction {
             use super::*;
-            #[derive(AnchorSerialize, AnchorDeserialize)]
-            pub enum #enum_name {
-                #ctor_variant
-                #(#state_method_variants)*
-                #(#variants),*
-            }
+
+            #ctor_variant
+            #(#state_method_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 {
     let mut accounts = std::collections::HashSet::new();
 
@@ -678,14 +767,14 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
         .map(|rpc| {
             let accounts_ident = &rpc.anchor_ident;
             let cpi_method = {
-                let ix_variant = generate_ix_variant(
-                    program,
-                    rpc.raw_method.sig.ident.to_string(),
-                    &rpc.args,
-                    false,
-                );
+                let ix_variant =
+                    generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, false);
                 let method_name = &rpc.ident;
                 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! {
                     pub fn #method_name<'a, 'b, 'c, 'info>(
                         ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
@@ -693,8 +782,10 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
                     ) -> ProgramResult {
                         let ix = {
                             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)?;
+                            let mut data = #sighash_tts.to_vec();
+                            data.append(&mut ix_data);
                             let accounts = ctx.accounts.to_account_metas(None);
                             anchor_lang::solana_program::instruction::Instruction {
                                 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
+}

+ 2 - 0
lang/syn/src/lib.rs

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

+ 3 - 2
ts/package.json

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

+ 56 - 33
ts/src/coder.ts

@@ -1,6 +1,7 @@
 import camelCase from "camelcase";
+import { snakeCase } from "snake-case";
 import { Layout } from "buffer-layout";
-import { sha256 } from "crypto-hash";
+import * as sha256 from "js-sha256";
 import * as borsh from "@project-serum/borsh";
 import {
   Idl,
@@ -16,6 +17,15 @@ import { IdlError } from "./error";
  * Number of bytes of the account discriminator.
  */
 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.
@@ -54,35 +64,48 @@ export default class Coder {
 /**
  * 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) {
     this.ixLayout = InstructionCoder.parseIxLayout(idl);
   }
 
-  public encode(ix: T): Buffer {
-    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
-    const len = this.ixLayout.encode(ix, buffer);
-    return buffer.slice(0, len);
+  /**
+   * 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 decode(ix: Buffer): T {
-    return this.ixLayout.decode(ix);
+  public _encode(nameSpace: string, ixName: string, ix: any): Buffer {
+    const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+    const methodName = camelCase(ixName);
+    const len = this.ixLayout.get(methodName).encode(ix, buffer);
+    const data = buffer.slice(0, len);
+    return Buffer.concat([sighash(nameSpace, ixName), data]);
   }
 
-  private static parseIxLayout(idl: Idl): Layout {
-    let stateMethods = idl.state ? idl.state.methods : [];
-    let ixLayouts = stateMethods
+  private static parseIxLayout(idl: Idl): Map<string, Layout> {
+    const stateMethods = idl.state ? idl.state.methods : [];
+
+    const ixLayouts = stateMethods
       .map((m: IdlStateMethod) => {
-        let fieldLayouts = m.args.map((arg: IdlField) =>
-          IdlCoder.fieldLayout(arg, idl.types)
-        );
+        let fieldLayouts = m.args.map((arg: IdlField) => {
+          return IdlCoder.fieldLayout(arg, idl.types);
+        });
         const name = camelCase(m.name);
-        return borsh.struct(fieldLayouts, name);
+        return [name, borsh.struct(fieldLayouts, name)];
       })
       .concat(
         idl.instructions.map((ix) => {
@@ -90,10 +113,11 @@ class InstructionCoder<T = any> {
             IdlCoder.fieldLayout(arg, idl.types)
           );
           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.
 export async function accountDiscriminator(name: string): Promise<Buffer> {
-  return Buffer.from(
-    (
-      await sha256(`account:${name}`, {
-        outputFormat: "buffer",
-      })
-    ).slice(0, 8)
-  );
+  // @ts-ignore
+  return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
 }
 
 // Calculates unique 8 byte discriminator prepended to all anchor state accounts.
 export async function stateDiscriminator(name: string): Promise<Buffer> {
-  return Buffer.from(
-    (
-      await sha256(`account:${name}`, {
-        outputFormat: "buffer",
-      })
-    ).slice(0, 8)
-  );
+  // @ts-ignore
+  return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
 }
 
 // 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))
     .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);
+}

+ 12 - 9
ts/src/rpc.ts

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

+ 42 - 1
ts/yarn.lock

@@ -1687,7 +1687,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-crypto-hash@^1.2.2, crypto-hash@^1.3.0:
+crypto-hash@^1.2.2:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
   integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
@@ -1923,6 +1923,14 @@ domutils@^1.5.1:
     dom-serializer "0"
     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:
   version "5.3.0"
   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"
     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:
   version "4.0.0"
   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"
     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:
   version "6.0.0"
   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"
   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:
   version "2.0.2"
   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"
     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:
   version "2.1.1"
   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"
   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:
   version "3.17.1"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"