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 0000000000..1ec01e806b Binary files /dev/null and b/web3.js/test/fixtures/noop-rust/solana_bpf_rust_noop.so differ