diff --git a/README.md b/README.md index b69d67a3c..43ec57d55 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,45 @@ # pyth-client -client API for on-chain pyth programs + +Pyth oracle program and off-chain client API. + +## Oracle Program + +The Pyth oracle program lives in the `program/` directory. +It consists of both C and Rust code, but everything can be built and tested using `cargo`. + +### Build Instructions + +First, make sure you have the [solana tool suite](https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool) +installed on your machine. (The build depends on some C makefiles that are in the tool suite.) + +Then, simply run `cargo build` to compile the oracle program as a native binary, or `cargo build-bpf` to build a BPF binary +that can be uploaded to the blockchain. This step will produce a program binary `target/deploy/pyth_oracle.so`. + +### Testing + +Simply run `cargo test`. This command will run several sets of tests: + +- Unit tests of individual functions +- Simulated transaction tests against the BPF binary running on a solana simulator +- Exhaustive / randomized test batteries for core oracle functionality + +Rust tests live in the `tests/` module of the rust code, and C tests are named something like `test_*.c`. +The C tests are linked into the rust binary so they run as part of `cargo test` as well (see `tests/test_c_code.rs`). + +You can also run `cargo test-bpf`, which runs the same tests as `cargo test`, though it's slightly slower and the UX is worse. + +### pre-commit hooks +pre-commit is a tool that checks and fixes simple issues (formatting, ...) before each commit. You can install it by following [their website](https://pre-commit.com/). In order to enable checks for this repo run `pre-commit install` from command-line in the root of this repo. + +The checks are also performed in the CI to ensure the code follows consistent formatting. Formatting is only currently enforced in the `program/` directory. +You might also need to install the nightly toolchain to run the formatting by running `rustup toolchain install nightly`. + +## pythd (off-chain client API) + +> :warning: pythd is deprecated and has been replaced by [pyth-agent](https://github.com/pyth-network/pyth-agent). +> This new client is backward compatible with pythd, but more stable and configurable. + +`pythd` provides exposes a web API for interacting with the on-chain oracle program. ### Build Instructions @@ -44,30 +84,6 @@ This command runs a recent pyth-client docker image that already has the necessa Therefore, once the container is running, all you have to do is run `cd pyth-client && ./scripts/build.sh`. Note that updates to the `pyth-client` directory made inside the docker container will be persisted to the host filesystem (which is probably desirable). -### Local development - -First, make sure you're building on the x86_64 architecture. -On a mac, this command will switch your shell to x86_64: - -`env /usr/bin/arch -x86_64 /bin/bash --login` - -then in the `program/c` directory, run: - -``` -make -make cpyth-bpf -make cpyth-native -``` - -then in the `program/rust` directory, run: - -``` -cargo build-bpf -cargo test -``` - -Note that the tests depend on the bpf build! - ### Fuzzing Build a docker image for running fuzz tests: @@ -130,8 +146,3 @@ root@pyth-dev# usermod -u 1002 -g 1004 -s /bin/bash pyth Finally, in docker extension inside VS Code click right and choose "Attach VS Code". If you're using a remote host in VS Code make sure to let this connection be open. -### pre-commit hooks -pre-commit is a tool that checks and fixes simple issues (formatting, ...) before each commit. You can install it by following [their website](https://pre-commit.com/). In order to enable checks for this repo run `pre-commit install` from command-line in the root of this repo. - -The checks are also performed in the CI to ensure the code follows consistent formatting. Formatting is only currently enforced in the `program/` directory. -You might also need to install the nightly toolchain to run the formatting by running `rustup toolchain install nightly`. diff --git a/docker/Dockerfile b/docker/Dockerfile index 5b89a4b8d..f1a7dccaa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -63,8 +63,6 @@ RUN ./pyth-client/scripts/patch-solana.sh # Build and test the oracle program. RUN cd pyth-client && ./scripts/build-bpf.sh . RUN cd pyth-client && ./scripts/check-size.sh -# Run aggregation logic tests -RUN cd pyth-client && ./scripts/run-aggregation-tests.sh RUN cd pyth-client/pyth && poetry install && poetry run python -m pytest ENTRYPOINT [] diff --git a/program/c/makefile b/program/c/makefile index 29d6c0260..d1a872a3c 100644 --- a/program/c/makefile +++ b/program/c/makefile @@ -16,7 +16,7 @@ endif # The all target is defined by the solana makefile included above and generates the needed .o file. .PHONY: cpyth-bpf cpyth-bpf: all - bash -c "ar rcs $(OUT_DIR)/libcpyth-bpf.a $(OUT_DIR)/**/*.o" + bash -c "ar rcs $(OUT_DIR)/libcpyth-bpf.a $(OUT_DIR)/oracle/*.o" # 2-Stage Contract Build @@ -27,3 +27,18 @@ cpyth-bpf: all cpyth-native: gcc -c ./src/oracle/native/upd_aggregate.c -o $(OUT_DIR)/cpyth-native.o -fPIC ar rcs $(OUT_DIR)/libcpyth-native.a $(OUT_DIR)/cpyth-native.o + + +# Note: there's probably a smart way to do this with wildcards but I (jayant) can't figure it out +.PHONY: test +test: + mkdir -p $(OUT_DIR)/test/ + gcc -c ./src/oracle/model/test_price_model.c -o $(OUT_DIR)/test/test_price_model.o -fPIC + gcc -c ./src/oracle/sort/test_sort_stable.c -o $(OUT_DIR)/test/test_sort_stable.o -fPIC + gcc -c ./src/oracle/util/test_align.c -o $(OUT_DIR)/test/test_align.o -fPIC + gcc -c ./src/oracle/util/test_avg.c -o $(OUT_DIR)/test/test_avg.o -fPIC + gcc -c ./src/oracle/util/test_hash.c -o $(OUT_DIR)/test/test_hash.o -fPIC + gcc -c ./src/oracle/util/test_prng.c -o $(OUT_DIR)/test/test_prng.o -fPIC + gcc -c ./src/oracle/util/test_round.c -o $(OUT_DIR)/test/test_round.o -fPIC + gcc -c ./src/oracle/util/test_sar.c -o $(OUT_DIR)/test/test_sar.o -fPIC + ar rcs $(OUT_DIR)/libcpyth-test.a $(OUT_DIR)/test/*.o diff --git a/program/c/src/oracle/model/clean b/program/c/src/oracle/model/clean deleted file mode 100755 index 2ca7bfcfd..000000000 --- a/program/c/src/oracle/model/clean +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -rm -rfv bin diff --git a/program/c/src/oracle/model/run_tests b/program/c/src/oracle/model/run_tests deleted file mode 100755 index 5c1d4d1dc..000000000 --- a/program/c/src/oracle/model/run_tests +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -./clean || exit 1 -mkdir -pv bin || exit 1 - -CC="gcc -g -Wall -Werror -Wextra -Wconversion -Wstrict-aliasing=2 -Wimplicit-fallthrough=2 -pedantic -D_XOPEN_SOURCE=600 -O2 -march=native -std=c17" - -set -x - -$CC test_price_model.c price_model.c -o bin/test_price_model || exit 1 - -bin/test_price_model || exit 1 - -echo all tests passed diff --git a/program/c/src/oracle/model/test_price_model.c b/program/c/src/oracle/model/test_price_model.c index 06038d8e6..71b74127a 100644 --- a/program/c/src/oracle/model/test_price_model.c +++ b/program/c/src/oracle/model/test_price_model.c @@ -14,10 +14,7 @@ qcmp( void const * _p, return 0; } -int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; +int test_price_model() { prng_t _prng[1]; prng_t * prng = prng_join( prng_new( _prng, (uint32_t)0, (uint64_t)0 ) ); @@ -29,10 +26,7 @@ main( int argc, int64_t val [3]; int64_t scratch[N]; - int ctr = 0; for( int iter=0; iter<10000000; iter++ ) { - if( !ctr ) { printf( "Completed %u iterations\n", iter ); ctr = 100000; } - ctr--; /* Generate a random test */ @@ -63,6 +57,5 @@ main( int argc, prng_delete( prng_leave( prng ) ); - printf( "pass\n" ); return 0; } diff --git a/program/c/src/oracle/sort/clean b/program/c/src/oracle/sort/clean deleted file mode 100755 index 2ca7bfcfd..000000000 --- a/program/c/src/oracle/sort/clean +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -rm -rfv bin diff --git a/program/c/src/oracle/sort/run_tests b/program/c/src/oracle/sort/run_tests deleted file mode 100755 index 3c0e1b699..000000000 --- a/program/c/src/oracle/sort/run_tests +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -./clean || exit 1 -mkdir -pv bin || exit 1 - -CC="gcc -g -Wall -Werror -Wextra -Wconversion -Wstrict-aliasing=2 -Wimplicit-fallthrough=2 -pedantic -D_XOPEN_SOURCE=600 -O2 -march=native -std=c17" - -set -x - -$CC test_sort_stable.c -o bin/test_sort_stable || exit 1 - -bin/test_sort_stable || exit 1 - -echo all tests passed diff --git a/program/c/src/oracle/sort/test_sort_stable.c b/program/c/src/oracle/sort/test_sort_stable.c index dc620a0d8..0b7c4cb7d 100644 --- a/program/c/src/oracle/sort/test_sort_stable.c +++ b/program/c/src/oracle/sort/test_sort_stable.c @@ -9,10 +9,7 @@ #define SORT_BEFORE(i,j) BEFORE(i,j) #include "tmpl/sort_stable.c" -int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; +int test_sort_stable() { # define N 96 int x[N]; @@ -23,7 +20,6 @@ main( int argc, additional information in the keys to validate stability as well). */ for( int n=0; n<=24; n++ ) { - printf( "Zero-One: Testing n=%i\n", n ); for( long b=0L; b<(1L<>i) & 1L))<<16) | i; for( int i=0; i~20 min (seq 0, idx 0) -#bin/test_prng_battery 4 0 0 || exit 1 # BigCrush: Takes >~3 hours (seq 0, idx 0) - -echo all tests passed diff --git a/program/c/src/oracle/util/test_align.c b/program/c/src/oracle/util/test_align.c index f958c89f2..e1a9e4468 100644 --- a/program/c/src/oracle/util/test_align.c +++ b/program/c/src/oracle/util/test_align.c @@ -3,9 +3,7 @@ #include "util.h" int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; +test_align() { uint32_t shift_mask = (uint32_t)(sizeof(uintptr_t)*(size_t)CHAR_BIT); if( !align_ispow2( (uintptr_t)shift_mask ) ) { @@ -17,10 +15,7 @@ main( int argc, prng_t _prng[1]; prng_t * prng = prng_join( prng_new( _prng, (uint32_t)0, (uint64_t)0 ) ); - int ctr = 0; for( int i=0; i<1000000000; i++ ) { - if( !ctr ) { printf( "Completed %i iterations\n", i ); ctr = 10000000; } - ctr--; /* Test align_ispow2 */ @@ -70,7 +65,5 @@ main( int argc, prng_delete( prng_leave( prng ) ); - printf( "pass\n" ); - return 0; } diff --git a/program/c/src/oracle/util/test_avg.c b/program/c/src/oracle/util/test_avg.c index 98f4f2502..0e84fe7f3 100644 --- a/program/c/src/oracle/util/test_avg.c +++ b/program/c/src/oracle/util/test_avg.c @@ -2,19 +2,11 @@ #include "util.h" int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; - +test_avg() { prng_t _prng[1]; prng_t * prng = prng_join( prng_new( _prng, (uint32_t)0, (uint64_t)0 ) ); - int ctr; - - ctr = 0; - for( int i=0; i<1000000000; i++ ) { - if( !ctr ) { printf( "reg: Completed %i iterations\n", i ); ctr = 10000000; } - ctr--; + for( int i=0; i<100000000; i++ ) { # define TEST(w) do { \ uint##w##_t x = prng_uint##w( prng ); \ @@ -75,18 +67,15 @@ main( int argc, # define N 512 - ctr = 0; - for( int i=0; i<10000000; i++ ) { - if( !ctr ) { printf( "mem: Completed %i iterations\n", i ); ctr = 100000; } - ctr--; + for( int i=0; i<1000000; i++ ) { # define TEST(w) do { \ uint##w##_t x[N]; \ uint32_t n = prng_uint32( prng ) & (uint32_t)(N-1); \ uint64_t a = (uint64_t)0; \ - for( uint32_t i=(uint32_t)0; i -#include -#include /* Assumes TestU01 install include directory is in the include search path */ -#include "util.h" - -static double -test_GetU01( void * param, - void * state ) { - (void)param; - return 2.3283064365386962890625e-10 /* 2^-32, exact */ * (double)prng_uint32( (prng_t *)state ); -} - -static unsigned long -test_GetBits( void * param, - void * state ) { - (void)param; - return (unsigned long)prng_uint32( (prng_t *)state ); -} - -static void -test_Write( void * state ) { - prng_t * prng = (prng_t *)state; - printf( "prng(0x%08lxU,0x%016lxUL)\n", (unsigned long)prng_seq( prng ), (unsigned long)prng_idx( prng ) ); -} - -static void -usage( char const * cmd ) { - fprintf( stderr, - "Usage: %s [bat] [seq] [idx]\n" - "\tbat 0 - FIPS-140.2 (fast)\n" - "\tbat 1 - Pseudo-DIEHARD (fast)\n" - "\tbat 2 - TestU01 SmallCrush (fast)\n" - "\tbat 3 - TestU01 Crush (~20 minutes)\n" - "\tbat 4 - TestU01 BigCrush (several hours)\n" - "Note that when running a test in a battery, the probability of it failing even\n" - "though the generator is fine is roughly 0.2%%. Thus, when repeatedly running\n" - "batteries that themselves contain a large number of tests, some test failures\n" - "are expected. So long as the test failures are are sporadic, don't occur in the\n" - "same place when running multiple times with random seq and/or idx, and don't\n" - "have p-values improbably close to 0 (p <<< 1/overall_num_tests_run) or 1\n" - "(1-p <<< 1/overall_num_tests_run), it is expected and normal\n", - cmd ); -} - -int -main( int argc, - char ** argv ) { - - /* Get command line arguments */ - - if( argc!=4 ) { usage( argv[0] ); return 1; } - int bat = atoi( argv[1] ); - uint32_t seq = (uint32_t)strtoul( argv[2], NULL, 0 ); - uint64_t idx = (uint64_t)strtoul( argv[3], NULL, 0 ); - - /* Create the test generator */ - - prng_t _prng[1]; - prng_t * prng = prng_join( prng_new( _prng, seq, idx ) ); - - /* Run the requested test battery */ - - char name[128]; - sprintf( name, "prng(0x%08lxU,0x%016lxUL)", (unsigned long)prng_seq( prng ), (unsigned long)prng_idx( prng ) ); - - unif01_Gen gen[1]; - gen->state = prng; - gen->param = NULL; - gen->name = name; - gen->GetU01 = test_GetU01; - gen->GetBits = test_GetBits; - gen->Write = test_Write; - - switch( bat ) { - case 0: bbattery_FIPS_140_2( gen ); break; - case 1: bbattery_pseudoDIEHARD( gen ); break; - case 2: bbattery_SmallCrush( gen ); break; - case 3: bbattery_Crush( gen ); break; - case 4: bbattery_BigCrush( gen ); break; -//case 5: bbattery_Rabbit( gen ); break; /* FIXME: NB */ -//case 6: bbattery_Alphabit( gen ); break; /* FIXME: NB/R/S */ -//case 7: bbattery_BlockAlphabit( gen ); break; /* FIXME: NB/R/S */ - default: usage( argv[0] ); return 1; - } - - /* Destroy the test generator */ - - prng_delete( prng_leave( prng ) ); - - return 0; -} diff --git a/program/c/src/oracle/util/test_round.c b/program/c/src/oracle/util/test_round.c index c9326d832..5b4afa6e1 100644 --- a/program/c/src/oracle/util/test_round.c +++ b/program/c/src/oracle/util/test_round.c @@ -1,17 +1,11 @@ #include int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; - +test_round() { unsigned i = (unsigned)0; - int ctr = 0; for( int x=-32767; x<=32767; x++ ) { for( int y=-32767; y<=32767; y++ ) { - if( !ctr ) { printf( "Completed %u iterations\n", i ); ctr = 10000000; } - ctr--; int u = (x+y)>>1; int v = (x>>1)+(y>>1); @@ -24,7 +18,6 @@ main( int argc, i++; } } - printf( "pass\n" ); return 0; } diff --git a/program/c/src/oracle/util/test_sar.c b/program/c/src/oracle/util/test_sar.c index 88366fb2c..3888795f0 100644 --- a/program/c/src/oracle/util/test_sar.c +++ b/program/c/src/oracle/util/test_sar.c @@ -2,9 +2,7 @@ #include "util.h" int -main( int argc, - char ** argv ) { - (void)argc; (void)argv; +test_sar() { prng_t _prng[1]; prng_t * prng = prng_join( prng_new( _prng, (uint32_t)0, (uint64_t)0 ) ); @@ -12,10 +10,7 @@ main( int argc, /* FIXME: EXPLICT COVERAGE OF EDGE CASES (PROBABLY STATICALLY FULLY SAMPLED ALREADY THOUGH FOR 8 AND 16 BIT TYPES) */ - int ctr = 0; - for( int i=0; i<1000000000; i++ ) { - if( !ctr ) { printf( "Completed %i iterations\n", i ); ctr = 10000000; } - ctr--; + for( int i=0; i<100000000; i++ ) { /* These tests assume the unit test platform has arithmetic right shift for signed integers (and the diagnostic printfs assume that @@ -42,7 +37,5 @@ main( int argc, prng_delete( prng_leave( prng ) ); - printf( "pass\n" ); - return 0; } diff --git a/program/rust/build.rs b/program/rust/build.rs index fa632b0a3..a4ab31024 100644 --- a/program/rust/build.rs +++ b/program/rust/build.rs @@ -21,17 +21,22 @@ fn main() { let out_dir = std::env::var("OUT_DIR").unwrap(); let out_dir = PathBuf::from(out_dir); + let mut make_targets = vec![]; + if target_arch == "bpf" { + make_targets.push("cpyth-bpf"); + } else { + make_targets.push("cpyth-native"); + } + make_targets.push("test"); + + // We must forward OUT_DIR as an env variable to the make script otherwise it will output // its artifacts to the wrong place. std::process::Command::new("make") .env("VERBOSE", "1") .env("OUT_DIR", out_dir.display().to_string()) .current_dir("../c") - .args([if target_arch == "bpf" { - "cpyth-bpf" - } else { - "cpyth-native" - }]) + .args(make_targets) .status() .expect("Failed to build C program"); @@ -41,6 +46,7 @@ fn main() { } else { println!("cargo:rustc-link-lib=static=cpyth-native"); } + println!("cargo:rustc-link-lib=static=cpyth-test"); println!("cargo:rustc-link-search={}", out_dir.display()); // Generate and write bindings diff --git a/program/rust/src/tests/mod.rs b/program/rust/src/tests/mod.rs index bcf334029..dc57920cf 100644 --- a/program/rust/src/tests/mod.rs +++ b/program/rust/src/tests/mod.rs @@ -4,6 +4,7 @@ mod test_add_price; mod test_add_product; mod test_add_publisher; mod test_aggregation; +mod test_c_code; mod test_del_price; mod test_del_product; mod test_del_publisher; diff --git a/program/rust/src/tests/test_c_code.rs b/program/rust/src/tests/test_c_code.rs new file mode 100644 index 000000000..3bd6a9ac8 --- /dev/null +++ b/program/rust/src/tests/test_c_code.rs @@ -0,0 +1,72 @@ +// This file links the various test_*.c files in the C oracle code so they run via cargo. +// Note that most of these tests are exhaustive (testing almost every possible input/output), so +// they take a minute or so to run. +mod c { + #[link(name = "cpyth-test")] + extern "C" { + pub fn test_price_model() -> i32; + pub fn test_sort_stable() -> i32; + pub fn test_align() -> i32; + pub fn test_avg() -> i32; + pub fn test_hash() -> i32; + pub fn test_prng() -> i32; + pub fn test_round() -> i32; + pub fn test_sar() -> i32; + } +} + +#[test] +fn test_price_model() { + unsafe { + assert_eq!(c::test_price_model(), 0); + } +} + +#[test] +fn test_sort_stable() { + unsafe { + assert_eq!(c::test_sort_stable(), 0); + } +} + +#[test] +fn test_align() { + unsafe { + assert_eq!(c::test_align(), 0); + } +} + +#[test] +fn test_avg() { + unsafe { + assert_eq!(c::test_avg(), 0); + } +} + +#[test] +fn test_hash() { + unsafe { + assert_eq!(c::test_hash(), 0); + } +} + +#[test] +fn test_prng() { + unsafe { + assert_eq!(c::test_prng(), 0); + } +} + +#[test] +fn test_round() { + unsafe { + assert_eq!(c::test_round(), 0); + } +} + +#[test] +fn test_sar() { + unsafe { + assert_eq!(c::test_sar(), 0); + } +} diff --git a/scripts/run-aggregation-tests.sh b/scripts/run-aggregation-tests.sh deleted file mode 100755 index 9854d9133..000000000 --- a/scripts/run-aggregation-tests.sh +++ /dev/null @@ -1,11 +0,0 @@ -set -eux - -PYTH_DIR=$( cd "${1:-.}" && pwd) -C_DIR="$PYTH_DIR/program/c/" - -cd "${C_DIR}/src/oracle/model" -./run_tests -cd "${C_DIR}/src/oracle/sort" -./run_tests -cd "${C_DIR}/src/oracle/util" -./run_tests \ No newline at end of file