From 4c4f5a0fb9bc3dfac212fca31807e787ed8aa2d2 Mon Sep 17 00:00:00 2001 From: Jack May Date: Fri, 10 May 2019 14:16:35 -0700 Subject: [PATCH] Add BPF Rust noop example (#316) --- web3.js/.travis.yml | 3 +- web3.js/README.md | 5 +- web3.js/examples/bpf-rust-noop/.gitignore | 3 + web3.js/examples/bpf-rust-noop/Cargo.toml | 25 ++ web3.js/examples/bpf-rust-noop/Xargo.toml | 4 + web3.js/examples/bpf-rust-noop/bpf.ld | 19 + web3.js/examples/bpf-rust-noop/build.sh | 27 ++ web3.js/examples/bpf-rust-noop/clean.sh | 5 + web3.js/examples/bpf-rust-noop/dump.sh | 12 + web3.js/examples/bpf-rust-noop/src/lib.rs | 55 +++ .../examples/bpf-rust-noop/src/solana_sdk.rs | 422 ++++++++++++++++++ web3.js/test/bpf-loader.test.js | 23 +- .../test/fixtures/{noop => noop-c}/build.sh | 0 .../test/fixtures/{noop => noop-c}/noop.so | Bin web3.js/test/fixtures/noop-rust/build.sh | 7 + .../noop-rust/solana_bpf_rust_noop.so | Bin 0 -> 112744 bytes 16 files changed, 605 insertions(+), 5 deletions(-) create mode 100644 web3.js/examples/bpf-rust-noop/.gitignore create mode 100644 web3.js/examples/bpf-rust-noop/Cargo.toml create mode 100644 web3.js/examples/bpf-rust-noop/Xargo.toml create mode 100644 web3.js/examples/bpf-rust-noop/bpf.ld create mode 100755 web3.js/examples/bpf-rust-noop/build.sh create mode 100755 web3.js/examples/bpf-rust-noop/clean.sh create mode 100755 web3.js/examples/bpf-rust-noop/dump.sh create mode 100644 web3.js/examples/bpf-rust-noop/src/lib.rs create mode 100644 web3.js/examples/bpf-rust-noop/src/solana_sdk.rs rename web3.js/test/fixtures/{noop => noop-c}/build.sh (100%) rename web3.js/test/fixtures/{noop => noop-c}/noop.so (100%) create mode 100755 web3.js/test/fixtures/noop-rust/build.sh create mode 100755 web3.js/test/fixtures/noop-rust/solana_bpf_rust_noop.so diff --git a/web3.js/.travis.yml b/web3.js/.travis.yml index d30726a87d..5482f0b78e 100644 --- a/web3.js/.travis.yml +++ b/web3.js/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo apt-get update - sudo apt-get install -y clang-7 --allow-unauthenticated - clang-7 --version - - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none + - curl https://sh.rustup.rs -sSf | sh -s -- -y - PATH=$HOME/.cargo/bin:$PATH - rustup --version @@ -31,6 +31,7 @@ script: - npm run lint - npm run codecov - make -C examples/bpf-c-noop/ + - ./examples/bpf-rust-noop/build.sh - npm run localnet:update - npm run localnet:up - npm run examples diff --git a/web3.js/README.md b/web3.js/README.md index b1307247d7..ff03b2427e 100644 --- a/web3.js/README.md +++ b/web3.js/README.md @@ -54,7 +54,10 @@ $ npm install --save @solana/web3.js ### BPF program development clang-7.0 must be installed to build BPF programs, such as -`examples/bpf-c-noop/`. See `bpf-sdk/README.md` for installation details +`examples/bpf-c-noop/`. See `bpf-sdk/README.md` for installation details. + +Rust must be installed to build Rust BPF programs, see: https://www.rust-lang.org/install.html such as +`examples/bpf-rust-noop/`. See https://www.rust-lang.org/install.html for installation details. ## Usage diff --git a/web3.js/examples/bpf-rust-noop/.gitignore b/web3.js/examples/bpf-rust-noop/.gitignore new file mode 100644 index 0000000000..e13de17f65 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/.gitignore @@ -0,0 +1,3 @@ +/target/ + +Cargo.lock diff --git a/web3.js/examples/bpf-rust-noop/Cargo.toml b/web3.js/examples/bpf-rust-noop/Cargo.toml new file mode 100644 index 0000000000..7a6473c5c8 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/Cargo.toml @@ -0,0 +1,25 @@ + +# Note: This crate must be built using build.sh + +[package] +name = "solana-bpf-rust-noop" +version = "0.15.0" +description = "Solana BPF noop program written in Rust" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" + +[dependencies] +# byteorder = { version = "1.3.1", default-features = false } +# heapless = { version = "0.4.0", default-features = false } +# byte = { version = "0.2", default-features = false } + +[workspace] +members = [] + +[lib] +name = "solana_bpf_rust_noop" +crate-type = ["cdylib"] + + diff --git a/web3.js/examples/bpf-rust-noop/Xargo.toml b/web3.js/examples/bpf-rust-noop/Xargo.toml new file mode 100644 index 0000000000..db70306d58 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/Xargo.toml @@ -0,0 +1,4 @@ + + +[dependencies.compiler_builtins] +path = "../../bpf-sdk/rust-bpf-sysroot/src/compiler-builtins" \ No newline at end of file diff --git a/web3.js/examples/bpf-rust-noop/bpf.ld b/web3.js/examples/bpf-rust-noop/bpf.ld new file mode 100644 index 0000000000..62a7170662 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/bpf.ld @@ -0,0 +1,19 @@ +PHDRS +{ + text PT_LOAD ; + rodata PT_LOAD ; + dynamic PT_DYNAMIC ; +} + +SECTIONS +{ + . = SIZEOF_HEADERS; + .text : { *(.text) } :text + .rodata : { *(.rodata) } :rodata + .dynamic : { *(.dynamic) } :dynamic + .dynsym : { *(.dynsym) } :dynamic + .dynstr : { *(.dynstr) } :dynamic + .gnu.hash : { *(.gnu.hash) } :dynamic + .rel.dyn : { *(.rel.dyn) } :dynamic + .hash : { *(.hash) } :dynamic +} diff --git a/web3.js/examples/bpf-rust-noop/build.sh b/web3.js/examples/bpf-rust-noop/build.sh new file mode 100755 index 0000000000..abfd7e6264 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")" + +cargo install xargo + +set -ex + +# Ensure the sdk is installed +../../bpf-sdk/scripts/install.sh +rustup override set bpf + +export RUSTFLAGS="$RUSTFLAGS \ + -C lto=no \ + -C opt-level=2 \ + -C link-arg=-Tbpf.ld \ + -C link-arg=-z -C link-arg=notext \ + -C link-arg=--Bdynamic \ + -C link-arg=-shared \ + -C link-arg=--entry=entrypoint \ + -C linker=../../bpf-sdk/llvm-native/bin/ld.lld" +export XARGO_HOME="$PWD/target/xargo" +export XARGO_RUST_SRC="../../bpf-sdk/rust-bpf-sysroot/src" +# export XARGO_RUST_SRC="../../../../../rust-bpf-sysroot/src" +xargo build --target bpfel-unknown-unknown --release -v + +{ { set +x; } 2>/dev/null; echo Success; } diff --git a/web3.js/examples/bpf-rust-noop/clean.sh b/web3.js/examples/bpf-rust-noop/clean.sh new file mode 100755 index 0000000000..ea95016e3d --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/clean.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -ex + +cargo clean diff --git a/web3.js/examples/bpf-rust-noop/dump.sh b/web3.js/examples/bpf-rust-noop/dump.sh new file mode 100755 index 0000000000..8108047d5b --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/dump.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +cp dump.txt dump_last.txt 2>/dev/null + +set -x +set -e + +./clean.sh +./build.sh +ls -la ./target/bpfel_unknown_unknown/release/solana_bpf_rust_noop.so > dump.txt +greadelf -aW ./target/bpfel_unknown_unknown/release/solana_bpf_rust_noop.so | rustfilt >> dump.txt +llvm-objdump -print-imm-hex --source --disassemble ./target/bpfel_unknown_unknown/release/solana_bpf_rust_noop.so >> dump.txt diff --git a/web3.js/examples/bpf-rust-noop/src/lib.rs b/web3.js/examples/bpf-rust-noop/src/lib.rs new file mode 100644 index 0000000000..4fa2a549c1 --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/src/lib.rs @@ -0,0 +1,55 @@ +//! @brief Example Rust-based BPF program that prints out the parameters passed to it + +#![cfg(not(test))] +#![no_std] + +mod solana_sdk; + +use solana_sdk::*; + +struct SStruct { + x: u64, + y: u64, + z: u64, +} + +#[inline(never)] +fn return_sstruct() -> SStruct { + SStruct { x: 1, y: 2, z: 3 } +} + +fn process(ka: &mut [SolKeyedAccount], data: &[u8], info: &SolClusterInfo) -> bool { + sol_log("Tick height:"); + sol_log_64(info.tick_height, 0, 0, 0, 0); + sol_log("Program identifier:"); + sol_log_key(&info.program_id); + + // Log the provided account keys and instruction input data. In the case of + // the no-op program, no account keys or input data are expected but real + // programs will have specific requirements so they can do their work. + sol_log("Account keys and instruction input data:"); + sol_log_params(ka, data); + + { + // Test - use core methods, unwrap + + // valid bytes, in a stack-allocated array + let sparkle_heart = [240, 159, 146, 150]; + + let result_str = core::str::from_utf8(&sparkle_heart).unwrap(); + + sol_log_64(0, 0, 0, 0, result_str.len() as u64); + sol_log(result_str); + assert_eq!("💖", result_str); + } + + { + // Test - struct return + let s = return_sstruct(); + sol_log_64(0, 0, s.x, s.y, s.z); + assert_eq!(s.x + s.y + s.z, 6); + } + + sol_log("Success"); + true +} diff --git a/web3.js/examples/bpf-rust-noop/src/solana_sdk.rs b/web3.js/examples/bpf-rust-noop/src/solana_sdk.rs new file mode 100644 index 0000000000..248a63a7cb --- /dev/null +++ b/web3.js/examples/bpf-rust-noop/src/solana_sdk.rs @@ -0,0 +1,422 @@ +//! @brief Solana Rust-based BPF program utility functions and types + +// extern crate heapless; + +// use self::heapless::consts::*; +// use self::heapless::String; // fixed capacity `std::Vec` // type level integer used to specify capacity +#[cfg(test)] +use self::tests::process; +use core::mem::size_of; +use core::panic::PanicInfo; +use core::slice::from_raw_parts; + +#[cfg(not(test))] +use process; + +// Panic handling +extern "C" { + pub fn sol_panic_() -> !; +} +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + sol_log("Panic!"); + // TODO rashes! sol_log(_info.payload().downcast_ref::<&str>().unwrap()); + if let Some(location) = _info.location() { + if !location.file().is_empty() { + // TODO location.file() returns empty str, if we get here its been fixed + sol_log(location.file()); + sol_log("location.file() is fixed!!"); + unsafe { + sol_panic_(); + } + } + sol_log_64(0, 0, 0, location.line() as u64, location.column() as u64); + } else { + sol_log("Panic! but could not get location information"); + } + unsafe { + sol_panic_(); + } +} + +extern "C" { + fn sol_log_(message: *const u8); +} +/// Helper function that prints a string to stdout +#[inline(never)] // stack intensive, block inline so everyone does not incur +pub fn sol_log(message: &str) { + // TODO This is extremely slow, do something better + let mut buf: [u8; 128] = [0; 128]; + for (i, b) in message.as_bytes().iter().enumerate() { + if i >= 126 { + break; + } + buf[i] = *b; + } + unsafe { + sol_log_(buf.as_ptr()); + } + + // let mut c_string: String = String::new(); + // if message.len() < 256 { + // if c_string.push_str(message).is_err() { + // c_string + // .push_str("Attempted to log a malformed string\0") + // .is_ok(); + // } + // if c_string.push('\0').is_err() { + // c_string.push_str("Failed to log string\0").is_ok(); + // }; + // } else { + // c_string + // .push_str("Attempted to log a string that is too long\0") + // .is_ok(); + // } + // unsafe { + // sol_log_(message.as_ptr()); + // } +} + +extern "C" { + fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64); +} +/// Helper function that prints a 64 bit values represented in hexadecimal +/// to stdout +pub fn sol_log_64(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64) { + unsafe { + sol_log_64_(arg1, arg2, arg3, arg4, arg5); + } +} + +/// Prints the hexadecimal representation of a public key +/// +/// @param key The public key to print +#[allow(dead_code)] +pub fn sol_log_key(key: &SolPubkey) { + for (i, k) in key.key.iter().enumerate() { + sol_log_64(0, 0, 0, i as u64, u64::from(*k)); + } +} + +/// Prints the hexadecimal representation of a slice +/// +/// @param slice The array to print +#[allow(dead_code)] +pub fn sol_log_slice(slice: &[u8]) { + for (i, s) in slice.iter().enumerate() { + sol_log_64(0, 0, 0, i as u64, u64::from(*s)); + } +} + +/// Prints the hexadecimal representation of the program's input parameters +/// +/// @param ka A pointer to an array of SolKeyedAccount to print +/// @param data A pointer to the instruction data to print +#[allow(dead_code)] +pub fn sol_log_params(ka: &[SolKeyedAccount], data: &[u8]) { + sol_log("- Number of KeyedAccounts"); + sol_log_64(0, 0, 0, 0, ka.len() as u64); + for k in ka.iter() { + sol_log("- Is signer"); + sol_log_64(0, 0, 0, 0, k.is_signer as u64); + sol_log("- Key"); + sol_log_key(&k.key); + sol_log("- Lamports"); + sol_log_64(0, 0, 0, 0, k.lamports); + sol_log("- AccountData"); + sol_log_slice(k.data); + sol_log("- Owner"); + sol_log_key(&k.owner); + } + sol_log("- Instruction data"); + sol_log_slice(data); +} + +pub const SIZE_PUBKEY: usize = 32; + +/// Public key +pub struct SolPubkey<'a> { + pub key: &'a [u8], +} + +/// Keyed Account +pub struct SolKeyedAccount<'a> { + /// Public key of the account + pub key: SolPubkey<'a>, + /// Public key of the account + pub is_signer: u64, + /// Number of lamports owned by this account + pub lamports: u64, + /// On-chain data within this account + pub data: &'a [u8], + /// Program that owns this account + pub owner: SolPubkey<'a>, +} + +/// Information about the state of the cluster immediately before the program +/// started executing the current instruction +pub struct SolClusterInfo<'a> { + /// Current ledger tick + pub tick_height: u64, + ///program_id of the currently executing program + pub program_id: SolPubkey<'a>, +} + +#[no_mangle] +pub extern "C" fn entrypoint(input: *mut u8) -> bool { + const NUM_KA: usize = 1; // Number of KeyedAccounts expected + let mut offset: usize = 0; + + // Number of KeyedAccounts present + + let num_ka = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let num_ka_ptr: *const u64 = input.add(offset) as *const u64; + *num_ka_ptr + }; + offset += 8; + + if num_ka != NUM_KA as u64 { + return false; + } + + // KeyedAccounts + + let is_signer = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let is_signer_ptr: *const u64 = input.add(offset) as *const u64; + *is_signer_ptr + }; + offset += size_of::(); + + let key_slice = unsafe { from_raw_parts(input.add(offset), SIZE_PUBKEY) }; + let key = SolPubkey { key: &key_slice }; + offset += SIZE_PUBKEY; + + let lamports = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let lamports_ptr: *const u64 = input.add(offset) as *const u64; + *lamports_ptr + }; + offset += size_of::(); + + let data_length = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let data_length_ptr: *const u64 = input.add(offset) as *const u64; + *data_length_ptr + } as usize; + offset += size_of::(); + + let data = unsafe { from_raw_parts(input.add(offset), data_length) }; + offset += data_length; + + let owner_slice = unsafe { from_raw_parts(input.add(offset), SIZE_PUBKEY) }; + let owner = SolPubkey { key: &owner_slice }; + offset += SIZE_PUBKEY; + + let mut ka = [SolKeyedAccount { + key, + is_signer, + lamports, + data, + owner, + }]; + + // Instruction data + + let data_length = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let data_length_ptr: *const u64 = input.add(offset) as *const u64; + *data_length_ptr + } as usize; + offset += size_of::(); + + let data = unsafe { from_raw_parts(input.add(offset), data_length) }; + offset += data_length; + + // Tick height + + let tick_height = unsafe { + #[allow(clippy::cast_ptr_alignment)] + let tick_height_ptr: *const u64 = input.add(offset) as *const u64; + *tick_height_ptr + }; + offset += size_of::(); + + // Id + + let program_id_slice = unsafe { from_raw_parts(input.add(offset), SIZE_PUBKEY) }; + let program_id: SolPubkey = SolPubkey { + key: &program_id_slice, + }; + + let info = SolClusterInfo { + tick_height, + program_id, + }; + + // Call user implementable function + process(&mut ka, &data, &info) +} + +#[cfg(test)] +mod tests { + extern crate std; + + use self::std::ffi::CStr; + use self::std::println; + use self::std::string::String; + use super::*; + + static mut _LOG_SCENARIO: u64 = 0; + fn get_log_scenario() -> u64 { + unsafe { _LOG_SCENARIO } + } + fn set_log_scenario(test: u64) { + unsafe { _LOG_SCENARIO = test }; + } + + #[no_mangle] + fn sol_log_(message: *const u8) { + let scenario = get_log_scenario(); + let c_str = unsafe { CStr::from_ptr(message as *const i8) }; + let string = c_str.to_str().unwrap(); + match scenario { + 1 => assert_eq!(string, "This is a test message"), + 2 => assert_eq!(string, "Attempted to log a string that is too long"), + 3 => { + let s: String = ['a'; 255].iter().collect(); + assert_eq!(string, s); + } + 4 => println!("{:?}", string), + _ => panic!("Unkown sol_log test"), + } + } + + static mut _LOG_64_SCENARIO: u64 = 0; + fn get_log_64_scenario() -> u64 { + unsafe { _LOG_64_SCENARIO } + } + fn set_log_64_scenario(test: u64) { + unsafe { _LOG_64_SCENARIO = test }; + } + + #[no_mangle] + fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64) { + let scenario = get_log_64_scenario(); + match scenario { + 1 => { + assert_eq!(1, arg1); + assert_eq!(2, arg2); + assert_eq!(3, arg3); + assert_eq!(4, arg4); + assert_eq!(5, arg5); + } + 2 => { + assert_eq!(0, arg1); + assert_eq!(0, arg2); + assert_eq!(0, arg3); + assert_eq!(arg4 + 1, arg5); + } + 3 => { + assert_eq!(0, arg1); + assert_eq!(0, arg2); + assert_eq!(0, arg3); + assert_eq!(arg4 + 1, arg5); + } + 4 => println!("{:?} {:?} {:?} {:?} {:?}", arg1, arg2, arg3, arg4, arg5), + _ => panic!("Unknown sol_log_64 test"), + } + } + + #[test] + fn test_sol_log() { + set_log_scenario(1); + sol_log("This is a test message"); + } + + #[test] + fn test_sol_log_long() { + set_log_scenario(2); + let s: String = ['a'; 256].iter().collect(); + sol_log(&s); + } + + #[test] + fn test_sol_log_max_length() { + set_log_scenario(3); + let s: String = ['a'; 255].iter().collect(); + sol_log(&s); + } + + #[test] + fn test_sol_log_64() { + set_log_64_scenario(1); + sol_log_64(1, 2, 3, 4, 5); + } + + #[test] + fn test_sol_log_key() { + set_log_64_scenario(2); + let key_array = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + ]; + let key = SolPubkey { key: &key_array }; + sol_log_key(&key); + } + + #[test] + fn test_sol_log_slice() { + set_log_64_scenario(3); + let array = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + ]; + sol_log_slice(&array); + } + + pub fn process(ka: &mut [SolKeyedAccount], data: &[u8], info: &SolClusterInfo) -> bool { + assert_eq!(1, ka.len()); + assert_eq!(1, ka[0].is_signer); + let key = [ + 151, 116, 3, 85, 181, 39, 151, 99, 155, 29, 208, 191, 255, 191, 11, 161, 4, 43, 104, + 189, 202, 240, 231, 111, 146, 255, 199, 71, 67, 34, 254, 68, + ]; + assert_eq!(SIZE_PUBKEY, ka[0].key.key.len()); + assert_eq!(key, ka[0].key.key); + assert_eq!(48, ka[0].lamports); + assert_eq!(1, ka[0].data.len()); + let owner = [0; 32]; + assert_eq!(SIZE_PUBKEY, ka[0].owner.key.len()); + assert_eq!(owner, ka[0].owner.key); + let d = [1, 0, 0, 0, 0, 0, 0, 0, 1]; + assert_eq!(9, data.len()); + assert_eq!(d, data); + assert_eq!(1, info.tick_height); + let program_id = [ + 190, 103, 191, 69, 193, 202, 38, 193, 95, 62, 131, 135, 105, 13, 142, 240, 155, 120, + 177, 90, 212, 54, 10, 118, 40, 33, 192, 8, 54, 141, 187, 63, + ]; + assert_eq!(program_id, info.program_id.key); + + true + } + + #[test] + fn test_entrypoint() { + set_log_scenario(4); + set_log_64_scenario(4); + let mut input: [u8; 154] = [ + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 151, 116, 3, 85, 181, 39, 151, 99, 155, + 29, 208, 191, 255, 191, 11, 161, 4, 43, 104, 189, 202, 240, 231, 111, 146, 255, 199, + 71, 67, 34, 254, 68, 48, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 190, 103, 191, + 69, 193, 202, 38, 193, 95, 62, 131, 135, 105, 13, 142, 240, 155, 120, 177, 90, 212, 54, + 10, 118, 40, 33, 192, 8, 54, 141, 187, 63, + ]; + + entrypoint(&mut input[0] as *mut u8); + } +} diff --git a/web3.js/test/bpf-loader.test.js b/web3.js/test/bpf-loader.test.js index 19ea3d86a5..f8809a566d 100644 --- a/web3.js/test/bpf-loader.test.js +++ b/web3.js/test/bpf-loader.test.js @@ -14,10 +14,10 @@ import {newAccountWithLamports} from './new-account-with-lamports'; if (!mockRpcEnabled) { // The default of 5 seconds is too slow for live testing sometimes - jest.setTimeout(30000); + jest.setTimeout(120000); } -test('load BPF program', async () => { +test('load BPF C program', async () => { if (mockRpcEnabled) { console.log('non-live test skipped'); return; @@ -25,7 +25,24 @@ test('load BPF program', async () => { const connection = new Connection(url); const from = await newAccountWithLamports(connection, 1024); - const data = await fs.readFile('test/fixtures/noop/noop.so'); + const data = await fs.readFile('test/fixtures/noop-c/noop.so'); + const programId = await BpfLoader.load(connection, from, data); + const transaction = new Transaction().add({ + keys: [{pubkey: from.publicKey, isSigner: true}], + programId, + }); + await sendAndConfirmTransaction(connection, transaction, from); +}); + +test('load BPF Rust program', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const connection = new Connection(url); + const from = await newAccountWithLamports(connection, 1024); + const data = await fs.readFile('test/fixtures/noop-rust/solana_bpf_rust_noop.so'); const programId = await BpfLoader.load(connection, from, data); const transaction = new Transaction().add({ keys: [{pubkey: from.publicKey, isSigner: true}], diff --git a/web3.js/test/fixtures/noop/build.sh b/web3.js/test/fixtures/noop-c/build.sh similarity index 100% rename from web3.js/test/fixtures/noop/build.sh rename to web3.js/test/fixtures/noop-c/build.sh diff --git a/web3.js/test/fixtures/noop/noop.so b/web3.js/test/fixtures/noop-c/noop.so similarity index 100% rename from web3.js/test/fixtures/noop/noop.so rename to web3.js/test/fixtures/noop-c/noop.so diff --git a/web3.js/test/fixtures/noop-rust/build.sh b/web3.js/test/fixtures/noop-rust/build.sh new file mode 100755 index 0000000000..0d795078fa --- /dev/null +++ b/web3.js/test/fixtures/noop-rust/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -ex + +cd "$(dirname "$0")" + +../../../examples/bpf-rust-noop/build.sh +cp ../../../examples/bpf-rust-noop/target/bpfel-unknown-unknown/release/solana_bpf_rust_noop.so . diff --git a/web3.js/test/fixtures/noop-rust/solana_bpf_rust_noop.so b/web3.js/test/fixtures/noop-rust/solana_bpf_rust_noop.so new file mode 100755 index 0000000000000000000000000000000000000000..1ec01e806b2a2995588b35f3c1e51cc040f9b5a1 GIT binary patch literal 112744 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 v4Zyzseg7{}elWz+;OEQQ!#~vK literal 0 HcmV?d00001