Rename programs to instruction_processors (#3789)
* Rename programs to instruction_processors * Updates around the code base to support instruction_processors rename * Kabab instruction_processors * Update Cargo.toml files and scripts to use instruction-processors * Update Cargo.toml to use instruction-processors * Update CI scripts to use instruction-processors
This commit is contained in:
@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "solana-bpf-programs"
|
||||
description = "Blockchain, Rebuilt for Scale"
|
||||
version = "0.14.0"
|
||||
documentation = "https://docs.rs/solana"
|
||||
homepage = "https://solana.com/"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
bpf_c = []
|
||||
bpf_rust = []
|
||||
|
||||
[build-dependencies]
|
||||
walkdir = "2"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
byteorder = "1.3.1"
|
||||
elf = "0.0.10"
|
||||
solana_rbpf = "=0.1.10"
|
||||
solana-bpfloader = { path = "../bpf_loader", version = "0.14.0" }
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[[bench]]
|
||||
name = "bpf_loader"
|
@ -1,128 +0,0 @@
|
||||
#![feature(test)]
|
||||
#![cfg(feature = "bpf_c")]
|
||||
extern crate test;
|
||||
|
||||
use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
|
||||
use solana_rbpf::EbpfVmRaw;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Error;
|
||||
use std::io::Read;
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
use test::Bencher;
|
||||
|
||||
/// BPF program file extension
|
||||
const PLATFORM_FILE_EXTENSION_BPF: &str = "so";
|
||||
/// Create a BPF program file name
|
||||
fn create_bpf_path(name: &str) -> PathBuf {
|
||||
let mut pathbuf = {
|
||||
let current_exe = env::current_exe().unwrap();
|
||||
PathBuf::from(current_exe.parent().unwrap().parent().unwrap())
|
||||
};
|
||||
pathbuf.push("bpf/");
|
||||
pathbuf.push(name);
|
||||
pathbuf.set_extension(PLATFORM_FILE_EXTENSION_BPF);
|
||||
pathbuf
|
||||
}
|
||||
|
||||
fn empty_check(_prog: &[u8]) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_elf() -> Result<Vec<u8>, std::io::Error> {
|
||||
let path = create_bpf_path("bench_alu");
|
||||
let mut file = File::open(&path).expect(&format!("Unable to open {:?}", path));
|
||||
let mut elf = Vec::new();
|
||||
file.read_to_end(&mut elf).unwrap();
|
||||
Ok(elf)
|
||||
}
|
||||
|
||||
const ARMSTRONG_LIMIT: u64 = 500;
|
||||
const ARMSTRONG_EXPECTED: u64 = 5;
|
||||
|
||||
#[bench]
|
||||
fn bench_program_load_elf(bencher: &mut Bencher) {
|
||||
let elf = load_elf().unwrap();
|
||||
let mut vm = EbpfVmRaw::new(None).unwrap();
|
||||
vm.set_verifier(empty_check).unwrap();
|
||||
|
||||
bencher.iter(|| {
|
||||
vm.set_elf(&elf).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_program_verify(bencher: &mut Bencher) {
|
||||
let elf = load_elf().unwrap();
|
||||
let mut vm = EbpfVmRaw::new(None).unwrap();
|
||||
vm.set_verifier(empty_check).unwrap();
|
||||
vm.set_elf(&elf).unwrap();
|
||||
|
||||
bencher.iter(|| {
|
||||
vm.set_verifier(solana_bpf_loader::bpf_verifier::check)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_program_alu(bencher: &mut Bencher) {
|
||||
let ns_per_s = 1000000000;
|
||||
let one_million = 1000000;
|
||||
let mut inner_iter = vec![];
|
||||
inner_iter
|
||||
.write_u64::<LittleEndian>(ARMSTRONG_LIMIT)
|
||||
.unwrap();
|
||||
inner_iter.write_u64::<LittleEndian>(0).unwrap();
|
||||
|
||||
let elf = load_elf().unwrap();
|
||||
let mut vm = solana_bpf_loader::create_vm(&elf).unwrap();
|
||||
|
||||
println!("Interpreted:");
|
||||
assert_eq!(
|
||||
1, /*true*/
|
||||
vm.execute_program(&mut inner_iter).unwrap()
|
||||
);
|
||||
assert_eq!(ARMSTRONG_LIMIT, LittleEndian::read_u64(&inner_iter));
|
||||
assert_eq!(
|
||||
ARMSTRONG_EXPECTED,
|
||||
LittleEndian::read_u64(&inner_iter[mem::size_of::<u64>()..])
|
||||
);
|
||||
|
||||
bencher.iter(|| {
|
||||
vm.execute_program(&mut inner_iter).unwrap();
|
||||
});
|
||||
let instructions = vm.get_last_instruction_count();
|
||||
let summary = bencher.bench(|_bencher| {}).unwrap();
|
||||
println!(" {:?} instructions", instructions);
|
||||
println!(" {:?} ns/iter median", summary.median as u64);
|
||||
assert!(0f64 != summary.median);
|
||||
let mips = (instructions * (ns_per_s / summary.median as u64)) / one_million;
|
||||
println!(" {:?} MIPS", mips);
|
||||
println!("{{ \"type\": \"bench\", \"name\": \"bench_program_alu_interpreted_mips\", \"median\": {:?}, \"deviation\": 0 }}", mips);
|
||||
|
||||
println!("JIT to native:");
|
||||
vm.jit_compile().unwrap();
|
||||
unsafe {
|
||||
assert_eq!(
|
||||
1, /*true*/
|
||||
vm.execute_program_jit(&mut inner_iter).unwrap()
|
||||
);
|
||||
}
|
||||
assert_eq!(ARMSTRONG_LIMIT, LittleEndian::read_u64(&inner_iter));
|
||||
assert_eq!(
|
||||
ARMSTRONG_EXPECTED,
|
||||
LittleEndian::read_u64(&inner_iter[mem::size_of::<u64>()..])
|
||||
);
|
||||
|
||||
bencher.iter(|| unsafe {
|
||||
vm.execute_program_jit(&mut inner_iter).unwrap();
|
||||
});
|
||||
let summary = bencher.bench(|_bencher| {}).unwrap();
|
||||
println!(" {:?} instructions", instructions);
|
||||
println!(" {:?} ns/iter median", summary.median as u64);
|
||||
assert!(0f64 != summary.median);
|
||||
let mips = (instructions * (ns_per_s / summary.median as u64)) / one_million;
|
||||
println!(" {:?} MIPS", mips);
|
||||
println!("{{ \"type\": \"bench\", \"name\": \"bench_program_alu_jit_to_native_mips\", \"median\": {:?}, \"deviation\": 0 }}", mips);
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
extern crate walkdir;
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn rerun_if_changed(files: &[&str], directories: &[&str]) {
|
||||
let mut all_files: Vec<_> = files.iter().map(|f| f.to_string()).collect();
|
||||
|
||||
for directory in directories {
|
||||
let files_in_directory: Vec<_> = WalkDir::new(directory)
|
||||
.into_iter()
|
||||
.map(|entry| entry.unwrap())
|
||||
.filter(|entry| entry.file_type().is_file())
|
||||
.map(|f| f.path().to_str().unwrap().to_owned())
|
||||
.collect();
|
||||
all_files.extend_from_slice(&files_in_directory[..]);
|
||||
}
|
||||
|
||||
for file in all_files {
|
||||
if !Path::new(&file).is_file() {
|
||||
panic!("{} is not a file", file);
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let bpf_c = !env::var("CARGO_FEATURE_BPF_C").is_err();
|
||||
if bpf_c {
|
||||
let out_dir = "OUT_DIR=../../../target/".to_string()
|
||||
+ &env::var("PROFILE").unwrap()
|
||||
+ &"/bpf".to_string();
|
||||
|
||||
rerun_if_changed(
|
||||
&["../../sdk/bpf/bpf.ld", "../../sdk/bpf/bpf.mk", "c/makefile"],
|
||||
&["../../sdk/bpf/inc", "../../sdk/bpf/scripts", "c/src"],
|
||||
);
|
||||
|
||||
println!("cargo:warning=(not a warning) Compiling C-based BPF programs");
|
||||
let status = Command::new("make")
|
||||
.current_dir("c")
|
||||
.arg("programs")
|
||||
.arg(&out_dir)
|
||||
.status()
|
||||
.expect("Failed to build C-based BPF programs");
|
||||
assert!(status.success());
|
||||
}
|
||||
|
||||
let bpf_rust = !env::var("CARGO_FEATURE_BPF_RUST").is_err();
|
||||
if bpf_rust {
|
||||
let install_dir =
|
||||
"../../../../target/".to_string() + &env::var("PROFILE").unwrap() + &"/bpf".to_string();
|
||||
|
||||
if !Path::new("rust/noop/target/bpfel-unknown-unknown/release/solana_bpf_rust_noop.so")
|
||||
.is_file()
|
||||
{
|
||||
// Cannot build Rust BPF programs as part of main build because
|
||||
// to build it requires calling Cargo with different parameters which
|
||||
// would deadlock due to recursive cargo calls
|
||||
panic!(
|
||||
"solana_bpf_rust_noop.so not found, you must manually run \
|
||||
`programs/bpf/rust/noop/build.sh` to build it"
|
||||
);
|
||||
}
|
||||
|
||||
rerun_if_changed(
|
||||
&[
|
||||
"rust/noop/bpf.ld",
|
||||
"rust/noop/build.sh",
|
||||
"rust/noop/Cargo.toml",
|
||||
"rust/noop/target/bpfel-unknown-unknown/release/solana_bpf_rust_noop.so",
|
||||
],
|
||||
&["rust/noop/src"],
|
||||
);
|
||||
|
||||
println!(
|
||||
"cargo:warning=(not a warning) Installing Rust-based BPF program: solana_bpf_rust_noop"
|
||||
);
|
||||
let status = Command::new("mkdir")
|
||||
.current_dir("rust/noop")
|
||||
.arg("-p")
|
||||
.arg(&install_dir)
|
||||
.status()
|
||||
.expect("Unable to create BPF install directory");
|
||||
assert!(status.success());
|
||||
|
||||
let status = Command::new("cp")
|
||||
.current_dir("rust/noop")
|
||||
.arg("target/bpfel-unknown-unknown/release/solana_bpf_rust_noop.so")
|
||||
.arg(&install_dir)
|
||||
.status()
|
||||
.expect("Failed to copy solana_rust_bpf_noop.so to install directory");
|
||||
assert!(status.success());
|
||||
}
|
||||
}
|
1
programs/bpf/c/.gitignore
vendored
1
programs/bpf/c/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/out/
|
@ -1,2 +0,0 @@
|
||||
BPF_SDK := ../../../sdk/bpf
|
||||
include $(BPF_SDK)/bpf.mk
|
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* @brief Benchmark program that does work
|
||||
*
|
||||
* Counts Armstrong Numbers between 1 and x
|
||||
*/
|
||||
|
||||
#include <solana_sdk.h>
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
uint64_t x = *(uint64_t *) input;
|
||||
uint64_t *result = (uint64_t *) input + 1;
|
||||
uint64_t count = 0;
|
||||
|
||||
for (int i = 1; i <= x; i++) {
|
||||
uint64_t temp = i;
|
||||
uint64_t num = 0;
|
||||
while (temp != 0) {
|
||||
uint64_t rem = (temp % 10);
|
||||
num += rem * rem * rem;
|
||||
temp /= 10;
|
||||
}
|
||||
if (i == num) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
sol_log_64(x, count, 0, 0, 0);
|
||||
*result = count;
|
||||
return true;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#include <criterion/criterion.h>
|
||||
#include "bench_alu.c"
|
||||
|
||||
Test(bench_alu, sanity) {
|
||||
uint64_t input[] = {500, 0};
|
||||
|
||||
cr_assert(entrypoint((uint8_t *) input));
|
||||
|
||||
cr_assert_eq(input[0], 500);
|
||||
cr_assert_eq(input[1], 5);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* @brief Example C-based BPF program that prints out the parameters
|
||||
* passed to it
|
||||
*/
|
||||
#include <solana_sdk.h>
|
||||
|
||||
#include "helper.h"
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
sol_log(__FILE__);
|
||||
helper_function();
|
||||
sol_log(__FILE__);
|
||||
return true;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* @brief Example C-based BPF program that prints out the parameters
|
||||
* passed to it
|
||||
*/
|
||||
#include <solana_sdk.h>
|
||||
|
||||
#include "helper.h"
|
||||
|
||||
void helper_function(void) {
|
||||
sol_log(__FILE__);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* @brief Example C-based BPF program that prints out the parameters
|
||||
* passed to it
|
||||
*/
|
||||
#include <solana_sdk.h>
|
||||
|
||||
void helper_function(void);
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* @brief Example C-based BPF program that moves funds from one account to
|
||||
* another
|
||||
*/
|
||||
|
||||
#include <solana_sdk.h>
|
||||
|
||||
/**
|
||||
* Number of SolKeyedAccount expected. The program should bail if an
|
||||
* unexpected number of accounts are passed to the program's entrypoint
|
||||
*/
|
||||
#define NUM_KA 3
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
SolKeyedAccount ka[NUM_KA];
|
||||
SolParameters params = (SolParameters) { .ka = ka };
|
||||
|
||||
if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!params.ka[0].is_signer) {
|
||||
sol_log("Transaction not signed by key 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t lamports = *(int64_t *)params.data;
|
||||
if (*params.ka[0].lamports >= lamports) {
|
||||
*params.ka[0].lamports -= lamports;
|
||||
*params.ka[2].lamports += lamports;
|
||||
// sol_log_64(0, 0, *ka[0].lamports, *ka[2].lamports, lamports);
|
||||
} else {
|
||||
// sol_log_64(0, 0, 0xFF, *ka[0].lamports, lamports);
|
||||
}
|
||||
return true;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
#include <solana_sdk.h>
|
||||
|
||||
static const char msg[] = "This is a message";
|
||||
static const char msg2[] = "This is a different message";
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
sol_log((char*)msg);
|
||||
sol_log((char*)msg2);
|
||||
return true;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @brief Example C++-based BPF program that prints out the parameters
|
||||
* passed to it
|
||||
*/
|
||||
#include <solana_sdk.h>
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
SolKeyedAccount ka[1];
|
||||
SolParameters params = (SolParameters) { .ka = ka };
|
||||
|
||||
sol_log(__FILE__);
|
||||
|
||||
if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log the provided input parameters. 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_params(¶ms);
|
||||
return true;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* @brief Example C-based BPF program that prints out the parameters
|
||||
* passed to it
|
||||
*/
|
||||
#include <solana_sdk.h>
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
SolKeyedAccount ka[1];
|
||||
SolParameters params = (SolParameters) { .ka = ka };
|
||||
|
||||
sol_log(__FILE__);
|
||||
|
||||
if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log the provided input parameters. 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_params(¶ms);
|
||||
return true;
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* @brief test program that generates BPF PC relative call instructions
|
||||
*/
|
||||
|
||||
#include <solana_sdk.h>
|
||||
|
||||
void __attribute__ ((noinline)) helper() {
|
||||
sol_log(__func__);
|
||||
}
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
sol_log(__func__);
|
||||
helper();
|
||||
return true;
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
#include <solana_sdk.h>
|
||||
|
||||
struct foo {const uint8_t *input;};
|
||||
void foo(const uint8_t *input, struct foo foo) ;
|
||||
|
||||
extern bool entrypoint(const uint8_t *input) {
|
||||
struct foo f;
|
||||
f.input = input;
|
||||
foo(input, f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void foo(const uint8_t *input, struct foo foo) {
|
||||
sol_log_64(0, 0, 0, (uint64_t)input, (uint64_t)foo.input);
|
||||
sol_assert(input == foo.input);
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
#include <solana_sdk.h>
|
||||
|
||||
struct test_struct { uint64_t x; uint64_t y; uint64_t z;};
|
||||
|
||||
static struct test_struct __attribute__ ((noinline)) test_function(void) {
|
||||
struct test_struct s;
|
||||
s.x = 3;
|
||||
s.y = 4;
|
||||
s.z = 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
extern bool entrypoint(const uint8_t* input) {
|
||||
struct test_struct s = test_function();
|
||||
sol_log("foobar");
|
||||
if (s.x + s.y + s.z == 12 ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
3
programs/bpf/rust/noop/.gitignore
vendored
3
programs/bpf/rust/noop/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/target/
|
||||
|
||||
Cargo.lock
|
@ -1,25 +0,0 @@
|
||||
|
||||
# Note: This crate must be built using build.sh
|
||||
|
||||
[package]
|
||||
name = "solana-bpf-rust-noop"
|
||||
version = "0.14.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"]
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
[dependencies.compiler_builtins]
|
||||
path = "../../../../sdk/bpf/rust-bpf-sysroot/src/compiler-builtins"
|
@ -1,19 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
cargo install xargo
|
||||
|
||||
set -e
|
||||
|
||||
# Ensure the sdk is installed
|
||||
../../../../sdk/bpf/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=../../../../sdk/bpf/llvm-native/bin/ld.lld"
|
||||
export XARGO_HOME="$PWD/target/xargo"
|
||||
export XARGO_RUST_SRC="../../../../sdk/bpf/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; }
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cargo clean
|
@ -1,12 +0,0 @@
|
||||
#!/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
|
@ -1,55 +0,0 @@
|
||||
//! @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
|
||||
}
|
@ -1,422 +0,0 @@
|
||||
//! @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);
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
#[cfg(any(feature = "bpf_c", feature = "bpf_rust"))]
|
||||
mod bpf {
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_runtime::loader_utils::{create_invoke_instruction, load_program};
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::native_loader;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// BPF program file extension
|
||||
const PLATFORM_FILE_EXTENSION_BPF: &str = "so";
|
||||
|
||||
/// Create a BPF program file name
|
||||
fn create_bpf_path(name: &str) -> PathBuf {
|
||||
let mut pathbuf = {
|
||||
let current_exe = env::current_exe().unwrap();
|
||||
PathBuf::from(current_exe.parent().unwrap().parent().unwrap())
|
||||
};
|
||||
pathbuf.push("bpf/");
|
||||
pathbuf.push(name);
|
||||
pathbuf.set_extension(PLATFORM_FILE_EXTENSION_BPF);
|
||||
pathbuf
|
||||
}
|
||||
|
||||
#[cfg(feature = "bpf_c")]
|
||||
mod bpf_c {
|
||||
use super::*;
|
||||
use solana_sdk::bpf_loader;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn test_program_bpf_c_noop() {
|
||||
solana_logger::setup();
|
||||
|
||||
let mut file = File::open(create_bpf_path("noop")).expect("file open failed");
|
||||
let mut elf = Vec::new();
|
||||
file.read_to_end(&mut elf).unwrap();
|
||||
|
||||
let (genesis_block, alice_keypair) = GenesisBlock::new(50);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
// Call user program
|
||||
let program_id = load_program(&bank_client, &alice_keypair, &bpf_loader::id(), elf);
|
||||
let instruction = create_invoke_instruction(alice_keypair.pubkey(), program_id, &1u8);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_program_bpf_c() {
|
||||
solana_logger::setup();
|
||||
|
||||
let programs = [
|
||||
"bpf_to_bpf",
|
||||
"multiple_static",
|
||||
"noop",
|
||||
"noop++",
|
||||
"relative_call",
|
||||
"struct_pass",
|
||||
"struct_ret",
|
||||
];
|
||||
for program in programs.iter() {
|
||||
println!("Test program: {:?}", program);
|
||||
let mut file = File::open(create_bpf_path(program)).expect("file open failed");
|
||||
let mut elf = Vec::new();
|
||||
file.read_to_end(&mut elf).unwrap();
|
||||
|
||||
let (genesis_block, alice_keypair) = GenesisBlock::new(50);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let loader_id = load_program(
|
||||
&bank_client,
|
||||
&alice_keypair,
|
||||
&native_loader::id(),
|
||||
"solana_bpf_loader".as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
// Call user program
|
||||
let program_id = load_program(&bank_client, &alice_keypair, &loader_id, elf);
|
||||
let instruction =
|
||||
create_invoke_instruction(alice_keypair.pubkey(), program_id, &1u8);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot currently build the Rust BPF program as part
|
||||
// of the rest of the build due to recursive `cargo build` causing
|
||||
// a build deadlock. Therefore you must build the Rust programs
|
||||
// yourself first by calling `make all` in the Rust BPF program's directory
|
||||
#[cfg(feature = "bpf_rust")]
|
||||
mod bpf_rust {
|
||||
use super::*;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn test_program_bpf_rust() {
|
||||
solana_logger::setup();
|
||||
|
||||
let programs = ["solana_bpf_rust_noop"];
|
||||
for program in programs.iter() {
|
||||
let filename = create_bpf_path(program);
|
||||
println!("Test program: {:?} from {:?}", program, filename);
|
||||
let mut file = File::open(filename).unwrap();
|
||||
let mut elf = Vec::new();
|
||||
file.read_to_end(&mut elf).unwrap();
|
||||
|
||||
let (genesis_block, alice_keypair) = GenesisBlock::new(50);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let loader_id = load_program(
|
||||
&bank_client,
|
||||
&alice_keypair,
|
||||
&native_loader::id(),
|
||||
"solana_bpf_loader".as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
// Call user program
|
||||
let program_id = load_program(&bank_client, &alice_keypair, &loader_id, elf);
|
||||
let instruction =
|
||||
create_invoke_instruction(alice_keypair.pubkey(), program_id, &1u8);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "solana-bpfloader"
|
||||
version = "0.14.0"
|
||||
description = "Solana BPF Loader"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
byteorder = "1.3.1"
|
||||
libc = "0.2.51"
|
||||
log = "0.4.2"
|
||||
solana_rbpf = "=0.1.10"
|
||||
serde = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_bpf_loader"
|
||||
crate-type = ["lib", "cdylib"]
|
@ -1,324 +0,0 @@
|
||||
use solana_rbpf::ebpf;
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
fn reject<S: AsRef<str>>(msg: S) -> Result<(), Error> {
|
||||
let full_msg = format!("[Verifier] Error: {}", msg.as_ref());
|
||||
Err(Error::new(ErrorKind::Other, full_msg))
|
||||
}
|
||||
|
||||
fn check_prog_len(prog: &[u8]) -> Result<(), Error> {
|
||||
if prog.len() % ebpf::INSN_SIZE != 0 {
|
||||
reject(format!(
|
||||
"eBPF program length must be a multiple of {:?} octets",
|
||||
ebpf::INSN_SIZE
|
||||
))?;
|
||||
}
|
||||
if prog.len() > ebpf::PROG_MAX_SIZE {
|
||||
reject(format!(
|
||||
"eBPF program length limited to {:?}, here {:?}",
|
||||
ebpf::PROG_MAX_INSNS,
|
||||
prog.len() / ebpf::INSN_SIZE
|
||||
))?;
|
||||
}
|
||||
|
||||
if prog.is_empty() {
|
||||
reject("No program set, call prog_set() to load one".to_string())?;
|
||||
}
|
||||
|
||||
// TODO BPF program may deterministically exit even if the last
|
||||
// instruction in the block is not an exit (might be earlier and jumped to)
|
||||
// TODO need to validate more intelligently
|
||||
// let last_insn = ebpf::get_insn(prog, (prog.len() / ebpf::INSN_SIZE) - 1);
|
||||
// if last_insn.opc != ebpf::EXIT {
|
||||
// reject("program does not end with “EXIT” instruction".to_string())?;
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_imm_nonzero(insn: &ebpf::Insn, insn_ptr: usize) -> Result<(), Error> {
|
||||
if insn.imm == 0 {
|
||||
reject(format!("division by 0 (insn #{:?})", insn_ptr))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_imm_endian(insn: &ebpf::Insn, insn_ptr: usize) -> Result<(), Error> {
|
||||
match insn.imm {
|
||||
16 | 32 | 64 => Ok(()),
|
||||
_ => reject(format!(
|
||||
"unsupported argument for LE/BE (insn #{:?})",
|
||||
insn_ptr
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_load_dw(prog: &[u8], insn_ptr: usize) -> Result<(), Error> {
|
||||
// We know we can reach next insn since we enforce an EXIT insn at the end of program, while
|
||||
// this function should be called only for LD_DW insn, that cannot be last in program.
|
||||
let next_insn = ebpf::get_insn(prog, insn_ptr + 1);
|
||||
if next_insn.opc != 0 {
|
||||
reject(format!(
|
||||
"incomplete LD_DW instruction (insn #{:?})",
|
||||
insn_ptr
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_jmp_offset(prog: &[u8], insn_ptr: usize) -> Result<(), Error> {
|
||||
let insn = ebpf::get_insn(prog, insn_ptr);
|
||||
if insn.off == -1 {
|
||||
reject(format!("infinite loop (insn #{:?})", insn_ptr))?;
|
||||
}
|
||||
|
||||
let dst_insn_ptr = insn_ptr as isize + 1 + insn.off as isize;
|
||||
if dst_insn_ptr < 0 || dst_insn_ptr as usize >= (prog.len() / ebpf::INSN_SIZE) {
|
||||
reject(format!(
|
||||
"jump out of code to #{:?} (insn #{:?})",
|
||||
dst_insn_ptr, insn_ptr
|
||||
))?;
|
||||
}
|
||||
|
||||
let dst_insn = ebpf::get_insn(prog, dst_insn_ptr as usize);
|
||||
if dst_insn.opc == 0 {
|
||||
reject(format!(
|
||||
"jump to middle of LD_DW at #{:?} (insn #{:?})",
|
||||
dst_insn_ptr, insn_ptr
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_registers(insn: &ebpf::Insn, store: bool, insn_ptr: usize) -> Result<(), Error> {
|
||||
if insn.src > 10 {
|
||||
reject(format!("invalid source register (insn #{:?})", insn_ptr))?;
|
||||
}
|
||||
|
||||
match (insn.dst, store) {
|
||||
(0...9, _) | (10, true) => Ok(()),
|
||||
(10, false) => reject(format!(
|
||||
"cannot write into register r10 (insn #{:?})",
|
||||
insn_ptr
|
||||
)),
|
||||
(_, _) => reject(format!(
|
||||
"invalid destination register (insn #{:?})",
|
||||
insn_ptr
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(prog: &[u8]) -> Result<(), Error> {
|
||||
check_prog_len(prog)?;
|
||||
|
||||
let mut insn_ptr: usize = 0;
|
||||
while insn_ptr * ebpf::INSN_SIZE < prog.len() {
|
||||
let insn = ebpf::get_insn(prog, insn_ptr);
|
||||
let mut store = false;
|
||||
|
||||
match insn.opc {
|
||||
// BPF_LD class
|
||||
ebpf::LD_ABS_B => {}
|
||||
ebpf::LD_ABS_H => {}
|
||||
ebpf::LD_ABS_W => {}
|
||||
ebpf::LD_ABS_DW => {}
|
||||
ebpf::LD_IND_B => {}
|
||||
ebpf::LD_IND_H => {}
|
||||
ebpf::LD_IND_W => {}
|
||||
ebpf::LD_IND_DW => {}
|
||||
|
||||
ebpf::LD_DW_IMM => {
|
||||
store = true;
|
||||
check_load_dw(prog, insn_ptr)?;
|
||||
insn_ptr += 1;
|
||||
}
|
||||
|
||||
// BPF_LDX class
|
||||
ebpf::LD_B_REG => {}
|
||||
ebpf::LD_H_REG => {}
|
||||
ebpf::LD_W_REG => {}
|
||||
ebpf::LD_DW_REG => {}
|
||||
|
||||
// BPF_ST class
|
||||
ebpf::ST_B_IMM => store = true,
|
||||
ebpf::ST_H_IMM => store = true,
|
||||
ebpf::ST_W_IMM => store = true,
|
||||
ebpf::ST_DW_IMM => store = true,
|
||||
|
||||
// BPF_STX class
|
||||
ebpf::ST_B_REG => store = true,
|
||||
ebpf::ST_H_REG => store = true,
|
||||
ebpf::ST_W_REG => store = true,
|
||||
ebpf::ST_DW_REG => store = true,
|
||||
ebpf::ST_W_XADD => {
|
||||
unimplemented!();
|
||||
}
|
||||
ebpf::ST_DW_XADD => {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
// BPF_ALU class
|
||||
ebpf::ADD32_IMM => {}
|
||||
ebpf::ADD32_REG => {}
|
||||
ebpf::SUB32_IMM => {}
|
||||
ebpf::SUB32_REG => {}
|
||||
ebpf::MUL32_IMM => {}
|
||||
ebpf::MUL32_REG => {}
|
||||
ebpf::DIV32_IMM => {
|
||||
check_imm_nonzero(&insn, insn_ptr)?;
|
||||
}
|
||||
ebpf::DIV32_REG => {}
|
||||
ebpf::OR32_IMM => {}
|
||||
ebpf::OR32_REG => {}
|
||||
ebpf::AND32_IMM => {}
|
||||
ebpf::AND32_REG => {}
|
||||
ebpf::LSH32_IMM => {}
|
||||
ebpf::LSH32_REG => {}
|
||||
ebpf::RSH32_IMM => {}
|
||||
ebpf::RSH32_REG => {}
|
||||
ebpf::NEG32 => {}
|
||||
ebpf::MOD32_IMM => {
|
||||
check_imm_nonzero(&insn, insn_ptr)?;
|
||||
}
|
||||
ebpf::MOD32_REG => {}
|
||||
ebpf::XOR32_IMM => {}
|
||||
ebpf::XOR32_REG => {}
|
||||
ebpf::MOV32_IMM => {}
|
||||
ebpf::MOV32_REG => {}
|
||||
ebpf::ARSH32_IMM => {}
|
||||
ebpf::ARSH32_REG => {}
|
||||
ebpf::LE => {
|
||||
check_imm_endian(&insn, insn_ptr)?;
|
||||
}
|
||||
ebpf::BE => {
|
||||
check_imm_endian(&insn, insn_ptr)?;
|
||||
}
|
||||
|
||||
// BPF_ALU64 class
|
||||
ebpf::ADD64_IMM => {}
|
||||
ebpf::ADD64_REG => {}
|
||||
ebpf::SUB64_IMM => {}
|
||||
ebpf::SUB64_REG => {}
|
||||
ebpf::MUL64_IMM => {
|
||||
check_imm_nonzero(&insn, insn_ptr)?;
|
||||
}
|
||||
ebpf::MUL64_REG => {}
|
||||
ebpf::DIV64_IMM => {
|
||||
check_imm_nonzero(&insn, insn_ptr)?;
|
||||
}
|
||||
ebpf::DIV64_REG => {}
|
||||
ebpf::OR64_IMM => {}
|
||||
ebpf::OR64_REG => {}
|
||||
ebpf::AND64_IMM => {}
|
||||
ebpf::AND64_REG => {}
|
||||
ebpf::LSH64_IMM => {}
|
||||
ebpf::LSH64_REG => {}
|
||||
ebpf::RSH64_IMM => {}
|
||||
ebpf::RSH64_REG => {}
|
||||
ebpf::NEG64 => {}
|
||||
ebpf::MOD64_IMM => {}
|
||||
ebpf::MOD64_REG => {}
|
||||
ebpf::XOR64_IMM => {}
|
||||
ebpf::XOR64_REG => {}
|
||||
ebpf::MOV64_IMM => {}
|
||||
ebpf::MOV64_REG => {}
|
||||
ebpf::ARSH64_IMM => {}
|
||||
ebpf::ARSH64_REG => {}
|
||||
|
||||
// BPF_JMP class
|
||||
ebpf::JA => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JEQ_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JEQ_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JGT_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JGT_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JGE_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JGE_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JLT_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JLT_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JLE_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JLE_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSET_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSET_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JNE_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JNE_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSGT_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSGT_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSGE_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSGE_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSLT_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSLT_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSLE_IMM => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::JSLE_REG => {
|
||||
check_jmp_offset(prog, insn_ptr)?;
|
||||
}
|
||||
ebpf::CALL_IMM => {}
|
||||
ebpf::CALL_REG => {}
|
||||
ebpf::EXIT => {}
|
||||
|
||||
_ => {
|
||||
reject(format!(
|
||||
"unknown eBPF opcode {:#2x} (insn #{:?})",
|
||||
insn.opc, insn_ptr
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
check_registers(&insn, store, insn_ptr)?;
|
||||
|
||||
insn_ptr += 1;
|
||||
}
|
||||
|
||||
// insn_ptr should now be equal to number of instructions.
|
||||
if insn_ptr != prog.len() / ebpf::INSN_SIZE {
|
||||
reject(format!("jumped out of code to #{:?}", insn_ptr))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
pub mod bpf_verifier;
|
||||
|
||||
use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
|
||||
use libc::c_char;
|
||||
use log::*;
|
||||
use solana_rbpf::{EbpfVmRaw, MemoryRegion};
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::loader_instruction::LoaderInstruction;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::solana_entrypoint;
|
||||
use std::ffi::CStr;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::mem;
|
||||
|
||||
// TODO use rbpf's disassemble
|
||||
#[allow(dead_code)]
|
||||
fn dump_program(key: &Pubkey, prog: &[u8]) {
|
||||
let mut eight_bytes: Vec<u8> = Vec::new();
|
||||
info!("BPF Program: {:?}", key);
|
||||
for i in prog.iter() {
|
||||
if eight_bytes.len() >= 7 {
|
||||
info!("{:02X?}", eight_bytes);
|
||||
eight_bytes.clear();
|
||||
} else {
|
||||
eight_bytes.push(i.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn helper_abort_verify(
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_arg5: u64,
|
||||
_ro_regions: &[MemoryRegion],
|
||||
_rw_regions: &[MemoryRegion],
|
||||
) -> Result<(()), Error> {
|
||||
Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Error: BPF program called abort()!",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn helper_abort(_arg1: u64, _arg2: u64, _arg3: u64, _arg4: u64, _arg5: u64) -> u64 {
|
||||
// Never called because its verify function always returns an error
|
||||
0
|
||||
}
|
||||
|
||||
pub fn helper_sol_panic_verify(
|
||||
_arg1: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_arg5: u64,
|
||||
_ro_regions: &[MemoryRegion],
|
||||
_rw_regions: &[MemoryRegion],
|
||||
) -> Result<(()), Error> {
|
||||
Err(Error::new(ErrorKind::Other, "Error: BPF program Panic!"))
|
||||
}
|
||||
|
||||
pub fn helper_sol_panic(_arg1: u64, _arg2: u64, _arg3: u64, _arg4: u64, _arg5: u64) -> u64 {
|
||||
// Never called because its verify function always returns an error
|
||||
0
|
||||
}
|
||||
|
||||
pub fn helper_sol_log_verify(
|
||||
addr: u64,
|
||||
_arg2: u64,
|
||||
_arg3: u64,
|
||||
_arg4: u64,
|
||||
_arg5: u64,
|
||||
ro_regions: &[MemoryRegion],
|
||||
_rw_regions: &[MemoryRegion],
|
||||
) -> Result<(()), Error> {
|
||||
for region in ro_regions.iter() {
|
||||
if region.addr <= addr && (addr as u64) < region.addr + region.len {
|
||||
let c_buf: *const c_char = addr as *const c_char;
|
||||
let max_size = region.addr + region.len - addr;
|
||||
unsafe {
|
||||
for i in 0..max_size {
|
||||
if std::ptr::read(c_buf.offset(i as isize)) == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(Error::new(ErrorKind::Other, "Error, Unterminated string"));
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Error: Load segfault, bad string pointer",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn helper_sol_log(addr: u64, _arg2: u64, _arg3: u64, _arg4: u64, _arg5: u64) -> u64 {
|
||||
let c_buf: *const c_char = addr as *const c_char;
|
||||
let c_str: &CStr = unsafe { CStr::from_ptr(c_buf) };
|
||||
match c_str.to_str() {
|
||||
Ok(slice) => info!("sol_log: {:?}", slice),
|
||||
Err(e) => warn!("Error: Cannot print invalid string: {}", e),
|
||||
};
|
||||
0
|
||||
}
|
||||
|
||||
pub fn helper_sol_log_u64(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> u64 {
|
||||
info!(
|
||||
"sol_log_u64: {:#x}, {:#x}, {:#x}, {:#x}, {:#x}",
|
||||
arg1, arg2, arg3, arg4, arg5
|
||||
);
|
||||
0
|
||||
}
|
||||
|
||||
pub fn create_vm(prog: &[u8]) -> Result<EbpfVmRaw, Error> {
|
||||
let mut vm = EbpfVmRaw::new(None)?;
|
||||
vm.set_verifier(bpf_verifier::check)?;
|
||||
vm.set_max_instruction_count(36000)?; // TODO 36000 is a wag, need to tune
|
||||
vm.set_elf(&prog)?;
|
||||
vm.register_helper_ex("abort", Some(helper_abort_verify), helper_abort)?;
|
||||
vm.register_helper_ex("sol_panic", Some(helper_sol_panic_verify), helper_sol_panic)?;
|
||||
vm.register_helper_ex(
|
||||
"sol_panic_",
|
||||
Some(helper_sol_panic_verify),
|
||||
helper_sol_panic,
|
||||
)?;
|
||||
vm.register_helper_ex("sol_log", Some(helper_sol_log_verify), helper_sol_log)?;
|
||||
vm.register_helper_ex("sol_log_", Some(helper_sol_log_verify), helper_sol_log)?;
|
||||
vm.register_helper_ex("sol_log_64", None, helper_sol_log_u64)?;
|
||||
vm.register_helper_ex("sol_log_64_", None, helper_sol_log_u64)?;
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
fn serialize_parameters(
|
||||
program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
tick_height: u64,
|
||||
) -> Vec<u8> {
|
||||
assert_eq!(32, mem::size_of::<Pubkey>());
|
||||
|
||||
let mut v: Vec<u8> = Vec::new();
|
||||
v.write_u64::<LittleEndian>(keyed_accounts.len() as u64)
|
||||
.unwrap();
|
||||
for info in keyed_accounts.iter_mut() {
|
||||
v.write_u64::<LittleEndian>(info.signer_key().is_some() as u64)
|
||||
.unwrap();
|
||||
v.write_all(info.unsigned_key().as_ref()).unwrap();
|
||||
v.write_u64::<LittleEndian>(info.account.lamports).unwrap();
|
||||
v.write_u64::<LittleEndian>(info.account.data.len() as u64)
|
||||
.unwrap();
|
||||
v.write_all(&info.account.data).unwrap();
|
||||
v.write_all(info.account.owner.as_ref()).unwrap();
|
||||
}
|
||||
v.write_u64::<LittleEndian>(data.len() as u64).unwrap();
|
||||
v.write_all(data).unwrap();
|
||||
v.write_u64::<LittleEndian>(tick_height).unwrap();
|
||||
v.write_all(program_id.as_ref()).unwrap();
|
||||
v
|
||||
}
|
||||
|
||||
fn deserialize_parameters(keyed_accounts: &mut [KeyedAccount], buffer: &[u8]) {
|
||||
assert_eq!(32, mem::size_of::<Pubkey>());
|
||||
|
||||
let mut start = mem::size_of::<u64>();
|
||||
for info in keyed_accounts.iter_mut() {
|
||||
start += mem::size_of::<u64>(); // skip signer_key boolean
|
||||
start += mem::size_of::<Pubkey>(); // skip pubkey
|
||||
info.account.lamports = LittleEndian::read_u64(&buffer[start..]);
|
||||
|
||||
start += mem::size_of::<u64>() // skip lamports
|
||||
+ mem::size_of::<u64>(); // skip length tag
|
||||
let end = start + info.account.data.len();
|
||||
info.account.data.clone_from_slice(&buffer[start..end]);
|
||||
|
||||
start += info.account.data.len() // skip data
|
||||
+ mem::size_of::<Pubkey>(); // skip owner
|
||||
}
|
||||
}
|
||||
|
||||
solana_entrypoint!(entrypoint);
|
||||
fn entrypoint(
|
||||
program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
tx_data: &[u8],
|
||||
tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
if keyed_accounts[0].account.executable {
|
||||
let (progs, params) = keyed_accounts.split_at_mut(1);
|
||||
let prog = &progs[0].account.data;
|
||||
info!("Call BPF program");
|
||||
//dump_program(keyed_accounts[0].key, prog);
|
||||
let mut vm = match create_vm(prog) {
|
||||
Ok(vm) => vm,
|
||||
Err(e) => {
|
||||
warn!("Failed to create BPF VM: {}", e);
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
};
|
||||
let mut v = serialize_parameters(program_id, params, &tx_data, tick_height);
|
||||
match vm.execute_program(v.as_mut_slice()) {
|
||||
Ok(status) => {
|
||||
if 0 == status {
|
||||
warn!("BPF program failed: {}", status);
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("BPF VM failed to run program: {}", e);
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
}
|
||||
deserialize_parameters(params, &v);
|
||||
info!(
|
||||
"BPF program executed {} instructions",
|
||||
vm.get_last_instruction_count()
|
||||
);
|
||||
} else if let Ok(instruction) = bincode::deserialize(tx_data) {
|
||||
if keyed_accounts[0].signer_key().is_none() {
|
||||
warn!("key[0] did not sign the transaction");
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
match instruction {
|
||||
LoaderInstruction::Write { offset, bytes } => {
|
||||
let offset = offset as usize;
|
||||
let len = bytes.len();
|
||||
debug!("Write: offset={} length={}", offset, len);
|
||||
if keyed_accounts[0].account.data.len() < offset + len {
|
||||
warn!(
|
||||
"Write overflow: {} < {}",
|
||||
keyed_accounts[0].account.data.len(),
|
||||
offset + len
|
||||
);
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
keyed_accounts[0].account.data[offset..offset + len].copy_from_slice(&bytes);
|
||||
}
|
||||
LoaderInstruction::Finalize => {
|
||||
keyed_accounts[0].account.executable = true;
|
||||
info!(
|
||||
"Finalize: account {:?}",
|
||||
keyed_accounts[0].signer_key().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Invalid program transaction: {:?}", tx_data);
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Error: Execution exceeded maximum number of instructions")]
|
||||
fn test_non_terminating_program() {
|
||||
#[rustfmt::skip]
|
||||
let prog = &[
|
||||
0x07, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // r6 + 1
|
||||
0x05, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, // goto -2
|
||||
0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit
|
||||
];
|
||||
let input = &mut [0x00];
|
||||
|
||||
let mut vm = EbpfVmRaw::new(None).unwrap();
|
||||
vm.set_verifier(bpf_verifier::check).unwrap();
|
||||
vm.set_max_instruction_count(10).unwrap();
|
||||
vm.set_program(prog).unwrap();
|
||||
vm.execute_program(input).unwrap();
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "solana-budget-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Budget program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
chrono = { version = "0.4.0", features = ["serde"] }
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_budget_api"
|
||||
crate-type = ["lib"]
|
@ -1,341 +0,0 @@
|
||||
//! The `budget_expr` module provides a domain-specific language for pa&yment plans. Users create BudgetExpr objects that
|
||||
//! are given to an interpreter. The interpreter listens for `Witness` transactions,
|
||||
//! which it uses to reduce the payment plan. When the budget is reduced to a
|
||||
//! `Payment`, the payment is executed.
|
||||
|
||||
use chrono::prelude::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::mem;
|
||||
|
||||
/// The types of events a payment plan can process.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Witness {
|
||||
/// The current time.
|
||||
Timestamp(DateTime<Utc>),
|
||||
|
||||
/// A signature from Pubkey.
|
||||
Signature,
|
||||
}
|
||||
|
||||
/// Some amount of lamports that should be sent to the `to` `Pubkey`.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Payment {
|
||||
/// Amount to be paid.
|
||||
pub lamports: u64,
|
||||
|
||||
/// The `Pubkey` that `lamports` should be paid to.
|
||||
pub to: Pubkey,
|
||||
}
|
||||
|
||||
/// A data type representing a `Witness` that the payment plan is waiting on.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Condition {
|
||||
/// Wait for a `Timestamp` `Witness` at or after the given `DateTime`.
|
||||
Timestamp(DateTime<Utc>, Pubkey),
|
||||
|
||||
/// Wait for a `Signature` `Witness` from `Pubkey`.
|
||||
Signature(Pubkey),
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
/// Return true if the given Witness satisfies this Condition.
|
||||
pub fn is_satisfied(&self, witness: &Witness, from: &Pubkey) -> bool {
|
||||
match (self, witness) {
|
||||
(Condition::Signature(pubkey), Witness::Signature) => pubkey == from,
|
||||
(Condition::Timestamp(dt, pubkey), Witness::Timestamp(last_time)) => {
|
||||
pubkey == from && dt <= last_time
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A data type representing a payment plan.
|
||||
#[repr(C)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum BudgetExpr {
|
||||
/// Make a payment.
|
||||
Pay(Payment),
|
||||
|
||||
/// Make a payment after some condition.
|
||||
After(Condition, Box<BudgetExpr>),
|
||||
|
||||
/// Either make a payment after one condition or a different payment after another
|
||||
/// condition, which ever condition is satisfied first.
|
||||
Or((Condition, Box<BudgetExpr>), (Condition, Box<BudgetExpr>)),
|
||||
|
||||
/// Make a payment after both of two conditions are satisfied
|
||||
And(Condition, Condition, Box<BudgetExpr>),
|
||||
}
|
||||
|
||||
impl BudgetExpr {
|
||||
/// Create the simplest budget - one that pays `lamports` to Pubkey.
|
||||
pub fn new_payment(lamports: u64, to: &Pubkey) -> Self {
|
||||
BudgetExpr::Pay(Payment { lamports, to: *to })
|
||||
}
|
||||
|
||||
/// Create a budget that pays `lamports` to `to` after being witnessed by `from`.
|
||||
pub fn new_authorized_payment(from: &Pubkey, lamports: u64, to: &Pubkey) -> Self {
|
||||
BudgetExpr::After(
|
||||
Condition::Signature(*from),
|
||||
Box::new(Self::new_payment(lamports, to)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a budget that pays `lamports` to `to` after being witnessed by `witness` unless
|
||||
/// canceled with a signature from `from`.
|
||||
pub fn new_cancelable_authorized_payment(
|
||||
witness: &Pubkey,
|
||||
lamports: u64,
|
||||
to: &Pubkey,
|
||||
from: Option<Pubkey>,
|
||||
) -> Self {
|
||||
if from.is_none() {
|
||||
return Self::new_authorized_payment(witness, lamports, to);
|
||||
}
|
||||
let from = from.unwrap();
|
||||
BudgetExpr::Or(
|
||||
(
|
||||
Condition::Signature(*witness),
|
||||
Box::new(BudgetExpr::new_payment(lamports, to)),
|
||||
),
|
||||
(
|
||||
Condition::Signature(from),
|
||||
Box::new(BudgetExpr::new_payment(lamports, &from)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a budget that pays lamports` to `to` after being witnessed by 2x `from`s
|
||||
pub fn new_2_2_multisig_payment(
|
||||
from0: &Pubkey,
|
||||
from1: &Pubkey,
|
||||
lamports: u64,
|
||||
to: &Pubkey,
|
||||
) -> Self {
|
||||
BudgetExpr::And(
|
||||
Condition::Signature(*from0),
|
||||
Condition::Signature(*from1),
|
||||
Box::new(Self::new_payment(lamports, to)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a budget that pays `lamports` to `to` after the given DateTime signed
|
||||
/// by `dt_pubkey`.
|
||||
pub fn new_future_payment(
|
||||
dt: DateTime<Utc>,
|
||||
dt_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
to: &Pubkey,
|
||||
) -> Self {
|
||||
BudgetExpr::After(
|
||||
Condition::Timestamp(dt, *dt_pubkey),
|
||||
Box::new(Self::new_payment(lamports, to)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a budget that pays `lamports` to `to` after the given DateTime
|
||||
/// signed by `dt_pubkey` unless canceled by `from`.
|
||||
pub fn new_cancelable_future_payment(
|
||||
dt: DateTime<Utc>,
|
||||
dt_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
to: &Pubkey,
|
||||
from: Option<Pubkey>,
|
||||
) -> Self {
|
||||
if from.is_none() {
|
||||
return Self::new_future_payment(dt, dt_pubkey, lamports, to);
|
||||
}
|
||||
let from = from.unwrap();
|
||||
BudgetExpr::Or(
|
||||
(
|
||||
Condition::Timestamp(dt, *dt_pubkey),
|
||||
Box::new(Self::new_payment(lamports, to)),
|
||||
),
|
||||
(
|
||||
Condition::Signature(from),
|
||||
Box::new(Self::new_payment(lamports, &from)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return Payment if the budget requires no additional Witnesses.
|
||||
pub fn final_payment(&self) -> Option<Payment> {
|
||||
match self {
|
||||
BudgetExpr::Pay(payment) => Some(payment.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the budget spends exactly `spendable_lamports`.
|
||||
pub fn verify(&self, spendable_lamports: u64) -> bool {
|
||||
match self {
|
||||
BudgetExpr::Pay(payment) => payment.lamports == spendable_lamports,
|
||||
BudgetExpr::After(_, sub_expr) | BudgetExpr::And(_, _, sub_expr) => {
|
||||
sub_expr.verify(spendable_lamports)
|
||||
}
|
||||
BudgetExpr::Or(a, b) => {
|
||||
a.1.verify(spendable_lamports) && b.1.verify(spendable_lamports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a witness to the budget to see if the budget can be reduced.
|
||||
/// If so, modify the budget in-place.
|
||||
pub fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) {
|
||||
let new_expr = match self {
|
||||
BudgetExpr::After(cond, sub_expr) if cond.is_satisfied(witness, from) => {
|
||||
Some(sub_expr.clone())
|
||||
}
|
||||
BudgetExpr::Or((cond, sub_expr), _) if cond.is_satisfied(witness, from) => {
|
||||
Some(sub_expr.clone())
|
||||
}
|
||||
BudgetExpr::Or(_, (cond, sub_expr)) if cond.is_satisfied(witness, from) => {
|
||||
Some(sub_expr.clone())
|
||||
}
|
||||
BudgetExpr::And(cond0, cond1, sub_expr) => {
|
||||
if cond0.is_satisfied(witness, from) {
|
||||
Some(Box::new(BudgetExpr::After(cond1.clone(), sub_expr.clone())))
|
||||
} else if cond1.is_satisfied(witness, from) {
|
||||
Some(Box::new(BudgetExpr::After(cond0.clone(), sub_expr.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(expr) = new_expr {
|
||||
mem::replace(self, *expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_signature_satisfied() {
|
||||
let from = Pubkey::default();
|
||||
assert!(Condition::Signature(from).is_satisfied(&Witness::Signature, &from));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_satisfied() {
|
||||
let dt1 = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
|
||||
let dt2 = Utc.ymd(2014, 11, 14).and_hms(10, 9, 8);
|
||||
let from = Pubkey::default();
|
||||
assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt1), &from));
|
||||
assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt2), &from));
|
||||
assert!(!Condition::Timestamp(dt2, from).is_satisfied(&Witness::Timestamp(dt1), &from));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify() {
|
||||
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
|
||||
let from = Pubkey::default();
|
||||
let to = Pubkey::default();
|
||||
assert!(BudgetExpr::new_payment(42, &to).verify(42));
|
||||
assert!(BudgetExpr::new_authorized_payment(&from, 42, &to).verify(42));
|
||||
assert!(BudgetExpr::new_future_payment(dt, &from, 42, &to).verify(42));
|
||||
assert!(
|
||||
BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from)).verify(42)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorized_payment() {
|
||||
let from = Pubkey::default();
|
||||
let to = Pubkey::default();
|
||||
|
||||
let mut expr = BudgetExpr::new_authorized_payment(&from, 42, &to);
|
||||
expr.apply_witness(&Witness::Signature, &from);
|
||||
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_future_payment() {
|
||||
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
|
||||
let from = Pubkey::new_rand();
|
||||
let to = Pubkey::new_rand();
|
||||
|
||||
let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
|
||||
expr.apply_witness(&Witness::Timestamp(dt), &from);
|
||||
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unauthorized_future_payment() {
|
||||
// Ensure timestamp will only be acknowledged if it came from the
|
||||
// whitelisted public key.
|
||||
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
|
||||
let from = Pubkey::new_rand();
|
||||
let to = Pubkey::new_rand();
|
||||
|
||||
let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
|
||||
let orig_expr = expr.clone();
|
||||
expr.apply_witness(&Witness::Timestamp(dt), &to); // <-- Attack!
|
||||
assert_eq!(expr, orig_expr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancelable_future_payment() {
|
||||
let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
|
||||
let from = Pubkey::default();
|
||||
let to = Pubkey::default();
|
||||
|
||||
let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
|
||||
expr.apply_witness(&Witness::Timestamp(dt), &from);
|
||||
assert_eq!(expr, BudgetExpr::new_payment(42, &to));
|
||||
|
||||
let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
|
||||
expr.apply_witness(&Witness::Signature, &from);
|
||||
assert_eq!(expr, BudgetExpr::new_payment(42, &from));
|
||||
}
|
||||
#[test]
|
||||
fn test_2_2_multisig_payment() {
|
||||
let from0 = Pubkey::new_rand();
|
||||
let from1 = Pubkey::new_rand();
|
||||
let to = Pubkey::default();
|
||||
|
||||
let mut expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
|
||||
expr.apply_witness(&Witness::Signature, &from0);
|
||||
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multisig_after_sig() {
|
||||
let from0 = Pubkey::new_rand();
|
||||
let from1 = Pubkey::new_rand();
|
||||
let from2 = Pubkey::new_rand();
|
||||
let to = Pubkey::default();
|
||||
|
||||
let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
|
||||
let mut expr = BudgetExpr::After(Condition::Signature(from2), Box::new(expr));
|
||||
|
||||
expr.apply_witness(&Witness::Signature, &from2);
|
||||
expr.apply_witness(&Witness::Signature, &from0);
|
||||
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multisig_after_ts() {
|
||||
let from0 = Pubkey::new_rand();
|
||||
let from1 = Pubkey::new_rand();
|
||||
let dt = Utc.ymd(2014, 11, 11).and_hms(7, 7, 7);
|
||||
let to = Pubkey::default();
|
||||
|
||||
let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
|
||||
let mut expr = BudgetExpr::After(Condition::Timestamp(dt, from0), Box::new(expr));
|
||||
|
||||
expr.apply_witness(&Witness::Timestamp(dt), &from0);
|
||||
assert_eq!(
|
||||
expr,
|
||||
BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to)
|
||||
);
|
||||
|
||||
expr.apply_witness(&Witness::Signature, &from0);
|
||||
assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
|
||||
}
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
use crate::budget_expr::BudgetExpr;
|
||||
use crate::budget_state::BudgetState;
|
||||
use crate::id;
|
||||
use bincode::serialized_size;
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
/// A smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Contract {
|
||||
/// The number of lamports allocated to the `BudgetExpr` and any transaction fees.
|
||||
pub lamports: u64,
|
||||
pub budget_expr: BudgetExpr,
|
||||
}
|
||||
|
||||
/// An instruction to progress the smart contract.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum BudgetInstruction {
|
||||
/// Declare and instantiate `BudgetExpr`.
|
||||
InitializeAccount(BudgetExpr),
|
||||
|
||||
/// Tell a payment plan acknowledge the given `DateTime` has past.
|
||||
ApplyTimestamp(DateTime<Utc>),
|
||||
|
||||
/// Tell the budget that the `InitializeAccount` with `Signature` has been
|
||||
/// signed by the containing transaction's `Pubkey`.
|
||||
ApplySignature,
|
||||
}
|
||||
|
||||
fn initialize_account(contract: &Pubkey, expr: BudgetExpr) -> Instruction {
|
||||
let mut keys = vec![];
|
||||
if let BudgetExpr::Pay(payment) = &expr {
|
||||
keys.push(AccountMeta::new(payment.to, false));
|
||||
}
|
||||
keys.push(AccountMeta::new(*contract, false));
|
||||
Instruction::new(id(), &BudgetInstruction::InitializeAccount(expr), keys)
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
from: &Pubkey,
|
||||
contract: &Pubkey,
|
||||
lamports: u64,
|
||||
expr: BudgetExpr,
|
||||
) -> Vec<Instruction> {
|
||||
if !expr.verify(lamports) {
|
||||
panic!("invalid budget expression");
|
||||
}
|
||||
let space = serialized_size(&BudgetState::new(expr.clone())).unwrap();
|
||||
vec![
|
||||
system_instruction::create_account(&from, contract, lamports, space, &id()),
|
||||
initialize_account(contract, expr),
|
||||
]
|
||||
}
|
||||
|
||||
/// Create a new payment script.
|
||||
pub fn payment(from: &Pubkey, to: &Pubkey, lamports: u64) -> Vec<Instruction> {
|
||||
let contract = Pubkey::new_rand();
|
||||
let expr = BudgetExpr::new_payment(lamports, to);
|
||||
create_account(from, &contract, lamports, expr)
|
||||
}
|
||||
|
||||
/// Create a future payment script.
|
||||
pub fn on_date(
|
||||
from: &Pubkey,
|
||||
to: &Pubkey,
|
||||
contract: &Pubkey,
|
||||
dt: DateTime<Utc>,
|
||||
dt_pubkey: &Pubkey,
|
||||
cancelable: Option<Pubkey>,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let expr = BudgetExpr::new_cancelable_future_payment(dt, dt_pubkey, lamports, to, cancelable);
|
||||
create_account(from, contract, lamports, expr)
|
||||
}
|
||||
|
||||
/// Create a multisig payment script.
|
||||
pub fn when_signed(
|
||||
from: &Pubkey,
|
||||
to: &Pubkey,
|
||||
contract: &Pubkey,
|
||||
witness: &Pubkey,
|
||||
cancelable: Option<Pubkey>,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let expr = BudgetExpr::new_cancelable_authorized_payment(witness, lamports, to, cancelable);
|
||||
create_account(from, contract, lamports, expr)
|
||||
}
|
||||
|
||||
pub fn apply_timestamp(
|
||||
from: &Pubkey,
|
||||
contract: &Pubkey,
|
||||
to: &Pubkey,
|
||||
dt: DateTime<Utc>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*from, true),
|
||||
AccountMeta::new(*contract, false),
|
||||
];
|
||||
if from != to {
|
||||
account_metas.push(AccountMeta::new(*to, false));
|
||||
}
|
||||
Instruction::new(id(), &BudgetInstruction::ApplyTimestamp(dt), account_metas)
|
||||
}
|
||||
|
||||
pub fn apply_signature(from: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*from, true),
|
||||
AccountMeta::new(*contract, false),
|
||||
];
|
||||
if from != to {
|
||||
account_metas.push(AccountMeta::new(*to, false));
|
||||
}
|
||||
Instruction::new(id(), &BudgetInstruction::ApplySignature, account_metas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::budget_expr::BudgetExpr;
|
||||
|
||||
#[test]
|
||||
fn test_budget_instruction_verify() {
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
payment(&alice_pubkey, &bob_pubkey, 1); // No panic! indicates success.
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_budget_instruction_overspend() {
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let expr = BudgetExpr::new_payment(2, &bob_pubkey);
|
||||
create_account(&alice_pubkey, &budget_pubkey, 1, expr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_budget_instruction_underspend() {
|
||||
let alice_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let expr = BudgetExpr::new_payment(1, &bob_pubkey);
|
||||
create_account(&alice_pubkey, &budget_pubkey, 2, expr);
|
||||
}
|
||||
}
|
@ -1,402 +0,0 @@
|
||||
//! budget program
|
||||
use crate::budget_expr::Witness;
|
||||
use crate::budget_instruction::BudgetInstruction;
|
||||
use crate::budget_state::{BudgetError, BudgetState};
|
||||
use bincode::deserialize;
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
||||
/// will progress one step.
|
||||
fn apply_signature(
|
||||
budget_state: &mut BudgetState,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
) -> Result<(), BudgetError> {
|
||||
let mut final_payment = None;
|
||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||
let key = keyed_accounts[0].signer_key().unwrap();
|
||||
expr.apply_witness(&Witness::Signature, key);
|
||||
final_payment = expr.final_payment();
|
||||
}
|
||||
|
||||
if let Some(payment) = final_payment {
|
||||
if let Some(key) = keyed_accounts[0].signer_key() {
|
||||
if &payment.to == key {
|
||||
budget_state.pending_budget = None;
|
||||
keyed_accounts[1].account.lamports -= payment.lamports;
|
||||
keyed_accounts[0].account.lamports += payment.lamports;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if &payment.to != keyed_accounts[2].unsigned_key() {
|
||||
trace!("destination missing");
|
||||
return Err(BudgetError::DestinationMissing);
|
||||
}
|
||||
budget_state.pending_budget = None;
|
||||
keyed_accounts[1].account.lamports -= payment.lamports;
|
||||
keyed_accounts[2].account.lamports += payment.lamports;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process a Witness Timestamp. Any payment plans waiting on this timestamp
|
||||
/// will progress one step.
|
||||
fn apply_timestamp(
|
||||
budget_state: &mut BudgetState,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
dt: DateTime<Utc>,
|
||||
) -> Result<(), BudgetError> {
|
||||
// Check to see if any timelocked transactions can be completed.
|
||||
let mut final_payment = None;
|
||||
|
||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||
let key = keyed_accounts[0].signer_key().unwrap();
|
||||
expr.apply_witness(&Witness::Timestamp(dt), key);
|
||||
final_payment = expr.final_payment();
|
||||
}
|
||||
|
||||
if let Some(payment) = final_payment {
|
||||
if &payment.to != keyed_accounts[2].unsigned_key() {
|
||||
trace!("destination missing");
|
||||
return Err(BudgetError::DestinationMissing);
|
||||
}
|
||||
budget_state.pending_budget = None;
|
||||
keyed_accounts[1].account.lamports -= payment.lamports;
|
||||
keyed_accounts[2].account.lamports += payment.lamports;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
let instruction = deserialize(data).map_err(|err| {
|
||||
info!("Invalid transaction data: {:?} {:?}", data, err);
|
||||
InstructionError::InvalidInstructionData
|
||||
})?;
|
||||
|
||||
trace!("process_instruction: {:?}", instruction);
|
||||
|
||||
match instruction {
|
||||
BudgetInstruction::InitializeAccount(expr) => {
|
||||
let expr = expr.clone();
|
||||
if let Some(payment) = expr.final_payment() {
|
||||
keyed_accounts[1].account.lamports = 0;
|
||||
keyed_accounts[0].account.lamports += payment.lamports;
|
||||
return Ok(());
|
||||
}
|
||||
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
|
||||
if Some(true) == existing.map(|x| x.initialized) {
|
||||
trace!("contract already exists");
|
||||
return Err(InstructionError::AccountAlreadyInitialized);
|
||||
}
|
||||
let mut budget_state = BudgetState::default();
|
||||
budget_state.pending_budget = Some(expr);
|
||||
budget_state.initialized = true;
|
||||
budget_state.serialize(&mut keyed_accounts[0].account.data)
|
||||
}
|
||||
BudgetInstruction::ApplyTimestamp(dt) => {
|
||||
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||
if !budget_state.is_pending() {
|
||||
return Ok(()); // Nothing to do here.
|
||||
}
|
||||
if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
return Err(InstructionError::UninitializedAccount);
|
||||
}
|
||||
if keyed_accounts[0].signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
trace!("apply timestamp");
|
||||
apply_timestamp(&mut budget_state, keyed_accounts, dt)
|
||||
.map_err(|e| InstructionError::CustomError(e as u32))?;
|
||||
trace!("apply timestamp committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
BudgetInstruction::ApplySignature => {
|
||||
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||
if !budget_state.is_pending() {
|
||||
return Ok(()); // Nothing to do here.
|
||||
}
|
||||
if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
return Err(InstructionError::UninitializedAccount);
|
||||
}
|
||||
if keyed_accounts[0].signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
trace!("apply signature");
|
||||
apply_signature(&mut budget_state, keyed_accounts)
|
||||
.map_err(|e| InstructionError::CustomError(e as u32))?;
|
||||
trace!("apply signature committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::budget_instruction;
|
||||
use crate::id;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
|
||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(lamports);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
(bank, mint_keypair)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_budget_payment() {
|
||||
let (bank, alice_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let instructions = budget_instruction::payment(&alice_pubkey, &bob_pubkey, 100);
|
||||
let message = Message::new(instructions);
|
||||
bank_client
|
||||
.send_message(&[&alice_keypair], message)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsigned_witness_key() {
|
||||
let (bank, alice_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
|
||||
// Initialize BudgetState
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let witness = Pubkey::new_rand();
|
||||
let instructions = budget_instruction::when_signed(
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
&budget_pubkey,
|
||||
&witness,
|
||||
None,
|
||||
1,
|
||||
);
|
||||
let message = Message::new(instructions);
|
||||
bank_client
|
||||
.send_message(&[&alice_keypair], message)
|
||||
.unwrap();
|
||||
|
||||
// Attack! Part 1: Sign a witness transaction with a random key.
|
||||
let mallory_keypair = Keypair::new();
|
||||
let mallory_pubkey = mallory_keypair.pubkey();
|
||||
bank_client
|
||||
.transfer(1, &alice_keypair, &mallory_pubkey)
|
||||
.unwrap();
|
||||
let instruction =
|
||||
budget_instruction::apply_signature(&mallory_pubkey, &budget_pubkey, &bob_pubkey);
|
||||
let mut message = Message::new(vec![instruction]);
|
||||
|
||||
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
|
||||
message.account_keys.push(alice_pubkey);
|
||||
message.instructions[0].accounts[0] = 3;
|
||||
|
||||
// Ensure the transaction fails because of the unsigned key.
|
||||
assert_eq!(
|
||||
bank_client
|
||||
.send_message(&[&mallory_keypair], message)
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsigned_timestamp() {
|
||||
let (bank, alice_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
|
||||
// Initialize BudgetState
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let dt = Utc::now();
|
||||
let instructions = budget_instruction::on_date(
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
&budget_pubkey,
|
||||
dt,
|
||||
&alice_pubkey,
|
||||
None,
|
||||
1,
|
||||
);
|
||||
let message = Message::new(instructions);
|
||||
bank_client
|
||||
.send_message(&[&alice_keypair], message)
|
||||
.unwrap();
|
||||
|
||||
// Attack! Part 1: Sign a timestamp transaction with a random key.
|
||||
let mallory_keypair = Keypair::new();
|
||||
let mallory_pubkey = mallory_keypair.pubkey();
|
||||
bank_client
|
||||
.transfer(1, &alice_keypair, &mallory_pubkey)
|
||||
.unwrap();
|
||||
let instruction =
|
||||
budget_instruction::apply_timestamp(&mallory_pubkey, &budget_pubkey, &bob_pubkey, dt);
|
||||
let mut message = Message::new(vec![instruction]);
|
||||
|
||||
// Attack! Part 2: Point the instruction to the expected, but unsigned, key.
|
||||
message.account_keys.push(alice_pubkey);
|
||||
message.instructions[0].accounts[0] = 3;
|
||||
|
||||
// Ensure the transaction fails because of the unsigned key.
|
||||
assert_eq!(
|
||||
bank_client
|
||||
.send_message(&[&mallory_keypair], message)
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pay_on_date() {
|
||||
let (bank, alice_keypair) = create_bank(2);
|
||||
let bank_client = BankClient::new(bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let mallory_pubkey = Pubkey::new_rand();
|
||||
let dt = Utc::now();
|
||||
let instructions = budget_instruction::on_date(
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
&budget_pubkey,
|
||||
dt,
|
||||
&alice_pubkey,
|
||||
None,
|
||||
1,
|
||||
);
|
||||
let message = Message::new(instructions);
|
||||
bank_client
|
||||
.send_message(&[&alice_keypair], message)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
|
||||
|
||||
let contract_account = bank_client
|
||||
.get_account_data(&budget_pubkey)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let budget_state = BudgetState::deserialize(&contract_account).unwrap();
|
||||
assert!(budget_state.is_pending());
|
||||
|
||||
// Attack! Try to payout to mallory_pubkey
|
||||
let instruction =
|
||||
budget_instruction::apply_timestamp(&alice_pubkey, &budget_pubkey, &mallory_pubkey, dt);
|
||||
assert_eq!(
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::CustomError(BudgetError::DestinationMissing as u32)
|
||||
)
|
||||
);
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 0);
|
||||
|
||||
let contract_account = bank_client
|
||||
.get_account_data(&budget_pubkey)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let budget_state = BudgetState::deserialize(&contract_account).unwrap();
|
||||
assert!(budget_state.is_pending());
|
||||
|
||||
// Now, acknowledge the time in the condition occurred and
|
||||
// that pubkey's funds are now available.
|
||||
let instruction =
|
||||
budget_instruction::apply_timestamp(&alice_pubkey, &budget_pubkey, &bob_pubkey, dt);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 0);
|
||||
assert_eq!(bank_client.get_balance(&bob_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel_payment() {
|
||||
let (bank, alice_keypair) = create_bank(3);
|
||||
let bank_client = BankClient::new(bank);
|
||||
let alice_pubkey = alice_keypair.pubkey();
|
||||
let budget_pubkey = Pubkey::new_rand();
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let dt = Utc::now();
|
||||
|
||||
let instructions = budget_instruction::on_date(
|
||||
&alice_pubkey,
|
||||
&bob_pubkey,
|
||||
&budget_pubkey,
|
||||
dt,
|
||||
&alice_pubkey,
|
||||
Some(alice_pubkey),
|
||||
1,
|
||||
);
|
||||
let message = Message::new(instructions);
|
||||
bank_client
|
||||
.send_message(&[&alice_keypair], message)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 2);
|
||||
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
|
||||
|
||||
let contract_account = bank_client
|
||||
.get_account_data(&budget_pubkey)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let budget_state = BudgetState::deserialize(&contract_account).unwrap();
|
||||
assert!(budget_state.is_pending());
|
||||
|
||||
// Attack! try to put the lamports into the wrong account with cancel
|
||||
let mallory_keypair = Keypair::new();
|
||||
let mallory_pubkey = mallory_keypair.pubkey();
|
||||
bank_client
|
||||
.transfer(1, &alice_keypair, &mallory_pubkey)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
|
||||
let instruction =
|
||||
budget_instruction::apply_signature(&mallory_pubkey, &budget_pubkey, &bob_pubkey);
|
||||
bank_client
|
||||
.send_instruction(&mallory_keypair, instruction)
|
||||
.unwrap();
|
||||
// nothing should be changed because apply witness didn't finalize a payment
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_balance(&budget_pubkey).unwrap(), 1);
|
||||
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
|
||||
|
||||
// Now, cancel the transaction. mint gets her funds back
|
||||
let instruction =
|
||||
budget_instruction::apply_signature(&alice_pubkey, &budget_pubkey, &alice_pubkey);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
assert_eq!(bank_client.get_balance(&alice_pubkey).unwrap(), 2);
|
||||
assert_eq!(bank_client.get_account_data(&budget_pubkey).unwrap(), None);
|
||||
assert_eq!(bank_client.get_account_data(&bob_pubkey).unwrap(), None);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
//! budget state
|
||||
use crate::budget_expr::BudgetExpr;
|
||||
use bincode::{self, deserialize, serialize_into};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum BudgetError {
|
||||
DestinationMissing,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct BudgetState {
|
||||
pub initialized: bool,
|
||||
pub pending_budget: Option<BudgetExpr>,
|
||||
}
|
||||
|
||||
impl BudgetState {
|
||||
pub fn new(budget_expr: BudgetExpr) -> Self {
|
||||
Self {
|
||||
initialized: true,
|
||||
pending_budget: Some(budget_expr),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending_budget.is_some()
|
||||
}
|
||||
|
||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), InstructionError> {
|
||||
serialize_into(output, self).map_err(|_| InstructionError::AccountDataTooSmall)
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
|
||||
deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use solana_sdk::account::Account;
|
||||
|
||||
#[test]
|
||||
fn test_serializer() {
|
||||
let mut a = Account::new(0, 512, &id());
|
||||
let b = BudgetState::default();
|
||||
b.serialize(&mut a.data).unwrap();
|
||||
let c = BudgetState::deserialize(&a.data).unwrap();
|
||||
assert_eq!(b, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serializer_data_too_small() {
|
||||
let mut a = Account::new(0, 1, &id());
|
||||
let b = BudgetState::default();
|
||||
assert_eq!(
|
||||
b.serialize(&mut a.data),
|
||||
Err(InstructionError::AccountDataTooSmall)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
pub mod budget_expr;
|
||||
pub mod budget_instruction;
|
||||
pub mod budget_processor;
|
||||
pub mod budget_state;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const BUDGET_PROGRAM_ID: [u8; 32] = [
|
||||
129, 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,
|
||||
];
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&BUDGET_PROGRAM_ID)
|
||||
}
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == BUDGET_PROGRAM_ID
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-budget-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana budget program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-budget-api = { path = "../budget_api", version = "0.14.0" }
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_budget_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_budget_api::budget_processor::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "solana-config-api"
|
||||
version = "0.14.0"
|
||||
description = "config program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_config_api"
|
||||
crate-type = ["lib"]
|
||||
|
@ -1,33 +0,0 @@
|
||||
use crate::id;
|
||||
use crate::ConfigState;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
/// Create a new, empty configuration account
|
||||
pub fn create_account<T: ConfigState>(
|
||||
from_account_pubkey: &Pubkey,
|
||||
config_account_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Instruction {
|
||||
system_instruction::create_account(
|
||||
from_account_pubkey,
|
||||
config_account_pubkey,
|
||||
lamports,
|
||||
T::max_space(),
|
||||
&id(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Store new data in a configuration account
|
||||
pub fn store<T: ConfigState>(
|
||||
from_account_pubkey: &Pubkey,
|
||||
config_account_pubkey: &Pubkey,
|
||||
data: &T,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*from_account_pubkey, true),
|
||||
AccountMeta::new(*config_account_pubkey, true),
|
||||
];
|
||||
Instruction::new(id(), data, account_metas)
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
//! Config program
|
||||
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
if keyed_accounts[1].signer_key().is_none() {
|
||||
error!("account[1] should sign the transaction");
|
||||
Err(InstructionError::MissingRequiredSignature)?;
|
||||
}
|
||||
|
||||
if keyed_accounts[1].account.data.len() < data.len() {
|
||||
error!("instruction data too large");
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
|
||||
keyed_accounts[1].account.data[0..data.len()].copy_from_slice(data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{config_instruction, id, ConfigState};
|
||||
use bincode::{deserialize, serialized_size};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
|
||||
struct MyConfig {
|
||||
pub item: u64,
|
||||
}
|
||||
impl MyConfig {
|
||||
pub fn new(item: u64) -> Self {
|
||||
Self { item }
|
||||
}
|
||||
pub fn deserialize(input: &[u8]) -> Option<Self> {
|
||||
deserialize(input).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigState for MyConfig {
|
||||
fn max_space() -> u64 {
|
||||
serialized_size(&Self::default()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(lamports);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
(bank, mint_keypair)
|
||||
}
|
||||
|
||||
fn create_config_client(bank: Bank, mint_keypair: Keypair) -> (BankClient, Keypair, Keypair) {
|
||||
let from_keypair = Keypair::new();
|
||||
let from_pubkey = from_keypair.pubkey();
|
||||
let config_keypair = Keypair::new();
|
||||
let config_pubkey = config_keypair.pubkey();
|
||||
|
||||
let bank_client = BankClient::new(bank);
|
||||
bank_client
|
||||
.transfer(42, &mint_keypair, &from_pubkey)
|
||||
.unwrap();
|
||||
|
||||
bank_client
|
||||
.send_instruction(
|
||||
&mint_keypair,
|
||||
config_instruction::create_account::<MyConfig>(
|
||||
&mint_keypair.pubkey(),
|
||||
&config_pubkey,
|
||||
1,
|
||||
),
|
||||
)
|
||||
.expect("new_account");
|
||||
|
||||
(bank_client, from_keypair, config_keypair)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_create_ok() {
|
||||
solana_logger::setup();
|
||||
let (bank, from_keypair) = create_bank(10_000);
|
||||
let (bank_client, _, config_keypair) = create_config_client(bank, from_keypair);
|
||||
let config_account_data = bank_client
|
||||
.get_account_data(&config_keypair.pubkey())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
MyConfig::default(),
|
||||
MyConfig::deserialize(&config_account_data).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_store_ok() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (bank_client, from_keypair, config_keypair) = create_config_client(bank, mint_keypair);
|
||||
let config_pubkey = config_keypair.pubkey();
|
||||
|
||||
let my_config = MyConfig::new(42);
|
||||
let instruction =
|
||||
config_instruction::store(&from_keypair.pubkey(), &config_pubkey, &my_config);
|
||||
let message = Message::new(vec![instruction]);
|
||||
bank_client
|
||||
.send_message(&[&from_keypair, &config_keypair], message)
|
||||
.unwrap();
|
||||
|
||||
let config_account_data = bank_client
|
||||
.get_account_data(&config_pubkey)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
my_config,
|
||||
MyConfig::deserialize(&config_account_data).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_store_fail_instruction_data_too_large() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (bank_client, from_keypair, config_keypair) = create_config_client(bank, mint_keypair);
|
||||
|
||||
let my_config = MyConfig::new(42);
|
||||
|
||||
// Replace instruction data with a vector that's too large
|
||||
let mut instruction =
|
||||
config_instruction::store(&from_keypair.pubkey(), &config_keypair.pubkey(), &my_config);
|
||||
instruction.data = vec![0; 123];
|
||||
|
||||
let message = Message::new(vec![instruction]);
|
||||
bank_client
|
||||
.send_message(&[&from_keypair, &config_keypair], message)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_store_fail_account1_not_signer() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let system_keypair = Keypair::new();
|
||||
let system_pubkey = system_keypair.pubkey();
|
||||
bank.transfer(42, &mint_keypair, &system_pubkey).unwrap();
|
||||
let (bank_client, from_keypair, config_keypair) = create_config_client(bank, mint_keypair);
|
||||
|
||||
let move_instruction = system_instruction::transfer(&system_pubkey, &Pubkey::default(), 42);
|
||||
let my_config = MyConfig::new(42);
|
||||
let mut store_instruction =
|
||||
config_instruction::store(&from_keypair.pubkey(), &config_keypair.pubkey(), &my_config);
|
||||
store_instruction.accounts[0].is_signer = false;
|
||||
store_instruction.accounts[1].is_signer = false;
|
||||
|
||||
// Don't sign the transaction with `config_client`
|
||||
let message = Message::new(vec![move_instruction, store_instruction]);
|
||||
bank_client
|
||||
.send_message(&[&system_keypair], message)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub mod config_instruction;
|
||||
pub mod config_processor;
|
||||
|
||||
const CONFIG_PROGRAM_ID: [u8; 32] = [
|
||||
133, 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,
|
||||
];
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == CONFIG_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&CONFIG_PROGRAM_ID)
|
||||
}
|
||||
|
||||
pub trait ConfigState: Serialize {
|
||||
/// Maximum space that the serialized representation will require
|
||||
fn max_space() -> u64;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-config-program"
|
||||
version = "0.14.0"
|
||||
description = "config program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-config-api = { path = "../config_api", version = "0.14.0" }
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_config_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_config_api::config_processor::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "solana-exchange-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Exchange program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
bincode = "1.1.3"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_exchange_api"
|
||||
crate-type = ["lib"]
|
@ -1,146 +0,0 @@
|
||||
//! Exchange program
|
||||
|
||||
use crate::exchange_state::*;
|
||||
use crate::id;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TradeRequestInfo {
|
||||
/// Direction of trade
|
||||
pub direction: Direction,
|
||||
|
||||
/// Token pair to trade
|
||||
pub pair: TokenPair,
|
||||
|
||||
/// Number of tokens to exchange; refers to the primary or the secondary depending on the direction
|
||||
pub tokens: u64,
|
||||
|
||||
/// The price ratio the primary price over the secondary price. The primary price is fixed
|
||||
/// and equal to the variable `SCALER`.
|
||||
pub price: u64,
|
||||
|
||||
/// Token account to deposit tokens on successful swap
|
||||
pub dst_account: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ExchangeInstruction {
|
||||
/// New token account
|
||||
/// key 0 - Signer
|
||||
/// key 1 - New token account
|
||||
AccountRequest,
|
||||
|
||||
/// Transfer tokens between two accounts
|
||||
/// key 0 - Account to transfer tokens to
|
||||
/// key 1 - Account to transfer tokens from. This can be the exchange program itself,
|
||||
/// the exchange has a limitless number of tokens it can transfer.
|
||||
TransferRequest(Token, u64),
|
||||
|
||||
/// Trade request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - Token account associated with this trade
|
||||
TradeRequest(TradeRequestInfo),
|
||||
|
||||
/// Trade cancellation
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Trade order to cancel
|
||||
TradeCancellation,
|
||||
|
||||
/// Trade swap request
|
||||
/// key 0 - Signer
|
||||
/// key 1 - Account in which to record the swap
|
||||
/// key 2 - 'To' trade order
|
||||
/// key 3 - `From` trade order
|
||||
/// key 4 - Token account associated with the To Trade
|
||||
/// key 5 - Token account associated with From trade
|
||||
/// key 6 - Token account in which to deposit the brokers profit from the swap.
|
||||
SwapRequest,
|
||||
}
|
||||
|
||||
pub fn account_request(owner: &Pubkey, new: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*owner, true),
|
||||
AccountMeta::new(*new, false),
|
||||
];
|
||||
Instruction::new(id(), &ExchangeInstruction::AccountRequest, account_metas)
|
||||
}
|
||||
|
||||
pub fn transfer_request(
|
||||
owner: &Pubkey,
|
||||
to: &Pubkey,
|
||||
from: &Pubkey,
|
||||
token: Token,
|
||||
tokens: u64,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*owner, true),
|
||||
AccountMeta::new(*to, false),
|
||||
AccountMeta::new(*from, false),
|
||||
];
|
||||
Instruction::new(
|
||||
id(),
|
||||
&ExchangeInstruction::TransferRequest(token, tokens),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn trade_request(
|
||||
owner: &Pubkey,
|
||||
trade: &Pubkey,
|
||||
direction: Direction,
|
||||
pair: TokenPair,
|
||||
tokens: u64,
|
||||
price: u64,
|
||||
src_account: &Pubkey,
|
||||
dst_account: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*owner, true),
|
||||
AccountMeta::new(*trade, false),
|
||||
AccountMeta::new(*src_account, false),
|
||||
];
|
||||
Instruction::new(
|
||||
id(),
|
||||
&ExchangeInstruction::TradeRequest(TradeRequestInfo {
|
||||
direction,
|
||||
pair,
|
||||
tokens,
|
||||
price,
|
||||
dst_account: *dst_account,
|
||||
}),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn trade_cancellation(owner: &Pubkey, trade: &Pubkey, account: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*owner, true),
|
||||
AccountMeta::new(*trade, false),
|
||||
AccountMeta::new(*account, false),
|
||||
];
|
||||
Instruction::new(id(), &ExchangeInstruction::TradeCancellation, account_metas)
|
||||
}
|
||||
|
||||
pub fn swap_request(
|
||||
owner: &Pubkey,
|
||||
swap: &Pubkey,
|
||||
to_trade: &Pubkey,
|
||||
from_trade: &Pubkey,
|
||||
to_trade_account: &Pubkey,
|
||||
from_trade_account: &Pubkey,
|
||||
profit_account: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*owner, true),
|
||||
AccountMeta::new(*swap, false),
|
||||
AccountMeta::new(*to_trade, false),
|
||||
AccountMeta::new(*from_trade, false),
|
||||
AccountMeta::new(*to_trade_account, false),
|
||||
AccountMeta::new(*from_trade_account, false),
|
||||
AccountMeta::new(*profit_account, false),
|
||||
];
|
||||
Instruction::new(id(), &ExchangeInstruction::SwapRequest, account_metas)
|
||||
}
|
@ -1,875 +0,0 @@
|
||||
//! Config processor
|
||||
|
||||
use crate::exchange_instruction::*;
|
||||
use crate::exchange_state::*;
|
||||
use crate::id;
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::cmp;
|
||||
|
||||
pub struct ExchangeProcessor {}
|
||||
|
||||
impl ExchangeProcessor {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn map_to_invalid_arg(err: std::boxed::Box<bincode::ErrorKind>) -> InstructionError {
|
||||
warn!("Deserialze failed: {:?}", err);
|
||||
InstructionError::InvalidArgument
|
||||
}
|
||||
|
||||
fn is_account_unallocated(data: &[u8]) -> Result<(), InstructionError> {
|
||||
let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?;
|
||||
if let ExchangeState::Unallocated = state {
|
||||
Ok(())
|
||||
} else {
|
||||
error!("New account is already in use");
|
||||
Err(InstructionError::InvalidAccountData)?
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_account(data: &[u8]) -> Result<TokenAccountInfo, InstructionError> {
|
||||
let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?;
|
||||
if let ExchangeState::Account(account) = state {
|
||||
Ok(account)
|
||||
} else {
|
||||
error!("Not a valid account");
|
||||
Err(InstructionError::InvalidAccountData)?
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_trade(data: &[u8]) -> Result<TradeOrderInfo, InstructionError> {
|
||||
let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?;
|
||||
if let ExchangeState::Trade(info) = state {
|
||||
Ok(info)
|
||||
} else {
|
||||
error!("Not a valid trade");
|
||||
Err(InstructionError::InvalidAccountData)?
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(state: &ExchangeState, data: &mut [u8]) -> Result<(), InstructionError> {
|
||||
let writer = std::io::BufWriter::new(data);
|
||||
match bincode::serialize_into(writer, state) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Serialize failed: {:?}", e);
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_swap(
|
||||
scaler: u64,
|
||||
swap: &mut TradeSwapInfo,
|
||||
to_trade: &mut TradeOrderInfo,
|
||||
from_trade: &mut TradeOrderInfo,
|
||||
to_trade_account: &mut TokenAccountInfo,
|
||||
from_trade_account: &mut TokenAccountInfo,
|
||||
profit_account: &mut TokenAccountInfo,
|
||||
) -> Result<(), InstructionError> {
|
||||
if to_trade.tokens == 0 || from_trade.tokens == 0 {
|
||||
error!("Inactive Trade, balance is zero");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if to_trade.price == 0 || from_trade.price == 0 {
|
||||
error!("Inactive Trade, price is zero");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
// Calc swap
|
||||
|
||||
trace!("tt {} ft {}", to_trade.tokens, from_trade.tokens);
|
||||
trace!("tp {} fp {}", to_trade.price, from_trade.price);
|
||||
|
||||
let max_to_secondary = to_trade.tokens * to_trade.price / scaler;
|
||||
let max_to_primary = from_trade.tokens * scaler / from_trade.price;
|
||||
|
||||
trace!("mtp {} mts {}", max_to_primary, max_to_secondary);
|
||||
|
||||
let max_primary = cmp::min(max_to_primary, to_trade.tokens);
|
||||
let max_secondary = cmp::min(max_to_secondary, from_trade.tokens);
|
||||
|
||||
trace!("mp {} ms {}", max_primary, max_secondary);
|
||||
|
||||
let primary_tokens = if max_secondary < max_primary {
|
||||
max_secondary * scaler / from_trade.price
|
||||
} else {
|
||||
max_primary
|
||||
};
|
||||
let secondary_tokens = if max_secondary < max_primary {
|
||||
max_secondary
|
||||
} else {
|
||||
max_primary * to_trade.price / scaler
|
||||
};
|
||||
|
||||
if primary_tokens == 0 || secondary_tokens == 0 {
|
||||
error!("Trade quantities to low to be fulfilled");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
trace!("pt {} st {}", primary_tokens, secondary_tokens);
|
||||
|
||||
let primary_cost = cmp::max(primary_tokens, secondary_tokens * scaler / to_trade.price);
|
||||
let secondary_cost = cmp::max(secondary_tokens, primary_tokens * from_trade.price / scaler);
|
||||
|
||||
trace!("pc {} sc {}", primary_cost, secondary_cost);
|
||||
|
||||
let primary_profit = primary_cost - primary_tokens;
|
||||
let secondary_profit = secondary_cost - secondary_tokens;
|
||||
|
||||
trace!("pp {} sp {}", primary_profit, secondary_profit);
|
||||
|
||||
let primary_token = to_trade.pair.primary();
|
||||
let secondary_token = from_trade.pair.secondary();
|
||||
|
||||
// Update tokens/accounts
|
||||
|
||||
if to_trade.tokens < primary_cost {
|
||||
error!("Not enough tokens in to account");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if from_trade.tokens < secondary_cost {
|
||||
error!("Not enough tokens in from account");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
to_trade.tokens -= primary_cost;
|
||||
from_trade.tokens -= secondary_cost;
|
||||
|
||||
to_trade_account.tokens[secondary_token] += secondary_tokens;
|
||||
from_trade_account.tokens[primary_token] += primary_tokens;
|
||||
|
||||
profit_account.tokens[primary_token] += primary_profit;
|
||||
profit_account.tokens[secondary_token] += secondary_profit;
|
||||
|
||||
swap.pair = to_trade.pair;
|
||||
swap.primary_tokens = primary_cost;
|
||||
swap.primary_price = to_trade.price;
|
||||
swap.secondary_tokens = secondary_cost;
|
||||
swap.secondary_price = from_trade.price;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_account_request(keyed_accounts: &mut [KeyedAccount]) -> Result<(), InstructionError> {
|
||||
const OWNER_INDEX: usize = 0;
|
||||
const NEW_ACCOUNT_INDEX: usize = 1;
|
||||
|
||||
if keyed_accounts.len() < 2 {
|
||||
error!("Not enough accounts");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
Self::is_account_unallocated(&keyed_accounts[NEW_ACCOUNT_INDEX].account.data)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&keyed_accounts[OWNER_INDEX].unsigned_key())
|
||||
.tokens(100_000, 100_000, 100_000, 100_000),
|
||||
),
|
||||
&mut keyed_accounts[NEW_ACCOUNT_INDEX].account.data,
|
||||
)
|
||||
}
|
||||
|
||||
fn do_transfer_request(
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
token: Token,
|
||||
tokens: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
const OWNER_INDEX: usize = 0;
|
||||
const TO_ACCOUNT_INDEX: usize = 1;
|
||||
const FROM_ACCOUNT_INDEX: usize = 2;
|
||||
|
||||
if keyed_accounts.len() < 3 {
|
||||
error!("Not enough accounts");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
let mut to_account =
|
||||
Self::deserialize_account(&keyed_accounts[TO_ACCOUNT_INDEX].account.data)?;
|
||||
|
||||
if &id() == keyed_accounts[FROM_ACCOUNT_INDEX].unsigned_key() {
|
||||
to_account.tokens[token] += tokens;
|
||||
} else {
|
||||
let mut from_account =
|
||||
Self::deserialize_account(&keyed_accounts[FROM_ACCOUNT_INDEX].account.data)?;
|
||||
|
||||
if &from_account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
|
||||
error!("Signer does not own from account");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
|
||||
if from_account.tokens[token] < tokens {
|
||||
error!("From account balance too low");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
|
||||
from_account.tokens[token] -= tokens;
|
||||
to_account.tokens[token] += tokens;
|
||||
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(from_account),
|
||||
&mut keyed_accounts[FROM_ACCOUNT_INDEX].account.data,
|
||||
)?;
|
||||
}
|
||||
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(to_account),
|
||||
&mut keyed_accounts[TO_ACCOUNT_INDEX].account.data,
|
||||
)
|
||||
}
|
||||
|
||||
fn do_trade_request(
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
info: &TradeRequestInfo,
|
||||
) -> Result<(), InstructionError> {
|
||||
const OWNER_INDEX: usize = 0;
|
||||
const TRADE_INDEX: usize = 1;
|
||||
const ACCOUNT_INDEX: usize = 2;
|
||||
|
||||
if keyed_accounts.len() < 3 {
|
||||
error!("Not enough accounts");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
Self::is_account_unallocated(&keyed_accounts[TRADE_INDEX].account.data)?;
|
||||
|
||||
let mut account = Self::deserialize_account(&keyed_accounts[ACCOUNT_INDEX].account.data)?;
|
||||
|
||||
if &account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
|
||||
error!("Signer does not own account");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
let from_token = match info.direction {
|
||||
Direction::To => info.pair.primary(),
|
||||
Direction::From => info.pair.secondary(),
|
||||
};
|
||||
if account.tokens[from_token] < info.tokens {
|
||||
error!("From token balance is too low");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
|
||||
if let Err(e) = check_trade(info.direction, info.tokens, info.price) {
|
||||
bincode::serialize(&e).unwrap();
|
||||
}
|
||||
|
||||
// Trade holds the tokens in escrow
|
||||
account.tokens[from_token] -= info.tokens;
|
||||
|
||||
Self::serialize(
|
||||
&ExchangeState::Trade(TradeOrderInfo {
|
||||
owner: *keyed_accounts[OWNER_INDEX].unsigned_key(),
|
||||
direction: info.direction,
|
||||
pair: info.pair,
|
||||
tokens: info.tokens,
|
||||
price: info.price,
|
||||
src_account: *keyed_accounts[ACCOUNT_INDEX].unsigned_key(),
|
||||
dst_account: info.dst_account,
|
||||
}),
|
||||
&mut keyed_accounts[TRADE_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(account),
|
||||
&mut keyed_accounts[ACCOUNT_INDEX].account.data,
|
||||
)
|
||||
}
|
||||
|
||||
fn do_trade_cancellation(keyed_accounts: &mut [KeyedAccount]) -> Result<(), InstructionError> {
|
||||
const OWNER_INDEX: usize = 0;
|
||||
const TRADE_INDEX: usize = 1;
|
||||
const ACCOUNT_INDEX: usize = 2;
|
||||
|
||||
if keyed_accounts.len() < 3 {
|
||||
error!("Not enough accounts");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
let mut trade = Self::deserialize_trade(&keyed_accounts[TRADE_INDEX].account.data)?;
|
||||
let mut account = Self::deserialize_account(&keyed_accounts[ACCOUNT_INDEX].account.data)?;
|
||||
|
||||
if &trade.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
|
||||
error!("Signer does not own trade");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
|
||||
if &account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
|
||||
error!("Signer does not own account");
|
||||
Err(InstructionError::GenericError)?
|
||||
}
|
||||
|
||||
let token = match trade.direction {
|
||||
Direction::To => trade.pair.primary(),
|
||||
Direction::From => trade.pair.secondary(),
|
||||
};
|
||||
|
||||
// Outstanding tokens transferred back to account
|
||||
account.tokens[token] += trade.tokens;
|
||||
// Trade becomes invalid
|
||||
trade.tokens = 0;
|
||||
|
||||
Self::serialize(
|
||||
&ExchangeState::Trade(trade),
|
||||
&mut keyed_accounts[TRADE_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(account),
|
||||
&mut keyed_accounts[ACCOUNT_INDEX].account.data,
|
||||
)
|
||||
}
|
||||
|
||||
fn do_swap_request(keyed_accounts: &mut [KeyedAccount]) -> Result<(), InstructionError> {
|
||||
const SWAP_ACCOUNT_INDEX: usize = 1;
|
||||
const TO_TRADE_INDEX: usize = 2;
|
||||
const FROM_TRADE_INDEX: usize = 3;
|
||||
const TO_ACCOUNT_INDEX: usize = 4;
|
||||
const FROM_ACCOUNT_INDEX: usize = 5;
|
||||
const PROFIT_ACCOUNT_INDEX: usize = 6;
|
||||
|
||||
if keyed_accounts.len() < 7 {
|
||||
error!("Not enough accounts");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
Self::is_account_unallocated(&keyed_accounts[SWAP_ACCOUNT_INDEX].account.data)?;
|
||||
let mut to_trade = Self::deserialize_trade(&keyed_accounts[TO_TRADE_INDEX].account.data)?;
|
||||
let mut from_trade =
|
||||
Self::deserialize_trade(&keyed_accounts[FROM_TRADE_INDEX].account.data)?;
|
||||
let mut to_trade_account =
|
||||
Self::deserialize_account(&keyed_accounts[TO_ACCOUNT_INDEX].account.data)?;
|
||||
let mut from_trade_account =
|
||||
Self::deserialize_account(&keyed_accounts[FROM_ACCOUNT_INDEX].account.data)?;
|
||||
let mut profit_account =
|
||||
Self::deserialize_account(&keyed_accounts[PROFIT_ACCOUNT_INDEX].account.data)?;
|
||||
|
||||
if &to_trade.dst_account != keyed_accounts[TO_ACCOUNT_INDEX].unsigned_key() {
|
||||
error!("To trade account and to account differ");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if &from_trade.dst_account != keyed_accounts[FROM_ACCOUNT_INDEX].unsigned_key() {
|
||||
error!("From trade account and from account differ");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if to_trade.direction != Direction::To {
|
||||
error!("To trade is not a To");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if from_trade.direction != Direction::From {
|
||||
error!("From trade is not a From");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if to_trade.pair != from_trade.pair {
|
||||
error!("Mismatched token pairs");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
if to_trade.direction == from_trade.direction {
|
||||
error!("Matching trade directions");
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
|
||||
let mut swap = TradeSwapInfo::default();
|
||||
swap.to_trade_order = *keyed_accounts[TO_TRADE_INDEX].unsigned_key();
|
||||
swap.from_trade_order = *keyed_accounts[FROM_TRADE_INDEX].unsigned_key();
|
||||
|
||||
if let Err(e) = Self::calculate_swap(
|
||||
SCALER,
|
||||
&mut swap,
|
||||
&mut to_trade,
|
||||
&mut from_trade,
|
||||
&mut to_trade_account,
|
||||
&mut from_trade_account,
|
||||
&mut profit_account,
|
||||
) {
|
||||
error!(
|
||||
"Swap calculation failed from {} for {} to {} for {}",
|
||||
from_trade.tokens, from_trade.price, to_trade.tokens, to_trade.price,
|
||||
);
|
||||
Err(e)?
|
||||
}
|
||||
|
||||
Self::serialize(
|
||||
&ExchangeState::Swap(swap),
|
||||
&mut keyed_accounts[SWAP_ACCOUNT_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Trade(to_trade),
|
||||
&mut keyed_accounts[TO_TRADE_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Trade(from_trade),
|
||||
&mut keyed_accounts[FROM_TRADE_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(to_trade_account),
|
||||
&mut keyed_accounts[TO_ACCOUNT_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(from_trade_account),
|
||||
&mut keyed_accounts[FROM_ACCOUNT_INDEX].account.data,
|
||||
)?;
|
||||
Self::serialize(
|
||||
&ExchangeState::Account(profit_account),
|
||||
&mut keyed_accounts[PROFIT_ACCOUNT_INDEX].account.data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
let command = bincode::deserialize::<ExchangeInstruction>(data).map_err(|err| {
|
||||
info!("Invalid transaction data: {:?} {:?}", data, err);
|
||||
InstructionError::InvalidInstructionData
|
||||
})?;
|
||||
|
||||
trace!("{:?}", command);
|
||||
|
||||
match command {
|
||||
ExchangeInstruction::AccountRequest => {
|
||||
ExchangeProcessor::do_account_request(keyed_accounts)
|
||||
}
|
||||
ExchangeInstruction::TransferRequest(token, tokens) => {
|
||||
ExchangeProcessor::do_transfer_request(keyed_accounts, token, tokens)
|
||||
}
|
||||
ExchangeInstruction::TradeRequest(info) => {
|
||||
ExchangeProcessor::do_trade_request(keyed_accounts, &info)
|
||||
}
|
||||
ExchangeInstruction::TradeCancellation => {
|
||||
ExchangeProcessor::do_trade_cancellation(keyed_accounts)
|
||||
}
|
||||
ExchangeInstruction::SwapRequest => ExchangeProcessor::do_swap_request(keyed_accounts),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::exchange_instruction;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_instruction;
|
||||
use std::mem;
|
||||
|
||||
fn try_calc(
|
||||
scaler: u64,
|
||||
primary_tokens: u64,
|
||||
primary_price: u64,
|
||||
secondary_tokens: u64,
|
||||
secondary_price: u64,
|
||||
primary_tokens_expect: u64,
|
||||
secondary_tokens_expect: u64,
|
||||
primary_account_tokens: Tokens,
|
||||
secondary_account_tokens: Tokens,
|
||||
profit_account_tokens: Tokens,
|
||||
) -> Result<(), InstructionError> {
|
||||
trace!(
|
||||
"Swap {} for {} to {} for {}",
|
||||
primary_tokens,
|
||||
primary_price,
|
||||
secondary_tokens,
|
||||
secondary_price,
|
||||
);
|
||||
let mut swap = TradeSwapInfo::default();
|
||||
let mut to_trade = TradeOrderInfo::default();
|
||||
let mut from_trade = TradeOrderInfo::default().direction(Direction::From);
|
||||
let mut to_account = TokenAccountInfo::default();
|
||||
let mut from_account = TokenAccountInfo::default();
|
||||
let mut profit_account = TokenAccountInfo::default();
|
||||
|
||||
to_trade.tokens = primary_tokens;
|
||||
to_trade.price = primary_price;
|
||||
from_trade.tokens = secondary_tokens;
|
||||
from_trade.price = secondary_price;
|
||||
ExchangeProcessor::calculate_swap(
|
||||
scaler,
|
||||
&mut swap,
|
||||
&mut to_trade,
|
||||
&mut from_trade,
|
||||
&mut to_account,
|
||||
&mut from_account,
|
||||
&mut profit_account,
|
||||
)?;
|
||||
|
||||
trace!(
|
||||
"{:?} {:?} {:?} {:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}",
|
||||
to_trade.tokens,
|
||||
primary_tokens_expect,
|
||||
from_trade.tokens,
|
||||
secondary_tokens_expect,
|
||||
to_account.tokens,
|
||||
primary_account_tokens,
|
||||
from_account.tokens,
|
||||
secondary_account_tokens,
|
||||
profit_account.tokens,
|
||||
profit_account_tokens
|
||||
);
|
||||
|
||||
assert_eq!(to_trade.tokens, primary_tokens_expect);
|
||||
assert_eq!(from_trade.tokens, secondary_tokens_expect);
|
||||
assert_eq!(to_account.tokens, primary_account_tokens);
|
||||
assert_eq!(from_account.tokens, secondary_account_tokens);
|
||||
assert_eq!(profit_account.tokens, profit_account_tokens);
|
||||
assert_eq!(swap.primary_tokens, primary_tokens - to_trade.tokens);
|
||||
assert_eq!(swap.primary_price, to_trade.price);
|
||||
assert_eq!(swap.secondary_tokens, secondary_tokens - from_trade.tokens);
|
||||
assert_eq!(swap.secondary_price, from_trade.price);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_calculate_swap() {
|
||||
solana_logger::setup();
|
||||
|
||||
try_calc(1, 50, 2, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
try_calc(1, 50, 1, 0, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
try_calc(1, 0, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
try_calc(1, 50, 1, 50, 0, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
try_calc(1, 50, 0, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
try_calc(1, 1, 2, 2, 3, 1, 2, Tokens::new(0, 0, 0, 0), Tokens::new( 0, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err();
|
||||
|
||||
try_calc(1, 50, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap();
|
||||
try_calc(1, 1, 2, 3, 3, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap();
|
||||
try_calc(1, 2, 2, 3, 3, 1, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap();
|
||||
try_calc(1, 3, 2, 3, 3, 2, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap();
|
||||
try_calc(1, 3, 2, 6, 3, 1, 0, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap();
|
||||
try_calc(1000, 1, 2000, 3, 3000, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap();
|
||||
try_calc(1, 3, 2, 7, 3, 1, 1, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap();
|
||||
try_calc(1000, 3000, 333, 1000, 500, 0, 1, Tokens::new(0, 999, 0, 0), Tokens::new(1998, 0, 0, 0), Tokens::new(1002, 0, 0, 0)).unwrap();
|
||||
try_calc(1000, 50, 100, 50, 101, 0,45, Tokens::new(0, 5, 0, 0), Tokens::new( 49, 0, 0, 0), Tokens::new( 1, 0, 0, 0)).unwrap();
|
||||
}
|
||||
|
||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(lamports);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
(bank, mint_keypair)
|
||||
}
|
||||
|
||||
fn create_client(bank: Bank, mint_keypair: Keypair) -> (BankClient, Keypair) {
|
||||
let owner = Keypair::new();
|
||||
let bank_client = BankClient::new(bank);
|
||||
bank_client
|
||||
.transfer(42, &mint_keypair, &owner.pubkey())
|
||||
.unwrap();
|
||||
|
||||
(bank_client, owner)
|
||||
}
|
||||
|
||||
fn create_account(client: &BankClient, owner: &Keypair) -> Pubkey {
|
||||
let new = Pubkey::new_rand();
|
||||
let instruction = system_instruction::create_account(
|
||||
&owner.pubkey(),
|
||||
&new,
|
||||
1,
|
||||
mem::size_of::<ExchangeState>() as u64,
|
||||
&id(),
|
||||
);
|
||||
client
|
||||
.send_instruction(&owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
new
|
||||
}
|
||||
|
||||
fn create_token_account(client: &BankClient, owner: &Keypair) -> Pubkey {
|
||||
let new = Pubkey::new_rand();
|
||||
let instruction = system_instruction::create_account(
|
||||
&owner.pubkey(),
|
||||
&new,
|
||||
1,
|
||||
mem::size_of::<ExchangeState>() as u64,
|
||||
&id(),
|
||||
);
|
||||
client
|
||||
.send_instruction(owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
let instruction = exchange_instruction::account_request(&owner.pubkey(), &new);
|
||||
client
|
||||
.send_instruction(owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
new
|
||||
}
|
||||
|
||||
fn transfer(client: &BankClient, owner: &Keypair, to: &Pubkey, token: Token, tokens: u64) {
|
||||
let instruction =
|
||||
exchange_instruction::transfer_request(&owner.pubkey(), to, &id(), token, tokens);
|
||||
client
|
||||
.send_instruction(owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
}
|
||||
|
||||
fn trade(
|
||||
client: &BankClient,
|
||||
owner: &Keypair,
|
||||
direction: Direction,
|
||||
pair: TokenPair,
|
||||
from_token: Token,
|
||||
src_tokens: u64,
|
||||
trade_tokens: u64,
|
||||
price: u64,
|
||||
) -> (Pubkey, Pubkey, Pubkey) {
|
||||
let trade = create_account(&client, &owner);
|
||||
let src = create_token_account(&client, &owner);
|
||||
let dst = create_token_account(&client, &owner);
|
||||
transfer(&client, &owner, &src, from_token, src_tokens);
|
||||
|
||||
let instruction = exchange_instruction::trade_request(
|
||||
&owner.pubkey(),
|
||||
&trade,
|
||||
direction,
|
||||
pair,
|
||||
trade_tokens,
|
||||
price,
|
||||
&src,
|
||||
&dst,
|
||||
);
|
||||
client
|
||||
.send_instruction(owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
(trade, src, dst)
|
||||
}
|
||||
|
||||
fn deserialize_swap(data: &[u8]) -> TradeSwapInfo {
|
||||
let state: ExchangeState =
|
||||
bincode::deserialize(data).expect(&format!("{}:{}", line!(), file!()));
|
||||
match state {
|
||||
ExchangeState::Swap(info) => info,
|
||||
_ => panic!("Not a valid swap"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_new_account() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (client, owner) = create_client(bank, mint_keypair);
|
||||
|
||||
let new = create_token_account(&client, &owner);
|
||||
let new_account_data = client.get_account_data(&new).unwrap().unwrap();
|
||||
|
||||
// Check results
|
||||
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&new_account_data).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_new_account_not_unallocated() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (client, owner) = create_client(bank, mint_keypair);
|
||||
|
||||
let new = create_token_account(&client, &owner);
|
||||
let instruction = exchange_instruction::account_request(&owner.pubkey(), &new);
|
||||
client
|
||||
.send_instruction(&owner, instruction)
|
||||
.expect_err(&format!("{}:{}", line!(), file!()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_new_transfer_request() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (client, owner) = create_client(bank, mint_keypair);
|
||||
|
||||
let new = create_token_account(&client, &owner);
|
||||
|
||||
let instruction =
|
||||
exchange_instruction::transfer_request(&owner.pubkey(), &new, &id(), Token::A, 42);
|
||||
client
|
||||
.send_instruction(&owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
|
||||
let new_account_data = client.get_account_data(&new).unwrap().unwrap();
|
||||
|
||||
// Check results
|
||||
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_042, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&new_account_data).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_new_trade_request() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (client, owner) = create_client(bank, mint_keypair);
|
||||
|
||||
let (trade, src, dst) = trade(
|
||||
&client,
|
||||
&owner,
|
||||
Direction::To,
|
||||
TokenPair::AB,
|
||||
Token::A,
|
||||
42,
|
||||
2,
|
||||
1000,
|
||||
);
|
||||
|
||||
let trade_account_data = client.get_account_data(&trade).unwrap().unwrap();
|
||||
let src_account_data = client.get_account_data(&src).unwrap().unwrap();
|
||||
let dst_account_data = client.get_account_data(&dst).unwrap().unwrap();
|
||||
|
||||
// check results
|
||||
|
||||
assert_eq!(
|
||||
TradeOrderInfo {
|
||||
owner: owner.pubkey(),
|
||||
direction: Direction::To,
|
||||
pair: TokenPair::AB,
|
||||
tokens: 2,
|
||||
price: 1000,
|
||||
src_account: src,
|
||||
dst_account: dst
|
||||
},
|
||||
ExchangeProcessor::deserialize_trade(&trade_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_040, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&src_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&dst_account_data).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exchange_new_swap_request() {
|
||||
solana_logger::setup();
|
||||
let (bank, mint_keypair) = create_bank(10_000);
|
||||
let (client, owner) = create_client(bank, mint_keypair);
|
||||
|
||||
let swap = create_account(&client, &owner);
|
||||
let profit = create_token_account(&client, &owner);
|
||||
let (to_trade, to_src, to_dst) = trade(
|
||||
&client,
|
||||
&owner,
|
||||
Direction::To,
|
||||
TokenPair::AB,
|
||||
Token::A,
|
||||
2,
|
||||
2,
|
||||
2000,
|
||||
);
|
||||
let (from_trade, from_src, from_dst) = trade(
|
||||
&client,
|
||||
&owner,
|
||||
Direction::From,
|
||||
TokenPair::AB,
|
||||
Token::B,
|
||||
3,
|
||||
3,
|
||||
3000,
|
||||
);
|
||||
|
||||
let instruction = exchange_instruction::swap_request(
|
||||
&owner.pubkey(),
|
||||
&swap,
|
||||
&to_trade,
|
||||
&from_trade,
|
||||
&to_dst,
|
||||
&from_dst,
|
||||
&profit,
|
||||
);
|
||||
client
|
||||
.send_instruction(&owner, instruction)
|
||||
.expect(&format!("{}:{}", line!(), file!()));
|
||||
|
||||
let to_trade_account_data = client.get_account_data(&to_trade).unwrap().unwrap();
|
||||
let to_src_account_data = client.get_account_data(&to_src).unwrap().unwrap();
|
||||
let to_dst_account_data = client.get_account_data(&to_dst).unwrap().unwrap();
|
||||
let from_trade_account_data = client.get_account_data(&from_trade).unwrap().unwrap();
|
||||
let from_src_account_data = client.get_account_data(&from_src).unwrap().unwrap();
|
||||
let from_dst_account_data = client.get_account_data(&from_dst).unwrap().unwrap();
|
||||
let profit_account_data = client.get_account_data(&profit).unwrap().unwrap();
|
||||
let swap_account_data = client.get_account_data(&swap).unwrap().unwrap();
|
||||
|
||||
// check results
|
||||
|
||||
assert_eq!(
|
||||
TradeOrderInfo {
|
||||
owner: owner.pubkey(),
|
||||
direction: Direction::To,
|
||||
pair: TokenPair::AB,
|
||||
tokens: 1,
|
||||
price: 2000,
|
||||
src_account: to_src,
|
||||
dst_account: to_dst
|
||||
},
|
||||
ExchangeProcessor::deserialize_trade(&to_trade_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&to_src_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_002, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&to_dst_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TradeOrderInfo {
|
||||
owner: owner.pubkey(),
|
||||
direction: Direction::From,
|
||||
pair: TokenPair::AB,
|
||||
tokens: 0,
|
||||
price: 3000,
|
||||
src_account: from_src,
|
||||
dst_account: from_dst
|
||||
},
|
||||
ExchangeProcessor::deserialize_trade(&from_trade_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&from_src_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_001, 100_000, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&from_dst_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TokenAccountInfo::default()
|
||||
.owner(&owner.pubkey())
|
||||
.tokens(100_000, 100_001, 100_000, 100_000),
|
||||
ExchangeProcessor::deserialize_account(&profit_account_data).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
TradeSwapInfo {
|
||||
pair: TokenPair::AB,
|
||||
to_trade_order: to_trade,
|
||||
from_trade_order: from_trade,
|
||||
primary_tokens: 1,
|
||||
primary_price: 2000,
|
||||
secondary_tokens: 3,
|
||||
secondary_price: 3000,
|
||||
},
|
||||
deserialize_swap(&swap_account_data)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::{error, fmt};
|
||||
|
||||
/// Fixed-point scaler, 10 = one base 10 digit to the right of the decimal, 100 = 2, ...
|
||||
/// Used by both price and amount in their fixed point representation
|
||||
pub const SCALER: u64 = 1000;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum ExchangeError {
|
||||
InvalidTrade(String),
|
||||
}
|
||||
impl error::Error for ExchangeError {}
|
||||
impl fmt::Display for ExchangeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ExchangeError::InvalidTrade(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported token types
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum Token {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
}
|
||||
impl Default for Token {
|
||||
fn default() -> Self {
|
||||
Token::A
|
||||
}
|
||||
}
|
||||
|
||||
// Values of tokens, could be quantities, prices, etc...
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct Tokens {
|
||||
pub A: u64,
|
||||
pub B: u64,
|
||||
pub C: u64,
|
||||
pub D: u64,
|
||||
}
|
||||
impl Tokens {
|
||||
pub fn new(a: u64, b: u64, c: u64, d: u64) -> Self {
|
||||
Self {
|
||||
A: a,
|
||||
B: b,
|
||||
C: c,
|
||||
D: d,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::ops::Index<Token> for Tokens {
|
||||
type Output = u64;
|
||||
fn index(&self, t: Token) -> &u64 {
|
||||
match t {
|
||||
Token::A => &self.A,
|
||||
Token::B => &self.B,
|
||||
Token::C => &self.C,
|
||||
Token::D => &self.D,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::ops::IndexMut<Token> for Tokens {
|
||||
fn index_mut(&mut self, t: Token) -> &mut u64 {
|
||||
match t {
|
||||
Token::A => &mut self.A,
|
||||
Token::B => &mut self.B,
|
||||
Token::C => &mut self.C,
|
||||
Token::D => &mut self.D,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[allow(non_snake_case)]
|
||||
pub enum TokenPair {
|
||||
AB,
|
||||
AC,
|
||||
AD,
|
||||
BC,
|
||||
BD,
|
||||
CD,
|
||||
}
|
||||
impl Default for TokenPair {
|
||||
fn default() -> Self {
|
||||
TokenPair::AB
|
||||
}
|
||||
}
|
||||
impl TokenPair {
|
||||
pub fn primary(self) -> Token {
|
||||
match self {
|
||||
TokenPair::AB | TokenPair::AC | TokenPair::AD => Token::A,
|
||||
TokenPair::BC | TokenPair::BD => Token::B,
|
||||
TokenPair::CD => Token::C,
|
||||
}
|
||||
}
|
||||
pub fn secondary(self) -> Token {
|
||||
match self {
|
||||
TokenPair::AB => Token::B,
|
||||
TokenPair::AC | TokenPair::BC => Token::C,
|
||||
TokenPair::AD | TokenPair::BD | TokenPair::CD => Token::D,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Token accounts are populated with this structure
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TokenAccountInfo {
|
||||
/// Investor who owns this account
|
||||
pub owner: Pubkey,
|
||||
/// Current number of tokens this account holds
|
||||
pub tokens: Tokens,
|
||||
}
|
||||
impl TokenAccountInfo {
|
||||
pub fn owner(mut self, owner: &Pubkey) -> Self {
|
||||
self.owner = *owner;
|
||||
self
|
||||
}
|
||||
pub fn tokens(mut self, a: u64, b: u64, c: u64, d: u64) -> Self {
|
||||
self.tokens = Tokens {
|
||||
A: a,
|
||||
B: b,
|
||||
C: c,
|
||||
D: d,
|
||||
};
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Direction of the exchange between two tokens in a pair
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum Direction {
|
||||
/// Trade first token type (primary) in the pair 'To' the second
|
||||
To,
|
||||
/// Trade first token type in the pair 'From' the second (secondary)
|
||||
From,
|
||||
}
|
||||
impl fmt::Display for Direction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Direction::To => write!(f, "T")?,
|
||||
Direction::From => write!(f, "F")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade accounts are populated with this structure
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TradeOrderInfo {
|
||||
/// Owner of the trade order
|
||||
pub owner: Pubkey,
|
||||
/// Direction of the exchange
|
||||
pub direction: Direction,
|
||||
/// Token pair indicating two tokens to exchange, first is primary
|
||||
pub pair: TokenPair,
|
||||
/// Number of tokens to exchange; primary or secondary depending on direction
|
||||
pub tokens: u64,
|
||||
/// Scaled price of the secondary token given the primary is equal to the scale value
|
||||
/// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens
|
||||
pub price: u64,
|
||||
/// account which the tokens were source from. The trade account holds the tokens in escrow
|
||||
/// until either one or more part of a swap or the trade is cancelled.
|
||||
pub src_account: Pubkey,
|
||||
/// account which the tokens the tokens will be deposited into on a successful trade
|
||||
pub dst_account: Pubkey,
|
||||
}
|
||||
impl Default for TradeOrderInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
owner: Pubkey::default(),
|
||||
pair: TokenPair::AB,
|
||||
direction: Direction::To,
|
||||
tokens: 0,
|
||||
price: 0,
|
||||
src_account: Pubkey::default(),
|
||||
dst_account: Pubkey::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TradeOrderInfo {
|
||||
pub fn pair(mut self, pair: TokenPair) -> Self {
|
||||
self.pair = pair;
|
||||
self
|
||||
}
|
||||
pub fn direction(mut self, direction: Direction) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
pub fn tokens(mut self, tokens: u64) -> Self {
|
||||
self.tokens = tokens;
|
||||
self
|
||||
}
|
||||
pub fn price(mut self, price: u64) -> Self {
|
||||
self.price = price;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_trade(direction: Direction, tokens: u64, price: u64) -> Result<(), ExchangeError> {
|
||||
match direction {
|
||||
Direction::To => {
|
||||
if tokens * price / SCALER == 0 {
|
||||
Err(ExchangeError::InvalidTrade(format!(
|
||||
"To trade of {} for {}/{} results in 0 tradeable tokens",
|
||||
tokens, SCALER, price
|
||||
)))?
|
||||
}
|
||||
}
|
||||
Direction::From => {
|
||||
if tokens * SCALER / price == 0 {
|
||||
Err(ExchangeError::InvalidTrade(format!(
|
||||
"From trade of {} for {}?{} results in 0 tradeable tokens",
|
||||
tokens, SCALER, price
|
||||
)))?
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Swap accounts are populated with this structure
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TradeSwapInfo {
|
||||
/// Pair swapped
|
||||
pub pair: TokenPair,
|
||||
/// `To` trade order
|
||||
pub to_trade_order: Pubkey,
|
||||
/// `From` trade order
|
||||
pub from_trade_order: Pubkey,
|
||||
/// Number of primary tokens exchanged
|
||||
pub primary_tokens: u64,
|
||||
/// Price the primary tokens were exchanged for
|
||||
pub primary_price: u64,
|
||||
/// Number of secondary tokens exchanged
|
||||
pub secondary_tokens: u64,
|
||||
/// Price the secondary tokens were exchanged for
|
||||
pub secondary_price: u64,
|
||||
}
|
||||
|
||||
/// Type of exchange account, account's user data is populated with this enum
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum ExchangeState {
|
||||
/// Account's Userdata is unallocated
|
||||
Unallocated,
|
||||
// Token account
|
||||
Account(TokenAccountInfo),
|
||||
// Trade order account
|
||||
Trade(TradeOrderInfo),
|
||||
// Swap account
|
||||
Swap(TradeSwapInfo),
|
||||
Invalid,
|
||||
}
|
||||
impl Default for ExchangeState {
|
||||
fn default() -> Self {
|
||||
ExchangeState::Unallocated
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
pub mod exchange_instruction;
|
||||
pub mod exchange_processor;
|
||||
pub mod exchange_state;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub const EXCHANGE_PROGRAM_ID: [u8; 32] = [
|
||||
134, 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,
|
||||
];
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == EXCHANGE_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&EXCHANGE_PROGRAM_ID)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "solana-exchange-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana exchange program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-exchange-api = { path = "../exchange_api", version = "0.14.0" }
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_exchange_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_exchange_api::exchange_processor::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "solana-failure-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana failure program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
log = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_failure_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,14 +0,0 @@
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::solana_entrypoint;
|
||||
|
||||
solana_entrypoint!(entrypoint);
|
||||
fn entrypoint(
|
||||
_program_id: &Pubkey,
|
||||
_keyed_accounts: &mut [KeyedAccount],
|
||||
_data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
Err(InstructionError::GenericError)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_runtime::loader_utils::{create_invoke_instruction, load_program};
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::native_loader;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
use solana_sdk::transaction::TransactionError;
|
||||
|
||||
#[test]
|
||||
fn test_program_native_failure() {
|
||||
let (genesis_block, alice_keypair) = GenesisBlock::new(50);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let program = "solana_failure_program".as_bytes().to_vec();
|
||||
let program_id = load_program(&bank_client, &alice_keypair, &native_loader::id(), program);
|
||||
|
||||
// Call user program
|
||||
let instruction = create_invoke_instruction(alice_keypair.pubkey(), program_id, &1u8);
|
||||
assert_eq!(
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::GenericError)
|
||||
);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "solana-noop-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana noop program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
log = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_noop_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,20 +0,0 @@
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::solana_entrypoint;
|
||||
|
||||
solana_entrypoint!(entrypoint);
|
||||
fn entrypoint(
|
||||
program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
info!("noop: program_id: {:?}", program_id);
|
||||
info!("noop: keyed_accounts: {:#?}", keyed_accounts);
|
||||
info!("noop: data: {:?}", data);
|
||||
info!("noop: tick_height: {:?}", tick_height);
|
||||
Ok(())
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_runtime::loader_utils::{create_invoke_instruction, load_program};
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::native_loader;
|
||||
use solana_sdk::signature::KeypairUtil;
|
||||
|
||||
#[test]
|
||||
fn test_program_native_noop() {
|
||||
solana_logger::setup();
|
||||
|
||||
let (genesis_block, alice_keypair) = GenesisBlock::new(50);
|
||||
let bank = Bank::new(&genesis_block);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let program = "solana_noop_program".as_bytes().to_vec();
|
||||
let program_id = load_program(&bank_client, &alice_keypair, &native_loader::id(), program);
|
||||
|
||||
// Call user program
|
||||
let instruction = create_invoke_instruction(alice_keypair.pubkey(), program_id, &1u8);
|
||||
bank_client
|
||||
.send_instruction(&alice_keypair, instruction)
|
||||
.unwrap();
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "solana-stake-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Stake program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../../metrics", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-vote-api = { path = "../vote_api", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_stake_api"
|
||||
crate-type = ["lib"]
|
@ -1,17 +0,0 @@
|
||||
pub mod stake_instruction;
|
||||
pub mod stake_state;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const STAKE_PROGRAM_ID: [u8; 32] = [
|
||||
135, 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,
|
||||
];
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == STAKE_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&STAKE_PROGRAM_ID)
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
use crate::id;
|
||||
use crate::stake_state::{StakeAccount, StakeState};
|
||||
use bincode::deserialize;
|
||||
use log::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum StakeInstruction {
|
||||
/// `Delegate` or `Assign` a stake account to a particular node
|
||||
/// expects 2 KeyedAccounts:
|
||||
/// StakeAccount to be updated
|
||||
/// VoteAccount to which this Stake will be delegated
|
||||
DelegateStake,
|
||||
|
||||
/// Redeem credits in the stake account
|
||||
/// expects 3 KeyedAccounts: the StakeAccount to be updated
|
||||
/// and the VoteAccount to which this Stake will be delegated
|
||||
RedeemVoteCredits,
|
||||
}
|
||||
|
||||
pub fn create_account(from_id: &Pubkey, staker_id: &Pubkey, lamports: u64) -> Vec<Instruction> {
|
||||
vec![system_instruction::create_account(
|
||||
from_id,
|
||||
staker_id,
|
||||
lamports,
|
||||
std::mem::size_of::<StakeState>() as u64,
|
||||
&id(),
|
||||
)]
|
||||
}
|
||||
|
||||
pub fn redeem_vote_credits(
|
||||
from_id: &Pubkey,
|
||||
mining_pool_id: &Pubkey,
|
||||
stake_id: &Pubkey,
|
||||
vote_id: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*from_id, true),
|
||||
AccountMeta::new(*mining_pool_id, false),
|
||||
AccountMeta::new(*stake_id, false),
|
||||
AccountMeta::new(*vote_id, false),
|
||||
];
|
||||
Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas)
|
||||
}
|
||||
|
||||
pub fn delegate_stake(from_id: &Pubkey, stake_id: &Pubkey, vote_id: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*from_id, true),
|
||||
AccountMeta::new(*stake_id, true),
|
||||
AccountMeta::new(*vote_id, false),
|
||||
];
|
||||
Instruction::new(id(), &StakeInstruction::DelegateStake, account_metas)
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
trace!("process_instruction: {:?}", data);
|
||||
trace!("keyed_accounts: {:?}", keyed_accounts);
|
||||
|
||||
if keyed_accounts.len() < 3 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
|
||||
// 0th index is the guy who paid for the transaction
|
||||
let (me, rest) = &mut keyed_accounts.split_at_mut(2);
|
||||
|
||||
let me = &mut me[1];
|
||||
|
||||
// TODO: data-driven unpack and dispatch of KeyedAccounts
|
||||
match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? {
|
||||
StakeInstruction::DelegateStake => {
|
||||
if rest.len() != 1 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
let vote = &rest[0];
|
||||
me.delegate_stake(vote)
|
||||
}
|
||||
StakeInstruction::RedeemVoteCredits => {
|
||||
if rest.len() != 2 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
let (stake, vote) = rest.split_at_mut(1);
|
||||
let stake = &mut stake[0];
|
||||
let vote = &mut vote[0];
|
||||
|
||||
me.redeem_vote_credits(stake, vote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bincode::serialize;
|
||||
use solana_sdk::account::Account;
|
||||
|
||||
#[test]
|
||||
fn test_stake_process_instruction_decode_bail() {
|
||||
// these will not call stake_state, have bogus contents
|
||||
|
||||
// gets the first check
|
||||
assert_eq!(
|
||||
process_instruction(
|
||||
&Pubkey::default(),
|
||||
&mut [KeyedAccount::new(
|
||||
&Pubkey::default(),
|
||||
false,
|
||||
&mut Account::default(),
|
||||
)],
|
||||
&serialize(&StakeInstruction::DelegateStake).unwrap(),
|
||||
0,
|
||||
),
|
||||
Err(InstructionError::InvalidInstructionData),
|
||||
);
|
||||
|
||||
// gets the check in delegate_stake
|
||||
assert_eq!(
|
||||
process_instruction(
|
||||
&Pubkey::default(),
|
||||
&mut [
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
],
|
||||
&serialize(&StakeInstruction::DelegateStake).unwrap(),
|
||||
0,
|
||||
),
|
||||
Err(InstructionError::InvalidInstructionData),
|
||||
);
|
||||
|
||||
// gets the check in redeem_vote_credits
|
||||
assert_eq!(
|
||||
process_instruction(
|
||||
&Pubkey::default(),
|
||||
&mut [
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
],
|
||||
&serialize(&StakeInstruction::RedeemVoteCredits).unwrap(),
|
||||
0,
|
||||
),
|
||||
Err(InstructionError::InvalidInstructionData),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
//! Stake state
|
||||
//! * delegate stakes to vote accounts
|
||||
//! * keep track of rewards
|
||||
//! * own mining pools
|
||||
|
||||
//use crate::{check_id, id};
|
||||
//use log::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::instruction_processor_utils::State;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_vote_api::vote_state::VoteState;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub enum StakeState {
|
||||
Delegate {
|
||||
voter_id: Pubkey,
|
||||
credits_observed: u64,
|
||||
},
|
||||
MiningPool,
|
||||
}
|
||||
|
||||
impl Default for StakeState {
|
||||
fn default() -> Self {
|
||||
StakeState::Delegate {
|
||||
voter_id: Pubkey::default(),
|
||||
credits_observed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: trusted values of network parameters come from where?
|
||||
const TICKS_PER_SECOND: f64 = 10f64;
|
||||
const TICKS_PER_SLOT: f64 = 8f64;
|
||||
|
||||
// credits/yr or slots/yr is seconds/year * ticks/second * slots/tick
|
||||
const CREDITS_PER_YEAR: f64 = (365f64 * 24f64 * 3600f64) * TICKS_PER_SECOND / TICKS_PER_SLOT;
|
||||
|
||||
// TODO: 20% is a niiice rate... TODO: make this a member of MiningPool?
|
||||
const STAKE_REWARD_TARGET_RATE: f64 = 0.20;
|
||||
|
||||
#[cfg(test)]
|
||||
const STAKE_GETS_PAID_EVERY_VOTE: u64 = 200_000_000; // if numbers above move, fix this
|
||||
|
||||
impl StakeState {
|
||||
pub fn calculate_rewards(
|
||||
credits_observed: u64,
|
||||
stake: u64,
|
||||
vote_state: &VoteState,
|
||||
) -> Option<(u64, u64)> {
|
||||
if credits_observed >= vote_state.credits() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let total_rewards = stake as f64
|
||||
* STAKE_REWARD_TARGET_RATE
|
||||
* (vote_state.credits() - credits_observed) as f64
|
||||
/ CREDITS_PER_YEAR;
|
||||
|
||||
// don't bother trying to collect fractional lamports
|
||||
if total_rewards < 1f64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(total_rewards);
|
||||
|
||||
if (voter_rewards < 1f64 || staker_rewards < 1f64) && is_split {
|
||||
// don't bother trying to collect fractional lamports
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((voter_rewards as u64, staker_rewards as u64))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StakeAccount {
|
||||
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError>;
|
||||
fn redeem_vote_credits(
|
||||
&mut self,
|
||||
stake_account: &mut KeyedAccount,
|
||||
vote_account: &mut KeyedAccount,
|
||||
) -> Result<(), InstructionError>;
|
||||
}
|
||||
|
||||
impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError> {
|
||||
if self.signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
if let StakeState::Delegate { .. } = self.state()? {
|
||||
let vote_state: VoteState = vote_account.state()?;
|
||||
self.set_state(&StakeState::Delegate {
|
||||
voter_id: *vote_account.unsigned_key(),
|
||||
credits_observed: vote_state.credits(),
|
||||
})
|
||||
} else {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
fn redeem_vote_credits(
|
||||
&mut self,
|
||||
stake_account: &mut KeyedAccount,
|
||||
vote_account: &mut KeyedAccount,
|
||||
) -> Result<(), InstructionError> {
|
||||
if let (
|
||||
StakeState::MiningPool,
|
||||
StakeState::Delegate {
|
||||
voter_id,
|
||||
credits_observed,
|
||||
},
|
||||
) = (self.state()?, stake_account.state()?)
|
||||
{
|
||||
let vote_state: VoteState = vote_account.state()?;
|
||||
|
||||
if voter_id != *vote_account.unsigned_key() {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
if credits_observed > vote_state.credits() {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
|
||||
if let Some((stakers_reward, voters_reward)) = StakeState::calculate_rewards(
|
||||
credits_observed,
|
||||
stake_account.account.lamports,
|
||||
&vote_state,
|
||||
) {
|
||||
if self.account.lamports < (stakers_reward + voters_reward) {
|
||||
return Err(InstructionError::UnbalancedInstruction);
|
||||
}
|
||||
self.account.lamports -= stakers_reward + voters_reward;
|
||||
stake_account.account.lamports += stakers_reward;
|
||||
vote_account.account.lamports += voters_reward;
|
||||
|
||||
stake_account.set_state(&StakeState::Delegate {
|
||||
voter_id,
|
||||
credits_observed: vote_state.credits(),
|
||||
})
|
||||
} else {
|
||||
// not worth collecting
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_vote_api::vote_state::{self, Vote};
|
||||
|
||||
#[test]
|
||||
fn test_stake_delegate_stake() {
|
||||
let vote_keypair = Keypair::new();
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..1000 {
|
||||
vote_state.process_vote(&Vote::new(i));
|
||||
}
|
||||
|
||||
let vote_pubkey = vote_keypair.pubkey();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
let stake_pubkey = Pubkey::default();
|
||||
let mut stake_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
|
||||
let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account);
|
||||
|
||||
assert_eq!(
|
||||
stake_keyed_account.delegate_stake(&vote_keyed_account),
|
||||
Err(InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
|
||||
let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account);
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account)
|
||||
.is_ok());
|
||||
|
||||
let stake_state: StakeState = stake_keyed_account.state().unwrap();
|
||||
assert_eq!(
|
||||
stake_state,
|
||||
StakeState::Delegate {
|
||||
voter_id: vote_keypair.pubkey(),
|
||||
credits_observed: vote_state.credits()
|
||||
}
|
||||
);
|
||||
let stake_state = StakeState::MiningPool;
|
||||
stake_keyed_account.set_state(&stake_state).unwrap();
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn test_stake_state_calculate_rewards() {
|
||||
let mut vote_state = VoteState::default();
|
||||
let mut vote_i = 0;
|
||||
|
||||
// put a credit in the vote_state
|
||||
while vote_state.credits() == 0 {
|
||||
vote_state.process_vote(&Vote::new(vote_i));
|
||||
vote_i += 1;
|
||||
}
|
||||
// this guy can't collect now, not enough stake to get paid on 1 credit
|
||||
assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state));
|
||||
// this guy can
|
||||
assert_eq!(
|
||||
Some((0, 1)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
// but, there's not enough to split
|
||||
vote_state.commission = std::u32::MAX / 2;
|
||||
assert_eq!(
|
||||
None,
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
|
||||
// put more credit in the vote_state
|
||||
while vote_state.credits() < 10 {
|
||||
vote_state.process_vote(&Vote::new(vote_i));
|
||||
vote_i += 1;
|
||||
}
|
||||
vote_state.commission = 0;
|
||||
assert_eq!(
|
||||
Some((0, 10)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
vote_state.commission = std::u32::MAX;
|
||||
assert_eq!(
|
||||
Some((10, 0)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
vote_state.commission = std::u32::MAX / 2;
|
||||
assert_eq!(
|
||||
Some((5, 5)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
// not even enough stake to get paid on 10 credits...
|
||||
assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stake_redeem_vote_credits() {
|
||||
let vote_keypair = Keypair::new();
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..1000 {
|
||||
vote_state.process_vote(&Vote::new(i));
|
||||
}
|
||||
|
||||
let vote_pubkey = vote_keypair.pubkey();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
let pubkey = Pubkey::default();
|
||||
let mut stake_account = Account::new(
|
||||
STAKE_GETS_PAID_EVERY_VOTE,
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
);
|
||||
let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account);
|
||||
|
||||
// delegate the stake
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account)
|
||||
.is_ok());
|
||||
|
||||
let mut mining_pool_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut mining_pool_keyed_account =
|
||||
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
||||
|
||||
// not a mining pool yet...
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
|
||||
mining_pool_keyed_account
|
||||
.set_state(&StakeState::MiningPool)
|
||||
.unwrap();
|
||||
|
||||
// no movement in vote account, so no redemption needed
|
||||
assert!(mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account)
|
||||
.is_ok());
|
||||
|
||||
// move the vote account forward
|
||||
vote_state.process_vote(&Vote::new(1000));
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
// now, no lamports in the pool!
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::UnbalancedInstruction)
|
||||
);
|
||||
|
||||
// add a lamport to pool
|
||||
mining_pool_keyed_account.account.lamports = 2;
|
||||
assert!(mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account)
|
||||
.is_ok()); // yay
|
||||
|
||||
// lamports only shifted around, none made or lost
|
||||
assert_eq!(
|
||||
2 + 100 + STAKE_GETS_PAID_EVERY_VOTE,
|
||||
mining_pool_account.lamports + vote_account.lamports + stake_account.lamports
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stake_redeem_vote_credits_vote_errors() {
|
||||
let vote_keypair = Keypair::new();
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..1000 {
|
||||
vote_state.process_vote(&Vote::new(i));
|
||||
}
|
||||
|
||||
let vote_pubkey = vote_keypair.pubkey();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
let pubkey = Pubkey::default();
|
||||
let mut stake_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account);
|
||||
|
||||
// delegate the stake
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account)
|
||||
.is_ok());
|
||||
|
||||
let mut mining_pool_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut mining_pool_keyed_account =
|
||||
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
||||
mining_pool_keyed_account
|
||||
.set_state(&StakeState::MiningPool)
|
||||
.unwrap();
|
||||
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..100 {
|
||||
// go back in time, previous state had 1000 votes
|
||||
vote_state.process_vote(&Vote::new(i));
|
||||
}
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
// voter credits lower than stake_delegate credits... TODO: is this an error?
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
|
||||
let vote1_keypair = Keypair::new();
|
||||
let vote1_pubkey = vote1_keypair.pubkey();
|
||||
let mut vote1_account =
|
||||
vote_state::create_account(&vote1_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote1_keyed_account = KeyedAccount::new(&vote1_pubkey, false, &mut vote1_account);
|
||||
vote1_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
// wrong voter_id...
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote1_keyed_account),
|
||||
Err(InstructionError::InvalidArgument)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-stake-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana stake program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-stake-api = { path = "../stake_api", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_stake_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_stake_api::stake_instruction::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "solana-storage-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Storage program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_storage_api"
|
||||
crate-type = ["lib"]
|
@ -1,24 +0,0 @@
|
||||
pub mod storage_contract;
|
||||
pub mod storage_instruction;
|
||||
pub mod storage_processor;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub const ENTRIES_PER_SEGMENT: u64 = 16;
|
||||
|
||||
pub fn get_segment_from_entry(entry_height: u64) -> usize {
|
||||
(entry_height / ENTRIES_PER_SEGMENT) as usize
|
||||
}
|
||||
|
||||
const STORAGE_PROGRAM_ID: [u8; 32] = [
|
||||
130, 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,
|
||||
];
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == STORAGE_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&STORAGE_PROGRAM_ID)
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
use crate::{get_segment_from_entry, ENTRIES_PER_SEGMENT};
|
||||
use log::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::instruction_processor_utils::State;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Signature;
|
||||
use std::cmp;
|
||||
|
||||
pub const TOTAL_VALIDATOR_REWARDS: u64 = 1;
|
||||
pub const TOTAL_REPLICATOR_REWARDS: u64 = 1;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum ProofStatus {
|
||||
Skipped,
|
||||
Valid,
|
||||
NotValid,
|
||||
}
|
||||
|
||||
impl Default for ProofStatus {
|
||||
fn default() -> Self {
|
||||
ProofStatus::Skipped
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Proof {
|
||||
pub id: Pubkey,
|
||||
pub signature: Signature,
|
||||
pub sha_state: Hash,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CheckedProof {
|
||||
pub proof: Proof,
|
||||
pub status: ProofStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum StorageContract {
|
||||
//don't move this
|
||||
Default,
|
||||
|
||||
ValidatorStorage {
|
||||
entry_height: u64,
|
||||
hash: Hash,
|
||||
lockout_validations: Vec<Vec<CheckedProof>>,
|
||||
reward_validations: Vec<Vec<CheckedProof>>,
|
||||
},
|
||||
ReplicatorStorage {
|
||||
proofs: Vec<Proof>,
|
||||
reward_validations: Vec<Vec<CheckedProof>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct StorageAccount<'a> {
|
||||
account: &'a mut Account,
|
||||
}
|
||||
|
||||
impl<'a> StorageAccount<'a> {
|
||||
pub fn new(account: &'a mut Account) -> Self {
|
||||
Self { account }
|
||||
}
|
||||
|
||||
pub fn submit_mining_proof(
|
||||
&mut self,
|
||||
id: Pubkey,
|
||||
sha_state: Hash,
|
||||
entry_height: u64,
|
||||
signature: Signature,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::Default = storage_contract {
|
||||
*storage_contract = StorageContract::ReplicatorStorage {
|
||||
proofs: vec![],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
};
|
||||
|
||||
if let StorageContract::ReplicatorStorage { proofs, .. } = &mut storage_contract {
|
||||
let segment_index = get_segment_from_entry(entry_height);
|
||||
if segment_index > proofs.len() || proofs.is_empty() {
|
||||
proofs.resize(cmp::max(1, segment_index), Proof::default());
|
||||
}
|
||||
|
||||
if segment_index > proofs.len() {
|
||||
// only possible if usize max < u64 max
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Mining proof submitted with contract {:?} entry_height: {}",
|
||||
sha_state, entry_height
|
||||
);
|
||||
|
||||
let proof_info = Proof {
|
||||
id,
|
||||
sha_state,
|
||||
signature,
|
||||
};
|
||||
proofs[segment_index] = proof_info;
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advertise_storage_recent_blockhash(
|
||||
&mut self,
|
||||
hash: Hash,
|
||||
entry_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::Default = storage_contract {
|
||||
*storage_contract = StorageContract::ValidatorStorage {
|
||||
entry_height: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: vec![],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
};
|
||||
|
||||
if let StorageContract::ValidatorStorage {
|
||||
entry_height: state_entry_height,
|
||||
hash: state_hash,
|
||||
reward_validations,
|
||||
lockout_validations,
|
||||
} = &mut storage_contract
|
||||
{
|
||||
let original_segments = *state_entry_height / ENTRIES_PER_SEGMENT;
|
||||
let segments = entry_height / ENTRIES_PER_SEGMENT;
|
||||
debug!(
|
||||
"advertise new last id segments: {} orig: {}",
|
||||
segments, original_segments
|
||||
);
|
||||
if segments <= original_segments {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
*state_entry_height = entry_height;
|
||||
*state_hash = hash;
|
||||
|
||||
// move lockout_validations to reward_validations
|
||||
*reward_validations = lockout_validations.clone();
|
||||
lockout_validations.clear();
|
||||
lockout_validations.resize(segments as usize, Vec::new());
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn proof_validation(
|
||||
&mut self,
|
||||
entry_height: u64,
|
||||
proofs: Vec<CheckedProof>,
|
||||
replicator_accounts: &mut [StorageAccount],
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::Default = storage_contract {
|
||||
*storage_contract = StorageContract::ValidatorStorage {
|
||||
entry_height: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: vec![],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
};
|
||||
|
||||
if let StorageContract::ValidatorStorage {
|
||||
entry_height: current_entry_height,
|
||||
lockout_validations,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
if entry_height >= *current_entry_height {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
let segment_index = get_segment_from_entry(entry_height);
|
||||
let mut previous_proofs = replicator_accounts
|
||||
.iter_mut()
|
||||
.filter_map(|account| {
|
||||
account
|
||||
.account
|
||||
.state()
|
||||
.ok()
|
||||
.map(move |contract| match contract {
|
||||
StorageContract::ReplicatorStorage { proofs, .. } => {
|
||||
Some((account, proofs[segment_index].clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if previous_proofs.len() != proofs.len() {
|
||||
// don't have all the accounts to validate the proofs against
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
let mut valid_proofs: Vec<_> = proofs
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, entry)| {
|
||||
let (account, proof) = &mut previous_proofs[i];
|
||||
if process_validation(account, segment_index, &proof, &entry).is_ok() {
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// allow validators to store successful validations
|
||||
lockout_validations[segment_index].append(&mut valid_proofs);
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claim_storage_reward(
|
||||
&mut self,
|
||||
entry_height: u64,
|
||||
tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::Default = storage_contract {
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
};
|
||||
|
||||
if let StorageContract::ValidatorStorage {
|
||||
reward_validations, ..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
let claims_index = get_segment_from_entry(entry_height);
|
||||
let _num_validations = count_valid_proofs(&reward_validations[claims_index]);
|
||||
// TODO can't just create lamports out of thin air
|
||||
// self.account.lamports += TOTAL_VALIDATOR_REWARDS * num_validations;
|
||||
reward_validations.clear();
|
||||
self.account.set_state(storage_contract)
|
||||
} else if let StorageContract::ReplicatorStorage {
|
||||
reward_validations, ..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
// if current tick height is a full segment away? then allow reward collection
|
||||
// storage needs to move to tick heights too, until then this makes little sense
|
||||
let current_index = get_segment_from_entry(tick_height);
|
||||
let claims_index = get_segment_from_entry(entry_height);
|
||||
if current_index <= claims_index || claims_index >= reward_validations.len() {
|
||||
debug!(
|
||||
"current {:?}, claim {:?}, rewards {:?}",
|
||||
current_index,
|
||||
claims_index,
|
||||
reward_validations.len()
|
||||
);
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
let _num_validations = count_valid_proofs(&reward_validations[claims_index]);
|
||||
// TODO can't just create lamports out of thin air
|
||||
// self.account.lamports += num_validations
|
||||
// * TOTAL_REPLICATOR_REWARDS
|
||||
// * (num_validations / reward_validations[claims_index].len() as u64);
|
||||
reward_validations.clear();
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the result of a proof validation into the replicator account
|
||||
fn store_validation_result(
|
||||
storage_account: &mut StorageAccount,
|
||||
segment_index: usize,
|
||||
status: ProofStatus,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = storage_account.account.state()?;
|
||||
match &mut storage_contract {
|
||||
StorageContract::ReplicatorStorage {
|
||||
proofs,
|
||||
reward_validations,
|
||||
..
|
||||
} => {
|
||||
if segment_index >= proofs.len() {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
if segment_index > reward_validations.len() || reward_validations.is_empty() {
|
||||
reward_validations.resize(cmp::max(1, segment_index), vec![]);
|
||||
}
|
||||
let result = proofs[segment_index].clone();
|
||||
reward_validations[segment_index].push(CheckedProof {
|
||||
proof: result,
|
||||
status,
|
||||
});
|
||||
}
|
||||
_ => return Err(InstructionError::InvalidAccountData),
|
||||
}
|
||||
storage_account.account.set_state(&storage_contract)
|
||||
}
|
||||
|
||||
fn count_valid_proofs(proofs: &[CheckedProof]) -> u64 {
|
||||
let mut num = 0;
|
||||
for proof in proofs {
|
||||
if let ProofStatus::Valid = proof.status {
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
num
|
||||
}
|
||||
|
||||
fn process_validation(
|
||||
account: &mut StorageAccount,
|
||||
segment_index: usize,
|
||||
proof: &Proof,
|
||||
checked_proof: &CheckedProof,
|
||||
) -> Result<(), InstructionError> {
|
||||
store_validation_result(account, segment_index, checked_proof.status.clone())?;
|
||||
if proof.signature != checked_proof.proof.signature
|
||||
|| checked_proof.status != ProofStatus::Valid
|
||||
{
|
||||
return Err(InstructionError::GenericError);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
|
||||
#[test]
|
||||
fn test_account_data() {
|
||||
solana_logger::setup();
|
||||
let mut account = Account::default();
|
||||
account.data.resize(4 * 1024, 0);
|
||||
let storage_account = StorageAccount::new(&mut account);
|
||||
// pretend it's a validator op code
|
||||
let mut contract = storage_account.account.state().unwrap();
|
||||
if let StorageContract::ValidatorStorage { .. } = contract {
|
||||
assert!(true)
|
||||
}
|
||||
if let StorageContract::ReplicatorStorage { .. } = &mut contract {
|
||||
panic!("Contract should not decode into two types");
|
||||
}
|
||||
|
||||
contract = StorageContract::ValidatorStorage {
|
||||
entry_height: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: vec![],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
storage_account.account.set_state(&contract).unwrap();
|
||||
if let StorageContract::ReplicatorStorage { .. } = contract {
|
||||
panic!("Wrong contract type");
|
||||
}
|
||||
contract = StorageContract::ReplicatorStorage {
|
||||
proofs: vec![],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
storage_account.account.set_state(&contract).unwrap();
|
||||
if let StorageContract::ValidatorStorage { .. } = contract {
|
||||
panic!("Wrong contract type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_validation() {
|
||||
let mut account = StorageAccount {
|
||||
account: &mut Account {
|
||||
lamports: 0,
|
||||
data: vec![],
|
||||
owner: id(),
|
||||
executable: false,
|
||||
},
|
||||
};
|
||||
let segment_index = 0_usize;
|
||||
let proof = Proof {
|
||||
id: Pubkey::default(),
|
||||
signature: Signature::default(),
|
||||
sha_state: Hash::default(),
|
||||
};
|
||||
let mut checked_proof = CheckedProof {
|
||||
proof: proof.clone(),
|
||||
status: ProofStatus::Valid,
|
||||
};
|
||||
|
||||
// account has no space
|
||||
process_validation(&mut account, segment_index, &proof, &checked_proof).unwrap_err();
|
||||
|
||||
account.account.data.resize(4 * 1024, 0);
|
||||
let storage_contract = &mut account.account.state().unwrap();
|
||||
if let StorageContract::Default = storage_contract {
|
||||
*storage_contract = StorageContract::ReplicatorStorage {
|
||||
proofs: vec![proof.clone()],
|
||||
reward_validations: vec![],
|
||||
};
|
||||
};
|
||||
account.account.set_state(storage_contract).unwrap();
|
||||
|
||||
// proof is valid
|
||||
process_validation(&mut account, segment_index, &proof, &checked_proof).unwrap();
|
||||
|
||||
checked_proof.status = ProofStatus::NotValid;
|
||||
|
||||
// proof failed verification
|
||||
process_validation(&mut account, segment_index, &proof, &checked_proof).unwrap_err();
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
use crate::id;
|
||||
use crate::storage_contract::CheckedProof;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
// TODO maybe split this into StorageReplicator and StorageValidator
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum StorageInstruction {
|
||||
SubmitMiningProof {
|
||||
sha_state: Hash,
|
||||
entry_height: u64,
|
||||
signature: Signature,
|
||||
},
|
||||
AdvertiseStorageRecentBlockhash {
|
||||
hash: Hash,
|
||||
entry_height: u64,
|
||||
},
|
||||
ClaimStorageReward {
|
||||
entry_height: u64,
|
||||
},
|
||||
ProofValidation {
|
||||
entry_height: u64,
|
||||
proofs: Vec<CheckedProof>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn mining_proof(
|
||||
from_pubkey: &Pubkey,
|
||||
sha_state: Hash,
|
||||
entry_height: u64,
|
||||
signature: Signature,
|
||||
) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::SubmitMiningProof {
|
||||
sha_state,
|
||||
entry_height,
|
||||
signature,
|
||||
};
|
||||
let account_metas = vec![AccountMeta::new(*from_pubkey, true)];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn advertise_recent_blockhash(
|
||||
from_pubkey: &Pubkey,
|
||||
storage_hash: Hash,
|
||||
entry_height: u64,
|
||||
) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::AdvertiseStorageRecentBlockhash {
|
||||
hash: storage_hash,
|
||||
entry_height,
|
||||
};
|
||||
let account_metas = vec![AccountMeta::new(*from_pubkey, true)];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn proof_validation(
|
||||
from_pubkey: &Pubkey,
|
||||
entry_height: u64,
|
||||
proofs: Vec<CheckedProof>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![AccountMeta::new(*from_pubkey, true)];
|
||||
proofs.iter().for_each(|checked_proof| {
|
||||
account_metas.push(AccountMeta::new(checked_proof.proof.id, false))
|
||||
});
|
||||
let storage_instruction = StorageInstruction::ProofValidation {
|
||||
entry_height,
|
||||
proofs,
|
||||
};
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn reward_claim(from_pubkey: &Pubkey, entry_height: u64) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::ClaimStorageReward { entry_height };
|
||||
let account_metas = vec![AccountMeta::new(*from_pubkey, true)];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
@ -1,387 +0,0 @@
|
||||
//! storage program
|
||||
//! Receive mining proofs from miners, validate the answers
|
||||
//! and give reward for good proofs.
|
||||
|
||||
use crate::storage_contract::StorageAccount;
|
||||
use crate::storage_instruction::StorageInstruction;
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
let num_keyed_accounts = keyed_accounts.len();
|
||||
let (me, rest) = keyed_accounts.split_at_mut(1);
|
||||
|
||||
// accounts_keys[0] must be signed
|
||||
let storage_account_pubkey = me[0].signer_key();
|
||||
if storage_account_pubkey.is_none() {
|
||||
info!("account[0] is unsigned");
|
||||
Err(InstructionError::MissingRequiredSignature)?;
|
||||
}
|
||||
let storage_account_pubkey = *storage_account_pubkey.unwrap();
|
||||
|
||||
let mut storage_account = StorageAccount::new(&mut me[0].account);
|
||||
let mut rest: Vec<_> = rest
|
||||
.iter_mut()
|
||||
.map(|keyed_account| StorageAccount::new(&mut keyed_account.account))
|
||||
.collect();
|
||||
|
||||
match bincode::deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? {
|
||||
StorageInstruction::SubmitMiningProof {
|
||||
sha_state,
|
||||
entry_height,
|
||||
signature,
|
||||
} => {
|
||||
if num_keyed_accounts != 1 {
|
||||
Err(InstructionError::InvalidArgument)?;
|
||||
}
|
||||
storage_account.submit_mining_proof(
|
||||
storage_account_pubkey,
|
||||
sha_state,
|
||||
entry_height,
|
||||
signature,
|
||||
)
|
||||
}
|
||||
StorageInstruction::AdvertiseStorageRecentBlockhash { hash, entry_height } => {
|
||||
if num_keyed_accounts != 1 {
|
||||
// keyed_accounts[0] should be the main storage key
|
||||
// to access its data
|
||||
Err(InstructionError::InvalidArgument)?;
|
||||
}
|
||||
storage_account.advertise_storage_recent_blockhash(hash, entry_height)
|
||||
}
|
||||
StorageInstruction::ClaimStorageReward { entry_height } => {
|
||||
if num_keyed_accounts != 1 {
|
||||
// keyed_accounts[0] should be the main storage key
|
||||
// to access its data
|
||||
Err(InstructionError::InvalidArgument)?;
|
||||
}
|
||||
storage_account.claim_storage_reward(entry_height, tick_height)
|
||||
}
|
||||
StorageInstruction::ProofValidation {
|
||||
entry_height,
|
||||
proofs,
|
||||
} => {
|
||||
if num_keyed_accounts == 1 {
|
||||
// have to have at least 1 replicator to do any verification
|
||||
Err(InstructionError::InvalidArgument)?;
|
||||
}
|
||||
storage_account.proof_validation(entry_height, proofs, &mut rest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use crate::storage_contract::{CheckedProof, Proof, ProofStatus, StorageContract};
|
||||
use crate::storage_instruction;
|
||||
use crate::ENTRIES_PER_SEGMENT;
|
||||
use bincode::deserialize;
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::account::{create_keyed_accounts, Account};
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::hash::{hash, Hash};
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil, Signature};
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
fn test_instruction(
|
||||
ix: &Instruction,
|
||||
program_accounts: &mut [Account],
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut keyed_accounts: Vec<_> = ix
|
||||
.accounts
|
||||
.iter()
|
||||
.zip(program_accounts.iter_mut())
|
||||
.map(|(account_meta, account)| {
|
||||
KeyedAccount::new(&account_meta.pubkey, account_meta.is_signer, account)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ret = process_instruction(&id(), &mut keyed_accounts, &ix.data, 42);
|
||||
info!("ret: {:?}", ret);
|
||||
ret
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_tx() {
|
||||
let pubkey = Pubkey::new_rand();
|
||||
let mut accounts = [(pubkey, Account::default())];
|
||||
let mut keyed_accounts = create_keyed_accounts(&mut accounts);
|
||||
assert!(process_instruction(&id(), &mut keyed_accounts, &[], 42).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_overflow() {
|
||||
let pubkey = Pubkey::new_rand();
|
||||
let mut keyed_accounts = Vec::new();
|
||||
let mut user_account = Account::default();
|
||||
keyed_accounts.push(KeyedAccount::new(&pubkey, true, &mut user_account));
|
||||
|
||||
let ix = storage_instruction::advertise_recent_blockhash(
|
||||
&pubkey,
|
||||
Hash::default(),
|
||||
ENTRIES_PER_SEGMENT,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
process_instruction(&id(), &mut keyed_accounts, &ix.data, 42),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_accounts_len() {
|
||||
let pubkey = Pubkey::new_rand();
|
||||
let mut accounts = [Account::default()];
|
||||
|
||||
let ix =
|
||||
storage_instruction::mining_proof(&pubkey, Hash::default(), 0, Signature::default());
|
||||
assert!(test_instruction(&ix, &mut accounts).is_err());
|
||||
|
||||
let mut accounts = [Account::default(), Account::default(), Account::default()];
|
||||
|
||||
assert!(test_instruction(&ix, &mut accounts).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_mining_invalid_entry_height() {
|
||||
solana_logger::setup();
|
||||
let pubkey = Pubkey::new_rand();
|
||||
let mut accounts = [Account::default(), Account::default()];
|
||||
accounts[1].data.resize(16 * 1024, 0);
|
||||
|
||||
let ix =
|
||||
storage_instruction::mining_proof(&pubkey, Hash::default(), 0, Signature::default());
|
||||
|
||||
// Haven't seen a transaction to roll over the epoch, so this should fail
|
||||
assert!(test_instruction(&ix, &mut accounts).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_mining_ok() {
|
||||
solana_logger::setup();
|
||||
let pubkey = Pubkey::new_rand();
|
||||
let mut accounts = [Account::default(), Account::default()];
|
||||
accounts[0].data.resize(16 * 1024, 0);
|
||||
|
||||
let ix =
|
||||
storage_instruction::mining_proof(&pubkey, Hash::default(), 0, Signature::default());
|
||||
|
||||
test_instruction(&ix, &mut accounts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_validate_mining() {
|
||||
solana_logger::setup();
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
let replicator_keypair = Keypair::new();
|
||||
let replicator = replicator_keypair.pubkey();
|
||||
let validator_keypair = Keypair::new();
|
||||
let validator = validator_keypair.pubkey();
|
||||
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
let entry_height = 0;
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let ix = system_instruction::create_account(&mint_pubkey, &validator, 10, 4 * 1042, &id());
|
||||
bank_client.send_instruction(&mint_keypair, ix).unwrap();
|
||||
|
||||
let ix = system_instruction::create_account(&mint_pubkey, &replicator, 10, 4 * 1042, &id());
|
||||
bank_client.send_instruction(&mint_keypair, ix).unwrap();
|
||||
|
||||
let ix = storage_instruction::advertise_recent_blockhash(
|
||||
&validator,
|
||||
Hash::default(),
|
||||
ENTRIES_PER_SEGMENT,
|
||||
);
|
||||
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let ix = storage_instruction::mining_proof(
|
||||
&replicator,
|
||||
Hash::default(),
|
||||
entry_height,
|
||||
Signature::default(),
|
||||
);
|
||||
bank_client
|
||||
.send_instruction(&replicator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let ix = storage_instruction::advertise_recent_blockhash(
|
||||
&validator,
|
||||
Hash::default(),
|
||||
ENTRIES_PER_SEGMENT * 2,
|
||||
);
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let ix = storage_instruction::proof_validation(
|
||||
&validator,
|
||||
entry_height,
|
||||
vec![CheckedProof {
|
||||
proof: Proof {
|
||||
id: replicator,
|
||||
signature: Signature::default(),
|
||||
sha_state: Hash::default(),
|
||||
},
|
||||
status: ProofStatus::Valid,
|
||||
}],
|
||||
);
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let ix = storage_instruction::advertise_recent_blockhash(
|
||||
&validator,
|
||||
Hash::default(),
|
||||
ENTRIES_PER_SEGMENT * 3,
|
||||
);
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let ix = storage_instruction::reward_claim(&validator, entry_height);
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
// TODO enable when rewards are working
|
||||
// assert_eq!(bank_client.get_balance(&validator).unwrap(), TOTAL_VALIDATOR_REWARDS);
|
||||
|
||||
// TODO extend BankClient with a method to force a block boundary
|
||||
// tick the bank into the next storage epoch so that rewards can be claimed
|
||||
//for _ in 0..=ENTRIES_PER_SEGMENT {
|
||||
// bank.register_tick(&bank.last_blockhash());
|
||||
//}
|
||||
|
||||
let ix = storage_instruction::reward_claim(&replicator, entry_height);
|
||||
bank_client
|
||||
.send_instruction(&replicator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
// TODO enable when rewards are working
|
||||
// assert_eq!(bank_client.get_balance(&replicator).unwrap(), TOTAL_REPLICATOR_REWARDS);
|
||||
}
|
||||
|
||||
fn get_storage_entry_height<C: SyncClient>(client: &C, account: &Pubkey) -> u64 {
|
||||
match client.get_account_data(&account).unwrap() {
|
||||
Some(storage_system_account_data) => {
|
||||
let contract = deserialize(&storage_system_account_data);
|
||||
if let Ok(contract) = contract {
|
||||
match contract {
|
||||
StorageContract::ValidatorStorage { entry_height, .. } => {
|
||||
return entry_height;
|
||||
}
|
||||
_ => info!("error in reading entry_height"),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
info!("error in reading entry_height");
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn get_storage_blockhash<C: SyncClient>(client: &C, account: &Pubkey) -> Hash {
|
||||
if let Some(storage_system_account_data) = client.get_account_data(&account).unwrap() {
|
||||
let contract = deserialize(&storage_system_account_data);
|
||||
if let Ok(contract) = contract {
|
||||
match contract {
|
||||
StorageContract::ValidatorStorage { hash, .. } => {
|
||||
return hash;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
Hash::default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bank_storage() {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(1000);
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
let replicator_keypair = Keypair::new();
|
||||
let replicator_pubkey = replicator_keypair.pubkey();
|
||||
let validator_keypair = Keypair::new();
|
||||
let validator_pubkey = validator_keypair.pubkey();
|
||||
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let x = 42;
|
||||
let x2 = x * 2;
|
||||
let storage_blockhash = hash(&[x2]);
|
||||
|
||||
bank_client
|
||||
.transfer(10, &mint_keypair, &replicator_pubkey)
|
||||
.unwrap();
|
||||
|
||||
let ix = system_instruction::create_account(
|
||||
&mint_pubkey,
|
||||
&replicator_pubkey,
|
||||
1,
|
||||
4 * 1024,
|
||||
&id(),
|
||||
);
|
||||
|
||||
bank_client.send_instruction(&mint_keypair, ix).unwrap();
|
||||
|
||||
let ix =
|
||||
system_instruction::create_account(&mint_pubkey, &validator_pubkey, 1, 4 * 1024, &id());
|
||||
|
||||
bank_client.send_instruction(&mint_keypair, ix).unwrap();
|
||||
|
||||
let ix = storage_instruction::advertise_recent_blockhash(
|
||||
&validator_pubkey,
|
||||
storage_blockhash,
|
||||
ENTRIES_PER_SEGMENT,
|
||||
);
|
||||
|
||||
bank_client
|
||||
.send_instruction(&validator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
let entry_height = 0;
|
||||
let ix = storage_instruction::mining_proof(
|
||||
&replicator_pubkey,
|
||||
Hash::default(),
|
||||
entry_height,
|
||||
Signature::default(),
|
||||
);
|
||||
let _result = bank_client
|
||||
.send_instruction(&replicator_keypair, ix)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_storage_entry_height(&bank_client, &validator_pubkey),
|
||||
ENTRIES_PER_SEGMENT
|
||||
);
|
||||
assert_eq!(
|
||||
get_storage_blockhash(&bank_client, &validator_pubkey),
|
||||
storage_blockhash
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-storage-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana storage program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-storage-api = { path = "../storage_api", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_storage_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_storage_api::storage_processor::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "solana-token-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Token API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_token_api"
|
||||
crate-type = ["lib"]
|
@ -1,13 +0,0 @@
|
||||
pub mod token_processor;
|
||||
mod token_state;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const TOKEN_PROGRAM_ID: [u8; 32] = [
|
||||
131, 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,
|
||||
];
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&TOKEN_PROGRAM_ID)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
use crate::token_state::TokenState;
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
info: &mut [KeyedAccount],
|
||||
input: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
TokenState::process(program_id, info, input).map_err(|e| {
|
||||
error!("error: {:?}", e);
|
||||
InstructionError::CustomError(e as u32)
|
||||
})
|
||||
}
|
@ -1,491 +0,0 @@
|
||||
use log::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
pub enum TokenError {
|
||||
InvalidArgument,
|
||||
InsufficentFunds,
|
||||
NotOwner,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "error")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for TokenError {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TokenError>;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TokenInfo {
|
||||
/// Total supply of tokens
|
||||
supply: u64,
|
||||
|
||||
/// Number of base 10 digits to the right of the decimal place in the total supply
|
||||
decimals: u8,
|
||||
|
||||
/// Descriptive name of this token
|
||||
name: String,
|
||||
|
||||
/// Symbol for this token
|
||||
symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TokenAccountDelegateInfo {
|
||||
/// The source account for the tokens
|
||||
source: Pubkey,
|
||||
|
||||
/// The original amount that this delegate account was authorized to spend up to
|
||||
original_amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TokenAccountInfo {
|
||||
/// The kind of token this account holds
|
||||
token: Pubkey,
|
||||
|
||||
/// Owner of this account
|
||||
owner: Pubkey,
|
||||
|
||||
/// Amount of tokens this account holds
|
||||
amount: u64,
|
||||
|
||||
/// If `delegate` None, `amount` belongs to this account.
|
||||
/// If `delegate` is Option<_>, `amount` represents the remaining allowance
|
||||
/// of tokens that may be transferred from the `source` account.
|
||||
delegate: Option<TokenAccountDelegateInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
enum TokenInstruction {
|
||||
NewToken(TokenInfo),
|
||||
NewTokenAccount,
|
||||
Transfer(u64),
|
||||
Approve(u64),
|
||||
SetOwner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TokenState {
|
||||
Unallocated,
|
||||
Token(TokenInfo),
|
||||
Account(TokenAccountInfo),
|
||||
Invalid,
|
||||
}
|
||||
impl Default for TokenState {
|
||||
fn default() -> TokenState {
|
||||
TokenState::Unallocated
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenState {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn map_to_invalid_args(err: std::boxed::Box<bincode::ErrorKind>) -> TokenError {
|
||||
warn!("invalid argument: {:?}", err);
|
||||
TokenError::InvalidArgument
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> Result<TokenState> {
|
||||
if input.is_empty() {
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
match input[0] {
|
||||
0 => Ok(TokenState::Unallocated),
|
||||
1 => Ok(TokenState::Token(
|
||||
bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?,
|
||||
)),
|
||||
2 => Ok(TokenState::Account(
|
||||
bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?,
|
||||
)),
|
||||
_ => Err(TokenError::InvalidArgument),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(self: &TokenState, output: &mut [u8]) -> Result<()> {
|
||||
if output.is_empty() {
|
||||
warn!("serialize fail: ouput.len is 0");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
match self {
|
||||
TokenState::Unallocated | TokenState::Invalid => Err(TokenError::InvalidArgument),
|
||||
TokenState::Token(token_info) => {
|
||||
output[0] = 1;
|
||||
let writer = std::io::BufWriter::new(&mut output[1..]);
|
||||
bincode::serialize_into(writer, &token_info).map_err(Self::map_to_invalid_args)
|
||||
}
|
||||
TokenState::Account(account_info) => {
|
||||
output[0] = 2;
|
||||
let writer = std::io::BufWriter::new(&mut output[1..]);
|
||||
bincode::serialize_into(writer, &account_info).map_err(Self::map_to_invalid_args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn amount(&self) -> Result<u64> {
|
||||
if let TokenState::Account(account_info) = self {
|
||||
Ok(account_info.amount)
|
||||
} else {
|
||||
Err(TokenError::InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn only_owner(&self, key: &Pubkey) -> Result<()> {
|
||||
if *key != Pubkey::default() {
|
||||
if let TokenState::Account(account_info) = self {
|
||||
if account_info.owner == *key {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
warn!("TokenState: non-owner rejected");
|
||||
Err(TokenError::NotOwner)
|
||||
}
|
||||
|
||||
pub fn process_newtoken(
|
||||
info: &mut [KeyedAccount],
|
||||
token_info: TokenInfo,
|
||||
input_accounts: &[TokenState],
|
||||
output_accounts: &mut Vec<(usize, TokenState)>,
|
||||
) -> Result<()> {
|
||||
if input_accounts.len() != 2 {
|
||||
error!("Expected 2 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let TokenState::Account(dest_account) = &input_accounts[1] {
|
||||
if info[0].signer_key().unwrap() != &dest_account.token {
|
||||
error!("account 1 token mismatch");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if dest_account.delegate.is_some() {
|
||||
error!("account 1 is a delegate and cannot accept tokens");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut output_dest_account = dest_account.clone();
|
||||
output_dest_account.amount = token_info.supply;
|
||||
output_accounts.push((1, TokenState::Account(output_dest_account)));
|
||||
} else {
|
||||
error!("account 1 invalid");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if input_accounts[0] != TokenState::Unallocated {
|
||||
error!("account 0 not available");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
output_accounts.push((0, TokenState::Token(token_info)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_newaccount(
|
||||
info: &mut [KeyedAccount],
|
||||
input_accounts: &[TokenState],
|
||||
output_accounts: &mut Vec<(usize, TokenState)>,
|
||||
) -> Result<()> {
|
||||
// key 0 - Destination new token account
|
||||
// key 1 - Owner of the account
|
||||
// key 2 - Token this account is associated with
|
||||
// key 3 - Source account that this account is a delegate for (optional)
|
||||
if input_accounts.len() < 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
if input_accounts[0] != TokenState::Unallocated {
|
||||
error!("account 0 is already allocated");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
let mut token_account_info = TokenAccountInfo {
|
||||
token: *info[2].unsigned_key(),
|
||||
owner: *info[1].unsigned_key(),
|
||||
amount: 0,
|
||||
delegate: None,
|
||||
};
|
||||
if input_accounts.len() >= 4 {
|
||||
token_account_info.delegate = Some(TokenAccountDelegateInfo {
|
||||
source: *info[3].unsigned_key(),
|
||||
original_amount: 0,
|
||||
});
|
||||
}
|
||||
output_accounts.push((0, TokenState::Account(token_account_info)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_transfer(
|
||||
info: &mut [KeyedAccount],
|
||||
amount: u64,
|
||||
input_accounts: &[TokenState],
|
||||
output_accounts: &mut Vec<(usize, TokenState)>,
|
||||
) -> Result<()> {
|
||||
if input_accounts.len() < 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let (TokenState::Account(source_account), TokenState::Account(dest_account)) =
|
||||
(&input_accounts[1], &input_accounts[2])
|
||||
{
|
||||
if source_account.token != dest_account.token {
|
||||
error!("account 1/2 token mismatch");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if dest_account.delegate.is_some() {
|
||||
error!("account 2 is a delegate and cannot accept tokens");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if info[0].signer_key().unwrap() != &source_account.owner {
|
||||
error!("owner of account 1 not present");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.amount < amount {
|
||||
Err(TokenError::InsufficentFunds)?;
|
||||
}
|
||||
|
||||
let mut output_source_account = source_account.clone();
|
||||
output_source_account.amount -= amount;
|
||||
output_accounts.push((1, TokenState::Account(output_source_account)));
|
||||
|
||||
if let Some(ref delegate_info) = source_account.delegate {
|
||||
if input_accounts.len() != 4 {
|
||||
error!("Expected 4 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let delegate_account = source_account;
|
||||
if let TokenState::Account(source_account) = &input_accounts[3] {
|
||||
if source_account.token != delegate_account.token {
|
||||
error!("account 1/3 token mismatch");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
if info[3].unsigned_key() != &delegate_info.source {
|
||||
error!("Account 1 is not a delegate of account 3");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.amount < amount {
|
||||
Err(TokenError::InsufficentFunds)?;
|
||||
}
|
||||
|
||||
let mut output_source_account = source_account.clone();
|
||||
output_source_account.amount -= amount;
|
||||
output_accounts.push((3, TokenState::Account(output_source_account)));
|
||||
} else {
|
||||
error!("account 3 is an invalid account");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut output_dest_account = dest_account.clone();
|
||||
output_dest_account.amount += amount;
|
||||
output_accounts.push((2, TokenState::Account(output_dest_account)));
|
||||
} else {
|
||||
error!("account 1 and/or 2 are invalid accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_approve(
|
||||
info: &mut [KeyedAccount],
|
||||
amount: u64,
|
||||
input_accounts: &[TokenState],
|
||||
output_accounts: &mut Vec<(usize, TokenState)>,
|
||||
) -> Result<()> {
|
||||
if input_accounts.len() != 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let (TokenState::Account(source_account), TokenState::Account(delegate_account)) =
|
||||
(&input_accounts[1], &input_accounts[2])
|
||||
{
|
||||
if source_account.token != delegate_account.token {
|
||||
error!("account 1/2 token mismatch");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if info[0].signer_key().unwrap() != &source_account.owner {
|
||||
error!("owner of account 1 not present");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.delegate.is_some() {
|
||||
error!("account 1 is a delegate");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
match &delegate_account.delegate {
|
||||
None => {
|
||||
error!("account 2 is not a delegate");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
Some(delegate_info) => {
|
||||
if info[1].unsigned_key() != &delegate_info.source {
|
||||
error!("account 2 is not a delegate of account 1");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut output_delegate_account = delegate_account.clone();
|
||||
output_delegate_account.amount = amount;
|
||||
output_delegate_account.delegate = Some(TokenAccountDelegateInfo {
|
||||
source: delegate_info.source,
|
||||
original_amount: amount,
|
||||
});
|
||||
output_accounts.push((2, TokenState::Account(output_delegate_account)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("account 1 and/or 2 are invalid accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_setowner(
|
||||
info: &mut [KeyedAccount],
|
||||
input_accounts: &[TokenState],
|
||||
output_accounts: &mut Vec<(usize, TokenState)>,
|
||||
) -> Result<()> {
|
||||
if input_accounts.len() < 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let TokenState::Account(source_account) = &input_accounts[1] {
|
||||
if info[0].signer_key().unwrap() != &source_account.owner {
|
||||
info!("owner of account 1 not present");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut output_source_account = source_account.clone();
|
||||
output_source_account.owner = *info[2].unsigned_key();
|
||||
output_accounts.push((1, TokenState::Account(output_source_account)));
|
||||
} else {
|
||||
info!("account 1 is invalid");
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(program_id: &Pubkey, info: &mut [KeyedAccount], input: &[u8]) -> Result<()> {
|
||||
let command =
|
||||
bincode::deserialize::<TokenInstruction>(input).map_err(Self::map_to_invalid_args)?;
|
||||
info!("process_transaction: command={:?}", command);
|
||||
|
||||
if info[0].signer_key().is_none() {
|
||||
Err(TokenError::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let input_accounts: Vec<TokenState> = info
|
||||
.iter()
|
||||
.map(|keyed_account| {
|
||||
let account = &keyed_account.account;
|
||||
if account.owner == *program_id {
|
||||
match Self::deserialize(&account.data) {
|
||||
Ok(token_state) => token_state,
|
||||
Err(err) => {
|
||||
error!("deserialize failed: {:?}", err);
|
||||
TokenState::Invalid
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenState::Invalid
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for account in &input_accounts {
|
||||
info!("input_account: data={:?}", account);
|
||||
}
|
||||
|
||||
let mut output_accounts: Vec<(_, _)> = vec![];
|
||||
|
||||
match command {
|
||||
TokenInstruction::NewToken(token_info) => {
|
||||
Self::process_newtoken(info, token_info, &input_accounts, &mut output_accounts)?
|
||||
}
|
||||
TokenInstruction::NewTokenAccount => {
|
||||
Self::process_newaccount(info, &input_accounts, &mut output_accounts)?
|
||||
}
|
||||
|
||||
TokenInstruction::Transfer(amount) => {
|
||||
Self::process_transfer(info, amount, &input_accounts, &mut output_accounts)?
|
||||
}
|
||||
|
||||
TokenInstruction::Approve(amount) => {
|
||||
Self::process_approve(info, amount, &input_accounts, &mut output_accounts)?
|
||||
}
|
||||
|
||||
TokenInstruction::SetOwner => {
|
||||
Self::process_setowner(info, &input_accounts, &mut output_accounts)?
|
||||
}
|
||||
}
|
||||
|
||||
for (index, account) in &output_accounts {
|
||||
info!("output_account: index={} data={:?}", index, account);
|
||||
Self::serialize(account, &mut info[*index].account.data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
pub fn serde() {
|
||||
assert_eq!(TokenState::deserialize(&[0]), Ok(TokenState::default()));
|
||||
|
||||
let mut data = vec![0; 256];
|
||||
|
||||
let account = TokenState::Account(TokenAccountInfo {
|
||||
token: Pubkey::new(&[1; 32]),
|
||||
owner: Pubkey::new(&[2; 32]),
|
||||
amount: 123,
|
||||
delegate: None,
|
||||
});
|
||||
account.serialize(&mut data).unwrap();
|
||||
assert_eq!(TokenState::deserialize(&data), Ok(account));
|
||||
|
||||
let account = TokenState::Token(TokenInfo {
|
||||
supply: 12345,
|
||||
decimals: 2,
|
||||
name: "A test token".to_string(),
|
||||
symbol: "TEST".to_string(),
|
||||
});
|
||||
account.serialize(&mut data).unwrap();
|
||||
assert_eq!(TokenState::deserialize(&data), Ok(account));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn serde_expect_fail() {
|
||||
let mut data = vec![0; 256];
|
||||
|
||||
// Certain TokenState's may not be serialized
|
||||
let account = TokenState::default();
|
||||
assert_eq!(account, TokenState::Unallocated);
|
||||
assert!(account.serialize(&mut data).is_err());
|
||||
assert!(account.serialize(&mut data).is_err());
|
||||
let account = TokenState::Invalid;
|
||||
assert!(account.serialize(&mut data).is_err());
|
||||
|
||||
// Bad deserialize data
|
||||
assert!(TokenState::deserialize(&[]).is_err());
|
||||
assert!(TokenState::deserialize(&[1]).is_err());
|
||||
assert!(TokenState::deserialize(&[1, 2]).is_err());
|
||||
assert!(TokenState::deserialize(&[2, 2]).is_err());
|
||||
assert!(TokenState::deserialize(&[3]).is_err());
|
||||
}
|
||||
|
||||
// Note: business logic tests are located in the @solana/web3.js test suite
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-token-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana token program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-token-api = { path = "../token_api", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_token_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_token_api::token_processor::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "solana-vote-api"
|
||||
version = "0.14.0"
|
||||
description = "Solana Vote program API"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1.3"
|
||||
log = "0.4.2"
|
||||
serde = "1.0.90"
|
||||
serde_derive = "1.0.90"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-metrics = { path = "../../metrics", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-runtime = { path = "../../runtime", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_vote_api"
|
||||
crate-type = ["lib"]
|
@ -1,17 +0,0 @@
|
||||
pub mod vote_instruction;
|
||||
pub mod vote_state;
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const VOTE_PROGRAM_ID: [u8; 32] = [
|
||||
132, 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,
|
||||
];
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == VOTE_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&VOTE_PROGRAM_ID)
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
//! Vote program
|
||||
//! Receive and processes votes from validators
|
||||
|
||||
use crate::id;
|
||||
use crate::vote_state::{self, Vote, VoteState};
|
||||
use bincode::deserialize;
|
||||
use log::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum VoteInstruction {
|
||||
/// Initialize the VoteState for this `vote account`
|
||||
/// takes a node_id and commission
|
||||
InitializeAccount(Pubkey, u32),
|
||||
|
||||
/// Authorize a voter to send signed votes.
|
||||
AuthorizeVoter(Pubkey),
|
||||
|
||||
/// A Vote instruction with recent votes
|
||||
Vote(Vec<Vote>),
|
||||
}
|
||||
|
||||
fn initialize_account(vote_id: &Pubkey, node_id: &Pubkey, commission: u32) -> Instruction {
|
||||
let account_metas = vec![AccountMeta::new(*vote_id, false)];
|
||||
Instruction::new(
|
||||
id(),
|
||||
&VoteInstruction::InitializeAccount(*node_id, commission),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
from_id: &Pubkey,
|
||||
vote_id: &Pubkey,
|
||||
node_id: &Pubkey,
|
||||
commission: u32,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let space = VoteState::size_of() as u64;
|
||||
let create_ix = system_instruction::create_account(&from_id, vote_id, lamports, space, &id());
|
||||
let init_ix = initialize_account(vote_id, node_id, commission);
|
||||
vec![create_ix, init_ix]
|
||||
}
|
||||
|
||||
pub fn authorize_voter(vote_id: &Pubkey, authorized_voter_id: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![AccountMeta::new(*vote_id, true)];
|
||||
Instruction::new(
|
||||
id(),
|
||||
&VoteInstruction::AuthorizeVoter(*authorized_voter_id),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn vote(vote_id: &Pubkey, recent_votes: Vec<Vote>) -> Instruction {
|
||||
let account_metas = vec![AccountMeta::new(*vote_id, true)];
|
||||
Instruction::new(id(), &VoteInstruction::Vote(recent_votes), account_metas)
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
_tick_height: u64,
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
trace!("process_instruction: {:?}", data);
|
||||
trace!("keyed_accounts: {:?}", keyed_accounts);
|
||||
|
||||
match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? {
|
||||
VoteInstruction::InitializeAccount(node_id, commission) => {
|
||||
let mut vote_account = &mut keyed_accounts[0];
|
||||
vote_state::initialize_account(&mut vote_account, &node_id, commission)
|
||||
}
|
||||
VoteInstruction::AuthorizeVoter(voter_id) => {
|
||||
let (vote_account, other_signers) = keyed_accounts.split_at_mut(1);
|
||||
let vote_account = &mut vote_account[0];
|
||||
|
||||
vote_state::authorize_voter(vote_account, other_signers, &voter_id)
|
||||
}
|
||||
VoteInstruction::Vote(vote) => {
|
||||
solana_metrics::submit(
|
||||
solana_metrics::influxdb::Point::new("vote-native")
|
||||
.add_field("count", solana_metrics::influxdb::Value::Integer(1))
|
||||
.to_owned(),
|
||||
);
|
||||
let (vote_account, other_signers) = keyed_accounts.split_at_mut(1);
|
||||
let vote_account = &mut vote_account[0];
|
||||
|
||||
vote_state::process_vote(vote_account, other_signers, &vote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::id;
|
||||
use crate::vote_instruction;
|
||||
use crate::vote_state::{Vote, VoteState};
|
||||
use solana_runtime::bank::Bank;
|
||||
use solana_runtime::bank_client::BankClient;
|
||||
use solana_sdk::client::SyncClient;
|
||||
use solana_sdk::genesis_block::GenesisBlock;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::transaction::{Result, TransactionError};
|
||||
|
||||
fn create_bank(lamports: u64) -> (Bank, Keypair) {
|
||||
let (genesis_block, mint_keypair) = GenesisBlock::new(lamports);
|
||||
let mut bank = Bank::new(&genesis_block);
|
||||
bank.add_instruction_processor(id(), process_instruction);
|
||||
(bank, mint_keypair)
|
||||
}
|
||||
|
||||
fn create_vote_account(
|
||||
bank_client: &BankClient,
|
||||
from_keypair: &Keypair,
|
||||
vote_id: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Result<()> {
|
||||
let ixs = vote_instruction::create_account(
|
||||
&from_keypair.pubkey(),
|
||||
vote_id,
|
||||
&Pubkey::new_rand(),
|
||||
0,
|
||||
lamports,
|
||||
);
|
||||
let message = Message::new(ixs);
|
||||
bank_client
|
||||
.send_message(&[from_keypair], message)
|
||||
.map_err(|err| err.unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn submit_vote(
|
||||
bank_client: &BankClient,
|
||||
vote_keypair: &Keypair,
|
||||
tick_height: u64,
|
||||
) -> Result<()> {
|
||||
let vote_ix = vote_instruction::vote(&vote_keypair.pubkey(), vec![Vote::new(tick_height)]);
|
||||
bank_client
|
||||
.send_instruction(vote_keypair, vote_ix)
|
||||
.map_err(|err| err.unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_bank_basic() {
|
||||
let (bank, from_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let vote_keypair = Keypair::new();
|
||||
let vote_id = vote_keypair.pubkey();
|
||||
|
||||
create_vote_account(&bank_client, &from_keypair, &vote_id, 100).unwrap();
|
||||
submit_vote(&bank_client, &vote_keypair, 0).unwrap();
|
||||
|
||||
let vote_account_data = bank_client.get_account_data(&vote_id).unwrap().unwrap();
|
||||
let vote_state = VoteState::deserialize(&vote_account_data).unwrap();
|
||||
assert_eq!(vote_state.votes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_via_bank_authorize_voter() {
|
||||
let (bank, mallory_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let vote_keypair = Keypair::new();
|
||||
let vote_id = vote_keypair.pubkey();
|
||||
|
||||
create_vote_account(&bank_client, &mallory_keypair, &vote_id, 100).unwrap();
|
||||
|
||||
let mallory_id = mallory_keypair.pubkey();
|
||||
let vote_ix = vote_instruction::authorize_voter(&vote_id, &mallory_id);
|
||||
|
||||
let message = Message::new(vec![vote_ix]);
|
||||
assert!(bank_client.send_message(&[&vote_keypair], message).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_via_bank_with_no_signature() {
|
||||
let (bank, mallory_keypair) = create_bank(10_000);
|
||||
let bank_client = BankClient::new(bank);
|
||||
|
||||
let vote_keypair = Keypair::new();
|
||||
let vote_id = vote_keypair.pubkey();
|
||||
|
||||
create_vote_account(&bank_client, &mallory_keypair, &vote_id, 100).unwrap();
|
||||
|
||||
let mallory_id = mallory_keypair.pubkey();
|
||||
let mut vote_ix = vote_instruction::vote(&vote_id, vec![Vote::new(0)]);
|
||||
vote_ix.accounts[0].is_signer = false; // <--- attack!! No signer required.
|
||||
|
||||
// Sneak in an instruction so that the transaction is signed but
|
||||
// the 0th account in the second instruction is not! The program
|
||||
// needs to check that it's signed.
|
||||
let move_ix = system_instruction::transfer(&mallory_id, &vote_id, 1);
|
||||
let message = Message::new(vec![move_ix, vote_ix]);
|
||||
let result = bank_client.send_message(&[&mallory_keypair], message);
|
||||
|
||||
// And ensure there's no vote.
|
||||
let vote_account_data = bank_client.get_account_data(&vote_id).unwrap().unwrap();
|
||||
let vote_state = VoteState::deserialize(&vote_account_data).unwrap();
|
||||
assert_eq!(vote_state.votes.len(), 0);
|
||||
|
||||
assert_eq!(
|
||||
result.unwrap_err().unwrap(),
|
||||
TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,610 +0,0 @@
|
||||
//! Vote state, vote program
|
||||
//! Receive and processes votes from validators
|
||||
use crate::id;
|
||||
use bincode::{deserialize, serialize_into, serialized_size, ErrorKind};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::account::{Account, KeyedAccount};
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::instruction_processor_utils::State;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// Maximum number of votes to keep around
|
||||
pub const MAX_LOCKOUT_HISTORY: usize = 31;
|
||||
pub const INITIAL_LOCKOUT: usize = 2;
|
||||
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Vote {
|
||||
// TODO: add signature of the state here as well
|
||||
/// A vote for height slot
|
||||
pub slot: u64,
|
||||
}
|
||||
|
||||
impl Vote {
|
||||
pub fn new(slot: u64) -> Self {
|
||||
Self { slot }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Lockout {
|
||||
pub slot: u64,
|
||||
pub confirmation_count: u32,
|
||||
}
|
||||
|
||||
impl Lockout {
|
||||
pub fn new(vote: &Vote) -> Self {
|
||||
Self {
|
||||
slot: vote.slot,
|
||||
confirmation_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// The number of slots for which this vote is locked
|
||||
pub fn lockout(&self) -> u64 {
|
||||
(INITIAL_LOCKOUT as u64).pow(self.confirmation_count)
|
||||
}
|
||||
|
||||
// The slot height at which this vote expires (cannot vote for any slot
|
||||
// less than this)
|
||||
pub fn expiration_slot(&self) -> u64 {
|
||||
self.slot + self.lockout()
|
||||
}
|
||||
pub fn is_expired(&self, slot: u64) -> bool {
|
||||
self.expiration_slot() < slot
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct VoteState {
|
||||
pub votes: VecDeque<Lockout>,
|
||||
pub node_id: Pubkey,
|
||||
pub authorized_voter_id: Pubkey,
|
||||
/// fraction of std::u32::MAX that represents what part of a rewards
|
||||
/// payout should be given to this VoteAccount
|
||||
pub commission: u32,
|
||||
pub root_slot: Option<u64>,
|
||||
credits: u64,
|
||||
}
|
||||
|
||||
impl VoteState {
|
||||
pub fn new(vote_id: &Pubkey, node_id: &Pubkey, commission: u32) -> Self {
|
||||
let votes = VecDeque::new();
|
||||
let credits = 0;
|
||||
let root_slot = None;
|
||||
Self {
|
||||
votes,
|
||||
node_id: *node_id,
|
||||
authorized_voter_id: *vote_id,
|
||||
credits,
|
||||
commission,
|
||||
root_slot,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size_of() -> usize {
|
||||
// Upper limit on the size of the Vote State. Equal to
|
||||
// size_of(VoteState) when votes.len() is MAX_LOCKOUT_HISTORY
|
||||
let mut vote_state = Self::default();
|
||||
vote_state.votes = VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]);
|
||||
vote_state.root_slot = Some(std::u64::MAX);
|
||||
serialized_size(&vote_state).unwrap() as usize
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
|
||||
deserialize(input).map_err(|_| InstructionError::InvalidAccountData)
|
||||
}
|
||||
|
||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), InstructionError> {
|
||||
serialize_into(output, self).map_err(|err| match *err {
|
||||
ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall,
|
||||
_ => InstructionError::GenericError,
|
||||
})
|
||||
}
|
||||
|
||||
/// returns commission split as (voter_portion, staker_portion) tuple
|
||||
///
|
||||
/// if commission calculation is 100% one way or other,
|
||||
/// indicate with None for the 0% side
|
||||
pub fn commission_split(&self, on: f64) -> (f64, f64, bool) {
|
||||
match self.commission {
|
||||
0 => (0.0, on, false),
|
||||
std::u32::MAX => (on, 0.0, false),
|
||||
split => {
|
||||
let mine = on * f64::from(split) / f64::from(std::u32::MAX);
|
||||
(mine, on - mine, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_votes(&mut self, votes: &[Vote]) {
|
||||
votes.iter().for_each(|v| self.process_vote(v));;
|
||||
}
|
||||
|
||||
pub fn process_vote(&mut self, vote: &Vote) {
|
||||
// Ignore votes for slots earlier than we already have votes for
|
||||
if self
|
||||
.votes
|
||||
.back()
|
||||
.map_or(false, |old_vote| old_vote.slot >= vote.slot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let vote = Lockout::new(&vote);
|
||||
|
||||
// TODO: Integrity checks
|
||||
// Verify the vote's bank hash matches what is expected
|
||||
|
||||
self.pop_expired_votes(vote.slot);
|
||||
// Once the stack is full, pop the oldest vote and distribute rewards
|
||||
if self.votes.len() == MAX_LOCKOUT_HISTORY {
|
||||
let vote = self.votes.pop_front().unwrap();
|
||||
self.root_slot = Some(vote.slot);
|
||||
self.credits += 1;
|
||||
}
|
||||
self.votes.push_back(vote);
|
||||
self.double_lockouts();
|
||||
}
|
||||
|
||||
pub fn nth_recent_vote(&self, position: usize) -> Option<&Lockout> {
|
||||
if position < self.votes.len() {
|
||||
let pos = self.votes.len() - 1 - position;
|
||||
self.votes.get(pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of "credits" owed to this account from the mining pool. Submit this
|
||||
/// VoteState to the Rewards program to trade credits for lamports.
|
||||
pub fn credits(&self) -> u64 {
|
||||
self.credits
|
||||
}
|
||||
|
||||
fn pop_expired_votes(&mut self, slot: u64) {
|
||||
loop {
|
||||
if self.votes.back().map_or(false, |v| v.is_expired(slot)) {
|
||||
self.votes.pop_back();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn double_lockouts(&mut self) {
|
||||
let stack_depth = self.votes.len();
|
||||
for (i, v) in self.votes.iter_mut().enumerate() {
|
||||
// Don't increase the lockout for this vote until we get more confirmations
|
||||
// than the max number of confirmations this vote has seen
|
||||
if stack_depth > i + v.confirmation_count as usize {
|
||||
v.confirmation_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authorize the given pubkey to sign votes. This may be called multiple times,
|
||||
/// but will implicitly withdraw authorization from the previously authorized
|
||||
/// voter. The default voter is the owner of the vote account's pubkey.
|
||||
pub fn authorize_voter(
|
||||
vote_account: &mut KeyedAccount,
|
||||
other_signers: &[KeyedAccount],
|
||||
authorized_voter_id: &Pubkey,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut vote_state: VoteState = vote_account.state()?;
|
||||
|
||||
// current authorized signer must say "yay"
|
||||
let authorized = Some(&vote_state.authorized_voter_id);
|
||||
if vote_account.signer_key() != authorized
|
||||
&& other_signers
|
||||
.iter()
|
||||
.all(|account| account.signer_key() != authorized)
|
||||
{
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
vote_state.authorized_voter_id = *authorized_voter_id;
|
||||
vote_account.set_state(&vote_state)
|
||||
}
|
||||
|
||||
/// Initialize the vote_state for a vote account
|
||||
/// Assumes that the account is being init as part of a account creation or balance transfer and
|
||||
/// that the transaction must be signed by the staker's keys
|
||||
pub fn initialize_account(
|
||||
vote_account: &mut KeyedAccount,
|
||||
node_id: &Pubkey,
|
||||
commission: u32,
|
||||
) -> Result<(), InstructionError> {
|
||||
let vote_state: VoteState = vote_account.state()?;
|
||||
|
||||
if vote_state.authorized_voter_id != Pubkey::default() {
|
||||
return Err(InstructionError::AccountAlreadyInitialized);
|
||||
}
|
||||
vote_account.set_state(&VoteState::new(
|
||||
vote_account.unsigned_key(),
|
||||
node_id,
|
||||
commission,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn process_vote(
|
||||
vote_account: &mut KeyedAccount,
|
||||
other_signers: &[KeyedAccount],
|
||||
votes: &[Vote],
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut vote_state: VoteState = vote_account.state()?;
|
||||
|
||||
if vote_state.authorized_voter_id == Pubkey::default() {
|
||||
return Err(InstructionError::UninitializedAccount);
|
||||
}
|
||||
|
||||
let authorized = Some(&vote_state.authorized_voter_id);
|
||||
// find a signer that matches the authorized_voter_id
|
||||
if vote_account.signer_key() != authorized
|
||||
&& other_signers
|
||||
.iter()
|
||||
.all(|account| account.signer_key() != authorized)
|
||||
{
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
vote_state.process_votes(&votes);
|
||||
vote_account.set_state(&vote_state)
|
||||
}
|
||||
|
||||
// utility function, used by Bank, tests
|
||||
pub fn create_account(
|
||||
vote_id: &Pubkey,
|
||||
node_id: &Pubkey,
|
||||
commission: u32,
|
||||
lamports: u64,
|
||||
) -> Account {
|
||||
let mut vote_account = Account::new(lamports, VoteState::size_of(), &id());
|
||||
|
||||
initialize_account(
|
||||
&mut KeyedAccount::new(vote_id, false, &mut vote_account),
|
||||
node_id,
|
||||
commission,
|
||||
)
|
||||
.unwrap();
|
||||
vote_account
|
||||
}
|
||||
|
||||
// utility function, used by Bank, tests
|
||||
pub fn vote(
|
||||
vote_id: &Pubkey,
|
||||
vote_account: &mut Account,
|
||||
vote: &Vote,
|
||||
) -> Result<VoteState, InstructionError> {
|
||||
process_vote(
|
||||
&mut KeyedAccount::new(vote_id, true, vote_account),
|
||||
&[],
|
||||
&[vote.clone()],
|
||||
)?;
|
||||
vote_account.state()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::vote_state;
|
||||
|
||||
const MAX_RECENT_VOTES: usize = 16;
|
||||
|
||||
#[test]
|
||||
fn test_initialize_vote_account() {
|
||||
let vote_account_id = Pubkey::new_rand();
|
||||
let mut vote_account = Account::new(100, VoteState::size_of(), &id());
|
||||
|
||||
let node_id = Pubkey::new_rand();
|
||||
|
||||
//init should pass
|
||||
let mut vote_account = KeyedAccount::new(&vote_account_id, false, &mut vote_account);
|
||||
let res = initialize_account(&mut vote_account, &node_id, 0);
|
||||
assert_eq!(res, Ok(()));
|
||||
|
||||
// reinit should fail
|
||||
let res = initialize_account(&mut vote_account, &node_id, 0);
|
||||
assert_eq!(res, Err(InstructionError::AccountAlreadyInitialized));
|
||||
}
|
||||
|
||||
fn create_test_account() -> (Pubkey, Account) {
|
||||
let vote_id = Pubkey::new_rand();
|
||||
(
|
||||
vote_id,
|
||||
vote_state::create_account(&vote_id, &Pubkey::new_rand(), 0, 100),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_serialize() {
|
||||
let mut buffer: Vec<u8> = vec![0; VoteState::size_of()];
|
||||
let mut vote_state = VoteState::default();
|
||||
vote_state
|
||||
.votes
|
||||
.resize(MAX_LOCKOUT_HISTORY, Lockout::default());
|
||||
assert!(vote_state.serialize(&mut buffer[0..4]).is_err());
|
||||
vote_state.serialize(&mut buffer).unwrap();
|
||||
assert_eq!(VoteState::deserialize(&buffer).unwrap(), vote_state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_voter_registration() {
|
||||
let (vote_id, vote_account) = create_test_account();
|
||||
|
||||
let vote_state: VoteState = vote_account.state().unwrap();
|
||||
assert_eq!(vote_state.authorized_voter_id, vote_id);
|
||||
assert!(vote_state.votes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote() {
|
||||
let (vote_id, mut vote_account) = create_test_account();
|
||||
|
||||
let vote = Vote::new(1);
|
||||
let vote_state = vote_state::vote(&vote_id, &mut vote_account, &vote).unwrap();
|
||||
assert_eq!(vote_state.votes, vec![Lockout::new(&vote)]);
|
||||
assert_eq!(vote_state.credits(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_signature() {
|
||||
let (vote_id, mut vote_account) = create_test_account();
|
||||
|
||||
let vote = vec![Vote::new(1)];
|
||||
|
||||
// unsigned
|
||||
let res = process_vote(
|
||||
&mut KeyedAccount::new(&vote_id, false, &mut vote_account),
|
||||
&[],
|
||||
&vote,
|
||||
);
|
||||
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
|
||||
|
||||
// unsigned
|
||||
let res = process_vote(
|
||||
&mut KeyedAccount::new(&vote_id, true, &mut vote_account),
|
||||
&[],
|
||||
&vote,
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
|
||||
// another voter
|
||||
let authorized_voter_id = Pubkey::new_rand();
|
||||
let res = authorize_voter(
|
||||
&mut KeyedAccount::new(&vote_id, false, &mut vote_account),
|
||||
&[],
|
||||
&authorized_voter_id,
|
||||
);
|
||||
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
|
||||
|
||||
let res = authorize_voter(
|
||||
&mut KeyedAccount::new(&vote_id, true, &mut vote_account),
|
||||
&[],
|
||||
&authorized_voter_id,
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
// verify authorized_voter_id can authorize authorized_voter_id ;)
|
||||
let res = authorize_voter(
|
||||
&mut KeyedAccount::new(&vote_id, false, &mut vote_account),
|
||||
&[KeyedAccount::new(
|
||||
&authorized_voter_id,
|
||||
true,
|
||||
&mut Account::default(),
|
||||
)],
|
||||
&authorized_voter_id,
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
|
||||
// not signed by authorized voter
|
||||
let vote = vec![Vote::new(2)];
|
||||
let res = process_vote(
|
||||
&mut KeyedAccount::new(&vote_id, true, &mut vote_account),
|
||||
&[],
|
||||
&vote,
|
||||
);
|
||||
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
|
||||
|
||||
// signed by authorized voter
|
||||
let vote = vec![Vote::new(2)];
|
||||
let res = process_vote(
|
||||
&mut KeyedAccount::new(&vote_id, false, &mut vote_account),
|
||||
&[KeyedAccount::new(
|
||||
&authorized_voter_id,
|
||||
true,
|
||||
&mut Account::default(),
|
||||
)],
|
||||
&vote,
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_without_initialization() {
|
||||
let vote_id = Pubkey::new_rand();
|
||||
let mut vote_account = Account::new(100, VoteState::size_of(), &id());
|
||||
|
||||
let res = vote_state::vote(&vote_id, &mut vote_account, &Vote::new(1));
|
||||
assert_eq!(res, Err(InstructionError::UninitializedAccount));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_lockout() {
|
||||
let (_vote_id, vote_account) = create_test_account();
|
||||
|
||||
let mut vote_state: VoteState = vote_account.state().unwrap();
|
||||
|
||||
for i in 0..(MAX_LOCKOUT_HISTORY + 1) {
|
||||
vote_state.process_vote(&Vote::new((INITIAL_LOCKOUT as usize * i) as u64));
|
||||
}
|
||||
|
||||
// The last vote should have been popped b/c it reached a depth of MAX_LOCKOUT_HISTORY
|
||||
assert_eq!(vote_state.votes.len(), MAX_LOCKOUT_HISTORY);
|
||||
assert_eq!(vote_state.root_slot, Some(0));
|
||||
check_lockouts(&vote_state);
|
||||
|
||||
// One more vote that confirms the entire stack,
|
||||
// the root_slot should change to the
|
||||
// second vote
|
||||
let top_vote = vote_state.votes.front().unwrap().slot;
|
||||
vote_state.process_vote(&Vote::new(
|
||||
vote_state.votes.back().unwrap().expiration_slot(),
|
||||
));
|
||||
assert_eq!(Some(top_vote), vote_state.root_slot);
|
||||
|
||||
// Expire everything except the first vote
|
||||
let vote = Vote::new(vote_state.votes.front().unwrap().expiration_slot());
|
||||
vote_state.process_vote(&vote);
|
||||
// First vote and new vote are both stored for a total of 2 votes
|
||||
assert_eq!(vote_state.votes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_double_lockout_after_expiration() {
|
||||
let voter_id = Pubkey::new_rand();
|
||||
let mut vote_state = VoteState::new(&voter_id, &Pubkey::new_rand(), 0);
|
||||
|
||||
for i in 0..3 {
|
||||
let vote = Vote::new(i as u64);
|
||||
vote_state.process_vote(&vote);
|
||||
}
|
||||
|
||||
check_lockouts(&vote_state);
|
||||
|
||||
// Expire the third vote (which was a vote for slot 2). The height of the
|
||||
// vote stack is unchanged, so none of the previous votes should have
|
||||
// doubled in lockout
|
||||
vote_state.process_vote(&Vote::new((2 + INITIAL_LOCKOUT + 1) as u64));
|
||||
check_lockouts(&vote_state);
|
||||
|
||||
// Vote again, this time the vote stack depth increases, so the lockouts should
|
||||
// double for everybody
|
||||
vote_state.process_vote(&Vote::new((2 + INITIAL_LOCKOUT + 2) as u64));
|
||||
check_lockouts(&vote_state);
|
||||
|
||||
// Vote again, this time the vote stack depth increases, so the lockouts should
|
||||
// double for everybody
|
||||
vote_state.process_vote(&Vote::new((2 + INITIAL_LOCKOUT + 3) as u64));
|
||||
check_lockouts(&vote_state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expire_multiple_votes() {
|
||||
let voter_id = Pubkey::new_rand();
|
||||
let mut vote_state = VoteState::new(&voter_id, &Pubkey::new_rand(), 0);
|
||||
|
||||
for i in 0..3 {
|
||||
let vote = Vote::new(i as u64);
|
||||
vote_state.process_vote(&vote);
|
||||
}
|
||||
|
||||
assert_eq!(vote_state.votes[0].confirmation_count, 3);
|
||||
|
||||
// Expire the second and third votes
|
||||
let expire_slot = vote_state.votes[1].slot + vote_state.votes[1].lockout() + 1;
|
||||
vote_state.process_vote(&Vote::new(expire_slot));
|
||||
assert_eq!(vote_state.votes.len(), 2);
|
||||
|
||||
// Check that the old votes expired
|
||||
assert_eq!(vote_state.votes[0].slot, 0);
|
||||
assert_eq!(vote_state.votes[1].slot, expire_slot);
|
||||
|
||||
// Process one more vote
|
||||
vote_state.process_vote(&Vote::new(expire_slot + 1));
|
||||
|
||||
// Confirmation count for the older first vote should remain unchanged
|
||||
assert_eq!(vote_state.votes[0].confirmation_count, 3);
|
||||
|
||||
// The later votes should still have increasing confirmation counts
|
||||
assert_eq!(vote_state.votes[1].confirmation_count, 2);
|
||||
assert_eq!(vote_state.votes[2].confirmation_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_credits() {
|
||||
let voter_id = Pubkey::new_rand();
|
||||
let mut vote_state = VoteState::new(&voter_id, &Pubkey::new_rand(), 0);
|
||||
|
||||
for i in 0..MAX_LOCKOUT_HISTORY {
|
||||
vote_state.process_vote(&Vote::new(i as u64));
|
||||
}
|
||||
|
||||
assert_eq!(vote_state.credits, 0);
|
||||
|
||||
vote_state.process_vote(&Vote::new(MAX_LOCKOUT_HISTORY as u64 + 1));
|
||||
assert_eq!(vote_state.credits, 1);
|
||||
vote_state.process_vote(&Vote::new(MAX_LOCKOUT_HISTORY as u64 + 2));
|
||||
assert_eq!(vote_state.credits(), 2);
|
||||
vote_state.process_vote(&Vote::new(MAX_LOCKOUT_HISTORY as u64 + 3));
|
||||
assert_eq!(vote_state.credits(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_vote() {
|
||||
let voter_id = Pubkey::new_rand();
|
||||
let mut vote_state = VoteState::new(&voter_id, &Pubkey::new_rand(), 0);
|
||||
vote_state.process_vote(&Vote::new(0));
|
||||
vote_state.process_vote(&Vote::new(1));
|
||||
vote_state.process_vote(&Vote::new(0));
|
||||
assert_eq!(vote_state.nth_recent_vote(0).unwrap().slot, 1);
|
||||
assert_eq!(vote_state.nth_recent_vote(1).unwrap().slot, 0);
|
||||
assert!(vote_state.nth_recent_vote(2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nth_recent_vote() {
|
||||
let voter_id = Pubkey::new_rand();
|
||||
let mut vote_state = VoteState::new(&voter_id, &Pubkey::new_rand(), 0);
|
||||
for i in 0..MAX_LOCKOUT_HISTORY {
|
||||
vote_state.process_vote(&Vote::new(i as u64));
|
||||
}
|
||||
for i in 0..(MAX_LOCKOUT_HISTORY - 1) {
|
||||
assert_eq!(
|
||||
vote_state.nth_recent_vote(i).unwrap().slot as usize,
|
||||
MAX_LOCKOUT_HISTORY - i - 1,
|
||||
);
|
||||
}
|
||||
assert!(vote_state.nth_recent_vote(MAX_LOCKOUT_HISTORY).is_none());
|
||||
}
|
||||
|
||||
fn check_lockouts(vote_state: &VoteState) {
|
||||
for (i, vote) in vote_state.votes.iter().enumerate() {
|
||||
let num_lockouts = vote_state.votes.len() - i;
|
||||
assert_eq!(
|
||||
vote.lockout(),
|
||||
INITIAL_LOCKOUT.pow(num_lockouts as u32) as u64
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn recent_votes(vote_state: &VoteState) -> Vec<Vote> {
|
||||
let start = vote_state.votes.len().saturating_sub(MAX_RECENT_VOTES);
|
||||
(start..vote_state.votes.len())
|
||||
.map(|i| Vote::new(vote_state.votes.get(i).unwrap().slot))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// check that two accounts with different data can be brought to the same state with one vote submission
|
||||
#[test]
|
||||
fn test_process_missed_votes() {
|
||||
let account_a = Pubkey::new_rand();
|
||||
let mut vote_state_a = VoteState::new(&account_a, &Pubkey::new_rand(), 0);
|
||||
let account_b = Pubkey::new_rand();
|
||||
let mut vote_state_b = VoteState::new(&account_b, &Pubkey::new_rand(), 0);
|
||||
|
||||
// process some votes on account a
|
||||
let votes_a: Vec<_> = (0..5).into_iter().map(|i| Vote::new(i)).collect();
|
||||
vote_state_a.process_votes(&votes_a);
|
||||
assert_ne!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
|
||||
|
||||
// as long as b has missed less than "NUM_RECENT" votes both accounts should be in sync
|
||||
let votes: Vec<_> = (0..MAX_RECENT_VOTES)
|
||||
.into_iter()
|
||||
.map(|i| Vote::new(i as u64))
|
||||
.collect();
|
||||
vote_state_a.process_votes(&votes);
|
||||
vote_state_b.process_votes(&votes);
|
||||
assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "solana-vote-program"
|
||||
version = "0.14.0"
|
||||
description = "Solana vote program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.2"
|
||||
solana-logger = { path = "../../logger", version = "0.14.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.14.0" }
|
||||
solana-vote-api = { path = "../vote_api", version = "0.14.0" }
|
||||
|
||||
[lib]
|
||||
name = "solana_vote_program"
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -1,3 +0,0 @@
|
||||
use solana_vote_api::vote_instruction::process_instruction;
|
||||
|
||||
solana_sdk::solana_entrypoint!(process_instruction);
|
Reference in New Issue
Block a user