From 6b86f85916e777b3bcb5d59dd1075026f0f660a9 Mon Sep 17 00:00:00 2001 From: TristanDebrunner Date: Mon, 15 Jul 2019 13:17:17 -0600 Subject: [PATCH] Add C API (#5072) --- Cargo.lock | 46 +++- Cargo.toml | 1 + sdk-c/.gitignore | 14 + sdk-c/Cargo.toml | 25 ++ sdk-c/README.md | 12 + sdk-c/build.rs | 24 ++ sdk-c/cbindgen.toml | 29 +++ sdk-c/src/lib.rs | 574 +++++++++++++++++++++++++++++++++++++++++ sdk/Cargo.toml | 2 +- sdk/src/pubkey.rs | 12 +- sdk/src/signature.rs | 7 + sdk/src/transaction.rs | 49 +++- 12 files changed, 777 insertions(+), 18 deletions(-) create mode 100644 sdk-c/.gitignore create mode 100644 sdk-c/Cargo.toml create mode 100644 sdk-c/README.md create mode 100644 sdk-c/build.rs create mode 100644 sdk-c/cbindgen.toml create mode 100644 sdk-c/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4a20fca694..490857af62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,22 @@ dependencies = [ "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cbindgen" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.29 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cc" version = "1.0.37" @@ -855,7 +871,7 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2795,7 +2811,7 @@ dependencies = [ "bs58 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2813,6 +2829,20 @@ dependencies = [ "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-sdk-c" +version = "0.17.0" +dependencies = [ + "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "bs58 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "cbindgen 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-ed25519-dalek 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-sdk 0.17.0", +] + [[package]] name = "solana-stake-api" version = "0.17.0" @@ -3375,6 +3405,14 @@ dependencies = [ "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "toml" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "traitobject" version = "0.1.0" @@ -3674,6 +3712,7 @@ dependencies = [ "checksum bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" "checksum bzip2-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6584aa36f5ad4c9247f5323b0a42f37802b37a836f0ad87084d7a33961abe25f" "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" +"checksum cbindgen 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0e7e19db9a3892c88c74cbbdcd218196068a928f1b60e736c448b13a1e81f277" "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" "checksum cexpr 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "644d693ecfa91955ed32dcc7eda4914e1be97a641fb6f0645a37348e20b230da" "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" @@ -3733,7 +3772,7 @@ dependencies = [ "checksum futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "62941eff9507c8177d448bd83a44d9b9760856e184081d8cd79ba9f03dd24981" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" "checksum generic-array 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cba710fd46ea0501f73f0dac107a3f97b5ea3fe0546e79e45e074f58459afa82" -"checksum generic-array 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1d9582501ecd567475c637337f51232d8adac81f31e4bd241edd40ad25a12bdf" +"checksum generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" "checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" @@ -3924,6 +3963,7 @@ dependencies = [ "checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92" "checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" "checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +"checksum toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b8c96d7873fa7ef8bdeb3a9cda3ac48389b4154f32b9803b4bc26220b677b039" "checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" "checksum treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" diff --git a/Cargo.toml b/Cargo.toml index 1c8fe5c49e..6762e43bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "bench-exchange", "bench-streamer", "bench-tps", + "sdk-c", "chacha-sys", "client", "core", diff --git a/sdk-c/.gitignore b/sdk-c/.gitignore new file mode 100644 index 0000000000..1971718e62 --- /dev/null +++ b/sdk-c/.gitignore @@ -0,0 +1,14 @@ +/include/ +/farf/ +/target/ + +**/*.rs.bk +.cargo + +# log files +*.log +log-*.txt + +# intellij files +/.idea/ +/solana.iml \ No newline at end of file diff --git a/sdk-c/Cargo.toml b/sdk-c/Cargo.toml new file mode 100644 index 0000000000..395ca6ba64 --- /dev/null +++ b/sdk-c/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "solana-sdk-c" +version = "0.17.0" +description = "Solana SDK C" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +license = "Apache-2.0" +edition = "2018" + +[lib] +name = "solana_sdk_c" +crate-type = ["staticlib"] + +[dependencies] +bincode = "1.1.4" +bs58 = "0.2.0" +libc = "0.2.58" +rand_chacha = "0.1.1" +rand_core = { version = ">=0.2, <0.4", default-features = false } +solana-sdk = { path = "../sdk", version = "0.17.0" } +solana-ed25519-dalek = "0.2.0" + +[build-dependencies] +cbindgen = "0.9.0" \ No newline at end of file diff --git a/sdk-c/README.md b/sdk-c/README.md new file mode 100644 index 0000000000..331fce8b6d --- /dev/null +++ b/sdk-c/README.md @@ -0,0 +1,12 @@ +# Solana SDK-C + +This crate exposes a C API to Solana functions written in Rust. The crate generates a static library, and uses `cbindgen` +to generate a header file during the build. To generate both: + +```shell +$ cd /sdk-c +$ cargo build +``` + +This will generate the static library in `/target/deps` and the header file in +`/sdk-c/include`. \ No newline at end of file diff --git a/sdk-c/build.rs b/sdk-c/build.rs new file mode 100644 index 0000000000..c262c210b1 --- /dev/null +++ b/sdk-c/build.rs @@ -0,0 +1,24 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let out_path = Path::new(&crate_dir); + let out_path = out_path.join(Path::new("include")); + + // Ensure `out_path` exists + fs::create_dir_all(&out_path).unwrap_or_else(|err| { + if err.kind() != std::io::ErrorKind::AlreadyExists { + panic!("Unable to create {:#?}: {:?}", out_path, err); + } + }); + + let out_path = out_path.join(Path::new("solana.h")); + let out_path = out_path.to_str().unwrap(); + + cbindgen::generate(crate_dir) + .unwrap() + .write_to_file(out_path); +} diff --git a/sdk-c/cbindgen.toml b/sdk-c/cbindgen.toml new file mode 100644 index 0000000000..b0c6a44615 --- /dev/null +++ b/sdk-c/cbindgen.toml @@ -0,0 +1,29 @@ +language = "C" +header = '''// Copyright 2018 Solana Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// Warning, this file is autogenerated by cbindgen. Don't modify this manually. +''' +include_version = true +include_guard = "SOLANA_H" +tab_width = 4 +documentation_style = "c99" + +[parse] +parse_deps = true +include = ["solana-sdk"] + +[export] +item_types = ["enums", "structs", "unions", "typedefs", "opaque", "functions"] \ No newline at end of file diff --git a/sdk-c/src/lib.rs b/sdk-c/src/lib.rs new file mode 100644 index 0000000000..32a3983f05 --- /dev/null +++ b/sdk-c/src/lib.rs @@ -0,0 +1,574 @@ +use bincode::{deserialize, serialize}; +use libc::{c_int, size_t}; +use rand_chacha::ChaChaRng; +use rand_core::SeedableRng; +use solana_ed25519_dalek::{SignatureError, KEYPAIR_LENGTH, PUBLIC_KEY_LENGTH}; +use solana_sdk::hash::Hash; +use solana_sdk::instruction::CompiledInstruction as CompiledInstructionNative; +use solana_sdk::message::Message as MessageNative; +use solana_sdk::message::MessageHeader as MessageHeaderNative; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::Signature as SignatureNative; +use solana_sdk::signature::{Keypair as KeypairNative, KeypairUtil}; +use solana_sdk::transaction::Transaction as TransactionNative; +use std::convert::TryInto; +use std::ffi::CString; +use std::os::raw::c_char; +use std::vec::Vec; +use std::{fmt, mem, ptr, slice}; + +#[repr(C)] +#[derive(Debug)] +pub struct Transaction { + /// A set of digital signatures of `account_keys`, `program_ids`, `recent_blockhash`, and `instructions`, signed by the first + /// signatures_len keys of account_keys + pub signatures: CVec, + + /// The message to sign. + pub message: Message, +} + +impl Transaction { + pub fn from_native(mut t: TransactionNative) -> Self { + t.signatures.shrink_to_fit(); + Self { + signatures: CVec::from_native( + t.signatures + .into_iter() + .map(Signature::from_native) + .collect(), + ), + message: Message::from_native(t.message), + } + } + + pub unsafe fn into_native(self) -> TransactionNative { + TransactionNative { + signatures: CVec::into_native(self.signatures) + .iter() + .map(|s| s.new_native()) + .collect(), + message: self.message.into_native(), + } + } + + #[allow(clippy::should_implement_trait)] + pub unsafe fn clone(&self) -> Self { + Self { + signatures: self.signatures.clone(), + message: self.message.clone(), + } + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct Message { + /// The message header, identifying signed and credit-only `account_keys` + pub header: MessageHeader, + + /// All the account keys used by this transaction + pub account_keys: CVec, + + /// The id of a recent ledger entry. + pub recent_blockhash: Hash, + + /// Programs that will be executed in sequence and committed in one atomic transaction if all + /// succeed. + pub instructions: CVec, +} + +impl Message { + pub fn from_native(m: MessageNative) -> Self { + Self { + header: MessageHeader::from_native(m.header), + account_keys: CVec::from_native(m.account_keys), + recent_blockhash: m.recent_blockhash, + instructions: CVec::from_native( + m.instructions + .into_iter() + .map(CompiledInstruction::from_native) + .collect(), + ), + } + } + + pub unsafe fn into_native(self) -> MessageNative { + MessageNative { + header: self.header.into_native(), + account_keys: CVec::into_native(self.account_keys), + recent_blockhash: self.recent_blockhash, + instructions: CVec::into_native(self.instructions) + .into_iter() + .map(|i| i.into_native()) + .collect(), + } + } + + #[allow(clippy::should_implement_trait)] + pub unsafe fn clone(&self) -> Self { + Self { + header: self.header.clone(), + account_keys: self.account_keys.clone(), + recent_blockhash: self.recent_blockhash, + instructions: self.instructions.clone(), + } + } +} + +/// An instruction to execute a program +#[repr(C)] +#[derive(Debug)] +pub struct CompiledInstruction { + /// Index into the transaction keys array indicating the program account that executes this instruction + pub program_id_index: u8, + + /// Ordered indices into the transaction keys array indicating which accounts to pass to the program + pub accounts: CVec, + + /// The program input data + pub data: CVec, +} + +impl CompiledInstruction { + pub fn from_native(c: CompiledInstructionNative) -> Self { + Self { + program_id_index: c.program_id_index, + accounts: CVec::from_native(c.accounts), + data: CVec::from_native(c.data), + } + } + + pub unsafe fn into_native(self) -> CompiledInstructionNative { + CompiledInstructionNative { + program_id_index: self.program_id_index, + accounts: CVec::into_native(self.accounts), + data: CVec::into_native(self.data), + } + } + + #[allow(clippy::should_implement_trait)] + pub unsafe fn clone(&self) -> Self { + Self { + program_id_index: self.program_id_index, + accounts: self.accounts.clone(), + data: self.data.clone(), + } + } +} + +#[repr(C)] +#[derive(Default, Debug, Clone)] +pub struct MessageHeader { + /// The number of signatures required for this message to be considered valid. The + /// signatures must match the first `num_required_signatures` of `account_keys`. + pub num_required_signatures: u8, + + /// The last num_credit_only_signed_accounts of the signed keys are credit-only accounts. + /// Programs may process multiple transactions that add lamports to the same credit-only + /// account within a single PoH entry, but are not permitted to debit lamports or modify + /// account data. Transactions targeting the same debit account are evaluated sequentially. + pub num_credit_only_signed_accounts: u8, + + /// The last num_credit_only_unsigned_accounts of the unsigned keys are credit-only accounts. + pub num_credit_only_unsigned_accounts: u8, +} + +impl MessageHeader { + pub fn from_native(h: MessageHeaderNative) -> Self { + Self { + num_required_signatures: h.num_required_signatures, + num_credit_only_signed_accounts: h.num_credit_only_signed_accounts, + num_credit_only_unsigned_accounts: h.num_credit_only_unsigned_accounts, + } + } + + pub fn into_native(self) -> MessageHeaderNative { + MessageHeaderNative { + num_required_signatures: self.num_required_signatures, + num_credit_only_signed_accounts: self.num_credit_only_signed_accounts, + num_credit_only_unsigned_accounts: self.num_credit_only_unsigned_accounts, + } + } +} + +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct Signature([u8; 64]); + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", bs58::encode(&self.0[..]).into_string()) + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", bs58::encode(&self.0[..]).into_string()) + } +} + +impl Signature { + pub fn from_native(s: SignatureNative) -> Self { + Self(s.into()) + } + + pub fn new_native(&self) -> SignatureNative { + SignatureNative::new(&self.0[..]) + } +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct Keypair([u8; KEYPAIR_LENGTH]); + +impl Keypair { + pub fn from_native(k: &KeypairNative) -> Self { + Self(k.to_bytes()) + } + + pub fn new_native(&self) -> Result { + KeypairNative::from_bytes(&self.0[..]) + } +} + +/// A representation of a Rust Vec that can be passed to C. Should not be modified or copied by C code. +#[repr(C)] +#[derive(Debug)] +pub struct CVec { + data: *mut T, + len: size_t, + capacity: size_t, +} + +impl CVec { + pub fn from_native(mut v: Vec) -> Self { + let out = Self { + data: v.as_mut_ptr(), + len: v.len(), + capacity: v.capacity(), + }; + mem::forget(v); + out + } + + pub unsafe fn into_native(self) -> Vec { + Vec::from_raw_parts(self.data, self.len, self.capacity) + } +} + +impl CVec { + #[allow(clippy::should_implement_trait)] + pub unsafe fn clone(&self) -> Self { + let native = Vec::from_raw_parts(self.data, self.len, self.capacity); + let mut new: Vec = Vec::with_capacity(native.capacity()); + as Clone>::clone_from(&mut new, &native); + mem::forget(native); + Self::from_native(new) + } +} + +impl CVec { + #[allow(clippy::should_implement_trait)] + pub unsafe fn clone(&self) -> Self { + let native = Vec::from_raw_parts(self.data, self.len, self.capacity); + let mut new: Vec = Vec::with_capacity(native.capacity()); + for elem in &native { + new.push(elem.clone()); + } + mem::forget(native); + Self::from_native(new) + } +} + +#[no_mangle] +pub unsafe extern "C" fn free_transaction(tx: *mut Transaction) { + Box::from_raw(tx); +} + +#[no_mangle] +pub unsafe extern "C" fn free_message(m: *mut Message) { + Box::from_raw(m); +} + +#[no_mangle] +pub unsafe extern "C" fn free_message_header(mh: *mut MessageHeader) { + Box::from_raw(mh); +} + +#[no_mangle] +pub unsafe extern "C" fn free_signature(s: *mut Signature) { + Box::from_raw(s); +} + +#[no_mangle] +pub unsafe extern "C" fn free_compiled_instruction(i: *mut CompiledInstruction) { + Box::from_raw(i); +} + +#[no_mangle] +pub unsafe extern "C" fn free_c_string(s: *mut c_char) { + CString::from_raw(s); +} + +#[no_mangle] +pub unsafe extern "C" fn new_unsigned_transaction(message: *mut Message) -> *mut Transaction { + let message = Box::from_raw(message); + let tx = Box::new(Transaction::from_native(TransactionNative::new_unsigned( + message.into_native(), + ))); + Box::into_raw(tx) +} + +/// Generate a `Keypair` from a uint8_t[32] seed. Assumes the pointer is to an array of length 32. +/// +/// # Undefined Behavior +/// +/// Causes UB if `seed` is not a pointer to an array of length 32 or if `seed` is `NULL` +#[no_mangle] +pub unsafe extern "C" fn generate_keypair(seed: *const u8) -> *mut Keypair { + let seed = <&[u8] as TryInto<&[u8; PUBLIC_KEY_LENGTH]>>::try_into(slice::from_raw_parts( + seed, + PUBLIC_KEY_LENGTH, + )) + .unwrap(); // Guaranteed not to panic + let mut rng = ChaChaRng::from_seed(*seed); + let keypair = KeypairNative::generate(&mut rng); + let keypair = Box::new(Keypair::from_native(&keypair)); + Box::into_raw(keypair) +} + +/// Get a pubkey from a keypair. Returns `NULL` if the conversion causes an error +/// +/// # Undefined Behavior +/// +/// Causes UB if `keypair` is `NULL` or if `keypair` in not a pointer to a valid `Keypair` +#[no_mangle] +pub unsafe extern "C" fn get_keypair_pubkey(keypair: *const Keypair) -> *mut Pubkey { + let keypair = if let Ok(k) = (*keypair).new_native() { + k + } else { + return ptr::null_mut(); + }; + let pubkey = Box::new(keypair.pubkey()); + Box::into_raw(pubkey) +} + +/// Serialize a `Transaction` and save a pointer to the resulting byte array to `serialized`, and its +/// length to `len`. Returns `0` for success, other for failure. Consumes and frees the `Transaction`. +/// +/// # Undefined Behavior +/// +/// Causes UB if any of the input pointers is `NULL`, or if `tx` is not a valid `Transaction` +#[no_mangle] +pub unsafe extern "C" fn serialize_transaction( + tx: *mut Transaction, + serialized: *mut *const u8, + len: *mut size_t, +) -> c_int { + let tx = Box::from_raw(tx); + let tx = tx.into_native(); + let mut serialized_tx = if let Ok(s) = serialize(&tx) { + s + } else { + return 1; + }; + serialized_tx.shrink_to_fit(); + *serialized = serialized_tx.as_mut_ptr(); + *len = serialized_tx.len(); + mem::forget(serialized_tx); + 0 +} + +/// Deserialize an array of bytes into a `Transaction`. Returns `NULL` if deserialization fails +/// +/// # Undefined Behavior +/// +/// Causes UB if `bytes` is `NULL`, or if `bytes` does not point to a valid array of length `len` +#[no_mangle] +pub unsafe extern "C" fn deserialize_transaction( + bytes: *const u8, + len: size_t, +) -> *mut Transaction { + let slice = slice::from_raw_parts(bytes, len); + let tx = if let Ok(t) = deserialize::(slice) { + t + } else { + return ptr::null_mut(); + }; + let tx = Box::new(Transaction::from_native(tx)); + Box::into_raw(tx) +} + +/// Sign a transaction with a subset of the required `Keypair`s. If the `recent_blockhash` supplied +/// does not match that of the `Transaction`, the supplied one will be used, and any existing +/// signatures will be discarded. Returns `0` for success, other for failure. +/// +/// # Undefined Behavior +/// +/// Causes UB if any of the pointers is `NULL`, or if `keypairs` does not point to a valid array of +/// `Keypairs` of length `num_keypairs` +#[no_mangle] +pub unsafe extern "C" fn transaction_partial_sign( + tx: *mut Transaction, + keypairs: *const Keypair, + num_keypairs: size_t, + recent_blockhash: *const Hash, +) -> c_int { + let mut tx = Box::from_raw(tx); + let mut tx_native = tx.clone().into_native(); + + let keypairs = slice::from_raw_parts(keypairs, num_keypairs); + let keypairs: Vec> = keypairs.iter().map(|k| k.new_native()).collect(); + let keypairs: Vec = if keypairs.iter().all(|k| k.is_ok()) { + keypairs + .into_iter() + .map(|k| k.expect("This shouldn't ever happen")) + .collect() + } else { + return 1; + }; + let keypairs_ref: Vec<&KeypairNative> = keypairs.iter().collect(); + + let positions = if let Ok(v) = tx_native.get_signing_keypair_positions(&keypairs_ref[..]) { + v + } else { + return 2; + }; + let positions: Vec = if positions.iter().all(|pos| pos.is_some()) { + positions + .iter() + .map(|pos| pos.expect("This shouldn't ever happen")) + .collect() + } else { + return 3; + }; + + tx_native.partial_sign_unchecked(&keypairs_ref[..], positions, *recent_blockhash); + *tx = Transaction::from_native(tx_native); + Box::into_raw(tx); + 0 +} + +/// Get the printable c-string of a Pubkey. The returned c-string must be freed with `free_c_string()` +/// Returns `NULL` if the conversion fails. +/// +/// # Undefined Behavior +/// +/// Causes UB if `pubkey` is `NULL`, or if the returned c-string is freed by any method other than +/// calling `free_c_string()` +#[no_mangle] +pub unsafe extern "C" fn get_pubkey_string(pubkey: *const Pubkey) -> *mut c_char { + if let Ok(s) = CString::new(format!("{}", *pubkey)) { + s.into_raw() + } else { + ptr::null_mut() + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use bincode::serialize; + use rand_chacha::ChaChaRng; + use rand_core::SeedableRng; + use solana_sdk::signature::{Keypair as KeypairNative, KeypairUtil}; + use solana_sdk::system_transaction; + + #[test] + fn test_generate_keypair() { + let seed = [1u8; 32]; + let mut rng = ChaChaRng::from_seed(seed); + let keypair = KeypairNative::generate(&mut rng); + let c_keypair = unsafe { Box::from_raw(generate_keypair(seed.as_ptr())) }; + assert_eq!(c_keypair.new_native(), Ok(keypair)); + } + + #[test] + fn test_get_pubkey() { + let seed = [1u8; 32]; + unsafe { + let keypair = generate_keypair(seed.as_ptr()); + let pubkey = Box::from_raw(get_keypair_pubkey(keypair)); + let keypair = Box::from_raw(keypair); + let keypair = keypair.new_native().unwrap(); + assert_eq!(keypair.pubkey(), *pubkey); + }; + } + + #[test] + fn test_serialize_transaction() { + let key = KeypairNative::new(); + let to = Pubkey::new_rand(); + let blockhash = Hash::default(); + let tx = system_transaction::create_user_account(&key, &to, 50, blockhash); + let serialized = serialize(&tx).unwrap(); + let tx = Box::new(Transaction::from_native(tx)); + let tx = Box::into_raw(tx); + let mut res: *const u8 = ptr::null_mut(); + let mut len: size_t = 0; + let slice; + unsafe { + assert_eq!(0, serialize_transaction(tx, &mut res, &mut len)); + slice = slice::from_raw_parts(res, len); + } + assert_eq!(serialized, slice); + } + + #[test] + fn test_deserialize_transaction() { + let key = KeypairNative::new(); + let to = Pubkey::new_rand(); + let blockhash = Hash::default(); + let tx = system_transaction::create_user_account(&key, &to, 50, blockhash); + let serialized = serialize(&tx).unwrap(); + let deserialized; + unsafe { + let ret = deserialize_transaction(serialized.as_ptr(), serialized.len()); + assert_ne!(ret, ptr::null_mut()); + let ret = Box::from_raw(ret); + deserialized = ret.into_native(); + } + assert_eq!(deserialized, tx); + } + + #[test] + fn test_deserialize_transaction_bad() { + let serialized_bad = vec![0u8; 3]; + let deserialized; + unsafe { + deserialized = deserialize_transaction(serialized_bad.as_ptr(), serialized_bad.len()); + } + assert_eq!(deserialized, ptr::null_mut()); + } + + #[test] + fn test_get_pubkey_string() { + let pubkey = Pubkey::new_rand(); + let str_native = format!("{}", pubkey); + let str_c; + unsafe { + str_c = CString::from_raw(get_pubkey_string(&pubkey)); + } + let str_c = str_c.into_string().unwrap(); + assert_eq!(str_native, str_c); + } + + #[test] + fn test_transaction_partial_sign() { + let key_native = KeypairNative::new(); + let to = Pubkey::new_rand(); + let blockhash = Hash::default(); + let mut tx_native = + system_transaction::create_user_account(&key_native, &to, 50, blockhash); + let tx = Box::into_raw(Box::new(Transaction::from_native(tx_native.clone()))); + let key = Keypair::from_native(&key_native); + let tx2; + unsafe { + assert_eq!(0, transaction_partial_sign(tx, &key, 1, &blockhash)); + tx2 = Box::from_raw(tx).into_native(); + } + tx_native.partial_sign(&[&key_native], blockhash); + assert_eq!(tx_native, tx2); + } +} diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 0a79236fbe..de6c2c0566 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -14,7 +14,7 @@ bincode = "1.1.4" bs58 = "0.2.0" byteorder = "1.3.2" chrono = { version = "0.4.7", features = ["serde"] } -generic-array = { version = "0.13.1", default-features = false, features = ["serde"] } +generic-array = { version = "0.13.2", default-features = false, features = ["serde", "more_lengths"] } hex = "0.3.2" itertools = "0.8.0" log = "0.4.2" diff --git a/sdk/src/pubkey.rs b/sdk/src/pubkey.rs index 59b9734a3e..fff8e7c8a1 100644 --- a/sdk/src/pubkey.rs +++ b/sdk/src/pubkey.rs @@ -1,5 +1,4 @@ -use generic_array::typenum::U32; -use generic_array::GenericArray; +use std::convert::TryFrom; use std::error; use std::fmt; use std::fs::{self, File}; @@ -8,9 +7,9 @@ use std::mem; use std::path::Path; use std::str::FromStr; -#[repr(C)] +#[repr(transparent)] #[derive(Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Pubkey(GenericArray); +pub struct Pubkey([u8; 32]); #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParsePubkeyError { @@ -43,7 +42,10 @@ impl FromStr for Pubkey { impl Pubkey { pub fn new(pubkey_vec: &[u8]) -> Self { - Pubkey(GenericArray::clone_from_slice(&pubkey_vec)) + Self( + <[u8; 32]>::try_from(<&[u8]>::clone(&pubkey_vec)) + .expect("Slice must be the same length as a Pubkey"), + ) } pub fn new_rand() -> Self { diff --git a/sdk/src/signature.rs b/sdk/src/signature.rs index 1742e04732..2174be3796 100644 --- a/sdk/src/signature.rs +++ b/sdk/src/signature.rs @@ -19,6 +19,7 @@ use std::str::FromStr; pub type Keypair = ed25519_dalek::Keypair; +#[repr(transparent)] #[derive(Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Signature(GenericArray); @@ -74,6 +75,12 @@ impl fmt::Display for Signature { } } +impl Into<[u8; 64]> for Signature { + fn into(self) -> [u8; 64] { + as Into<[u8; 64]>>::into(self.0) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ParseSignatureError { WrongSize, diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index e4e176d538..9cec326be9 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -193,9 +193,24 @@ impl Transaction { /// if recent_blockhash is not the same as currently in the transaction, /// clear any prior signatures and update recent_blockhash pub fn partial_sign(&mut self, keypairs: &[&T], recent_blockhash: Hash) { - let signed_keys = - &self.message.account_keys[0..self.message.header.num_required_signatures as usize]; + let positions = self + .get_signing_keypair_positions(keypairs) + .expect("account_keys doesn't contain num_required_signatures keys"); + let positions: Vec = positions + .iter() + .map(|pos| pos.expect("keypair-pubkey mismatch")) + .collect(); + self.partial_sign_unchecked(keypairs, positions, recent_blockhash) + } + /// Sign the transaction and place the signatures in their associated positions in `signatures` + /// without checking that the positions are correct. + pub fn partial_sign_unchecked( + &mut self, + keypairs: &[&T], + positions: Vec, + recent_blockhash: Hash, + ) { // if you change the blockhash, you're re-signing... if recent_blockhash != self.message.recent_blockhash { self.message.recent_blockhash = recent_blockhash; @@ -204,16 +219,32 @@ impl Transaction { .for_each(|signature| *signature = Signature::default()); } - for keypair in keypairs { - let i = signed_keys - .iter() - .position(|pubkey| pubkey == &keypair.pubkey()) - .expect("keypair-pubkey mismatch"); - - self.signatures[i] = keypair.sign_message(&self.message_data()) + for i in 0..positions.len() { + self.signatures[positions[i]] = keypairs[i].sign_message(&self.message_data()) } } + /// Get the positions of the pubkeys in `account_keys` associated with signing keypairs + pub fn get_signing_keypair_positions( + &self, + keypairs: &[&T], + ) -> Result>> { + if self.message.account_keys.len() < self.message.header.num_required_signatures as usize { + return Err(TransactionError::InvalidAccountIndex); + } + let signed_keys = + &self.message.account_keys[0..self.message.header.num_required_signatures as usize]; + + Ok(keypairs + .iter() + .map(|keypair| { + signed_keys + .iter() + .position(|pubkey| pubkey == &keypair.pubkey()) + }) + .collect()) + } + pub fn is_signed(&self) -> bool { self.signatures .iter()