Add BPF Rust noop example (#316)

This commit is contained in:
Jack May 2019-05-10 14:16:35 -07:00 committed by Michael Vines
parent f7a02e54df
commit 4c4f5a0fb9
16 changed files with 605 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
/target/
Cargo.lock

View File

@ -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 <maintainers@solana.com>"]
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"]

View File

@ -0,0 +1,4 @@
[dependencies.compiler_builtins]
path = "../../bpf-sdk/rust-bpf-sysroot/src/compiler-builtins"

View File

@ -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
}

View File

@ -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; }

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -ex
cargo clean

View File

@ -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

View File

@ -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
}

View File

@ -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<U256> = 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::<u64>();
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::<u64>();
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::<u64>();
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::<u64>();
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::<u64>();
// 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);
}
}

View File

@ -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}],

7
web3.js/test/fixtures/noop-rust/build.sh vendored Executable file
View File

@ -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 .

Binary file not shown.