Skip to content

Commit

Permalink
runtime: add instruction processing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
riptl committed Jan 17, 2024
1 parent 03de6bb commit b8ad346
Show file tree
Hide file tree
Showing 8 changed files with 798 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/flamenco/runtime/tests/Local.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$(call add-hdrs,fd_exec_test.pb.h)
$(call add-objs,fd_exec_test.pb,fd_flamenco)

ifdef FD_HAS_INT128
$(call add-hdrs,fd_exec_instr_test.h)
$(call add-objs,fd_exec_instr_test,fd_flamenco)

$(call make-unit-test,test_exec_instr,test_exec_instr,fd_flamenco fd_funk fd_ballet fd_util)
endif
2 changes: 2 additions & 0 deletions src/flamenco/runtime/tests/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fd_exec_test.pb.h fd_exec_test.pb.c: fd_exec_test.proto
nanopb_generator.py -I ../.. -I . -L "" -C fd_exec_test.proto
302 changes: 302 additions & 0 deletions src/flamenco/runtime/tests/fd_exec_instr_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
#define FD_SCRATCH_USE_HANDHOLDING 1
#include "fd_exec_instr_test.h"
#include "../fd_acc_mgr.h"
#include "../fd_executor.h"
#include "../context/fd_exec_epoch_ctx.h"
#include "../context/fd_exec_slot_ctx.h"
#include "../context/fd_exec_txn_ctx.h"
#include "../../../funk/fd_funk.h"
#include <assert.h>

struct __attribute__((aligned(32UL))) fd_exec_instr_test_runner_private {
fd_funk_t * funk;
};

ulong
fd_exec_instr_test_runner_align( void ) {
return alignof(fd_exec_instr_test_runner_t);
}

ulong
fd_exec_instr_test_runner_footprint( void ) {
ulong l = FD_LAYOUT_INIT;
l = FD_LAYOUT_APPEND( l, alignof(fd_exec_instr_test_runner_t), sizeof(fd_exec_instr_test_runner_t) );
l = FD_LAYOUT_APPEND( l, fd_funk_align(), fd_funk_footprint() );
return l;
}

fd_exec_instr_test_runner_t *
fd_exec_instr_test_runner_new( void * mem,
ulong wksp_tag ) {
FD_SCRATCH_ALLOC_INIT( l, mem );
void * runner_mem = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_exec_instr_test_runner_t), sizeof(fd_exec_instr_test_runner_t) );
void * funk_mem = FD_SCRATCH_ALLOC_APPEND( l, fd_funk_align(), fd_funk_footprint() );
FD_SCRATCH_ALLOC_FINI( l, alignof(fd_exec_instr_test_runner_t) );

ulong txn_max = 4+fd_tile_cnt();
ulong rec_max = 1024UL;
fd_funk_t * funk = fd_funk_join( fd_funk_new( funk_mem, wksp_tag, (ulong)fd_tickcount(), txn_max, rec_max ) );
if( FD_UNLIKELY( !funk ) ) {
FD_LOG_WARNING(( "fd_funk_new() failed" ));
return NULL;
}

fd_exec_instr_test_runner_t * runner = runner_mem;
runner->funk = funk;
return runner;
}

void *
fd_exec_instr_test_runner_delete( fd_exec_instr_test_runner_t * runner ) {
if( FD_UNLIKELY( !runner ) ) return NULL;
fd_funk_delete( fd_funk_leave( runner->funk ) );
runner->funk = NULL;
return runner;
}

static void
_load_account( fd_borrowed_account_t * acc,
fd_acc_mgr_t * acc_mgr,
fd_funk_txn_t * funk_txn,
fd_exec_test_acct_state_t const * state ) {
fd_borrowed_account_init( acc );

fd_pubkey_t pubkey[1]; memcpy( pubkey, state->address, sizeof(fd_pubkey_t) );

int err = fd_acc_mgr_modify( /* acc_mgr */ acc_mgr,
/* txn */ funk_txn,
/* pubkey */ pubkey,
/* do_create */ 1,
/* min_data_sz */ state->data->size,
acc );
assert( err==FD_ACC_MGR_SUCCESS );

fd_memcpy( acc->data, state->data->bytes, state->data->size );

acc->meta->info.lamports = state->lamports;
acc->meta->info.executable = state->executable;
acc->meta->info.rent_epoch = state->rent_epoch;
acc->meta->dlen = state->data->size;
memcpy( acc->meta->info.owner, state->owner, sizeof(fd_pubkey_t) );
}

static int
_context_create( fd_exec_instr_test_runner_t * runner,
fd_exec_instr_ctx_t * ctx,
fd_exec_test_instr_context_t const * test_ctx ) {

memset( ctx, 0, sizeof(fd_exec_instr_ctx_t) );

fd_funk_t * funk = runner->funk;

/* Generate unique ID for funk txn */

static FD_TL ulong xid_seq = 0UL;

fd_funk_txn_xid_t xid[1] = {0};
xid->ul[0] = fd_log_app_id();
xid->ul[1] = fd_log_thread_id();
xid->ul[2] = xid_seq++;
xid->ul[3] = (ulong)fd_tickcount();

/* Create temporary funk transaction and scratch contexts */

fd_funk_txn_t * funk_txn = fd_funk_txn_prepare( funk, NULL, xid, 1 );
fd_scratch_push();

/* Allocate contexts */

uchar * epoch_ctx_mem = fd_scratch_alloc( FD_EXEC_EPOCH_CTX_ALIGN, FD_EXEC_EPOCH_CTX_FOOTPRINT );
uchar * slot_ctx_mem = fd_scratch_alloc( FD_EXEC_SLOT_CTX_ALIGN, FD_EXEC_SLOT_CTX_FOOTPRINT );
uchar * txn_ctx_mem = fd_scratch_alloc( FD_EXEC_TXN_CTX_ALIGN, FD_EXEC_TXN_CTX_FOOTPRINT );

fd_exec_epoch_ctx_t * epoch_ctx = fd_exec_epoch_ctx_join( fd_exec_epoch_ctx_new( epoch_ctx_mem ) );
fd_exec_slot_ctx_t * slot_ctx = fd_exec_slot_ctx_join ( fd_exec_slot_ctx_new ( slot_ctx_mem ) );
fd_exec_txn_ctx_t * txn_ctx = fd_exec_txn_ctx_join ( fd_exec_txn_ctx_new ( txn_ctx_mem ) );

epoch_ctx->valloc = fd_scratch_virtual();

assert( epoch_ctx );
assert( slot_ctx );

/* Set up epoch context */

epoch_ctx->epoch_bank.rent.lamports_per_uint8_year = 3480;
epoch_ctx->epoch_bank.rent.exemption_threshold = 2;
epoch_ctx->epoch_bank.rent.burn_percent = 50;

/* Restore feature flags */

fd_features_disable_all( &epoch_ctx->features );
for( ulong j=0UL; j < test_ctx->feature_set.features_count; j++ ) {
ulong prefix = test_ctx->feature_set.features[j];
fd_feature_id_t const * id = fd_feature_id_query( prefix );
if( FD_UNLIKELY( !id ) ) {
FD_LOG_WARNING(( "Unsupported feature ID 0x%16lx", prefix ));
return 0;
}
/* Enabled since genesis */
fd_features_set( &epoch_ctx->features, id, 0UL );
}

/* Create account manager */

fd_acc_mgr_t * acc_mgr = fd_acc_mgr_new( fd_scratch_alloc( FD_ACC_MGR_ALIGN, FD_ACC_MGR_FOOTPRINT ), funk );

/* Set up slot context */

slot_ctx->epoch_ctx = epoch_ctx;
slot_ctx->funk_txn = funk_txn;
slot_ctx->acc_mgr = acc_mgr;
slot_ctx->valloc = fd_scratch_virtual();

/* TODO: Restore slot_bank */

/* Set up txn context */

txn_ctx->epoch_ctx = epoch_ctx;
txn_ctx->slot_ctx = slot_ctx;
txn_ctx->funk_txn = funk_txn;
txn_ctx->acc_mgr = acc_mgr;

/* Set up instruction context */

fd_instr_info_t * info = fd_scratch_alloc( alignof(fd_instr_info_t), sizeof(fd_instr_info_t) );
assert( info );
memset( info, 0, sizeof(fd_instr_info_t) );

if( test_ctx->data ) {
info->data_sz = (ushort)test_ctx->data->size;
info->data = test_ctx->data->bytes;
}

memcpy( info->program_id_pubkey.uc, test_ctx->program_id, sizeof(fd_pubkey_t) );

/* Prepare borrowed account table (correctly handles aliasing) */

fd_borrowed_account_t * borrowed_accts =
fd_scratch_alloc( alignof(fd_borrowed_account_t), test_ctx->accounts_count * sizeof(fd_borrowed_account_t) );
fd_memset( borrowed_accts, 0, test_ctx->accounts_count * sizeof(fd_borrowed_account_t) );

/* Load accounts into database */

for( ulong j=0UL; j < test_ctx->accounts_count; j++ )
_load_account( &borrowed_accts[j], acc_mgr, funk_txn, &test_ctx->accounts[j] );

/* Load instruction accounts */

if( FD_UNLIKELY( test_ctx->instr_accounts_count > 128 ) ) {
/* TODO remove this hardcoded constant */
FD_LOG_WARNING(( "Too many instruction accounts" ));
return 0;
}
for( ulong j=0UL; j < test_ctx->instr_accounts_count; j++ ) {
uint index = test_ctx->instr_accounts[j].index;
if( index >= test_ctx->accounts_count ) {
FD_LOG_WARNING(( "Instruction account index out of range" ));
return 0;
}

fd_borrowed_account_t * acc = &borrowed_accts[ index ];
uint flags = 0;
flags |= test_ctx->instr_accounts[j].is_writable ? FD_INSTR_ACCT_FLAGS_IS_WRITABLE : 0;
flags |= test_ctx->instr_accounts[j].is_signer ? FD_INSTR_ACCT_FLAGS_IS_SIGNER : 0;

info->borrowed_accounts[j] = acc;
info->acct_flags [j] = (uchar)flags;
memcpy( info->acct_pubkeys[j].uc, acc->pubkey, sizeof(fd_pubkey_t) );
}
info->acct_cnt = (uchar)test_ctx->instr_accounts_count;

ctx->epoch_ctx = epoch_ctx;
ctx->slot_ctx = slot_ctx;
ctx->txn_ctx = txn_ctx;
ctx->funk_txn = funk_txn;
ctx->acc_mgr = acc_mgr;
ctx->valloc = fd_scratch_virtual();
ctx->instr = info;

return 1;
}

static void
_context_destroy( fd_exec_instr_test_runner_t * runner,
fd_exec_instr_ctx_t * ctx ) {
fd_exec_slot_ctx_t * slot_ctx = ctx->slot_ctx;
fd_exec_epoch_ctx_t * epoch_ctx = slot_ctx->epoch_ctx;
fd_acc_mgr_t * acc_mgr = slot_ctx->acc_mgr;
fd_funk_txn_t * funk_txn = slot_ctx->funk_txn;

fd_exec_slot_ctx_delete ( fd_exec_slot_ctx_leave ( slot_ctx ) );
fd_exec_epoch_ctx_delete( fd_exec_epoch_ctx_leave( epoch_ctx ) );
fd_acc_mgr_delete( acc_mgr );
fd_scratch_pop();
fd_funk_txn_cancel( runner->funk, funk_txn, 1 );
}

static int
_diff_effects( fd_exec_instr_ctx_t * ctx,
fd_exec_test_instr_effects_t const * expected,
int exec_result ) {

if( expected->result != exec_result ) {
FD_LOG_WARNING(( "Expected result (%d-%s), got (%d-%s)",
expected->result, fd_executor_instr_strerror( expected->result ),
exec_result, fd_executor_instr_strerror( exec_result ) ));
return 0;
}

if( ( exec_result==FD_EXECUTOR_INSTR_ERR_CUSTOM_ERR ) &
( expected->custom_err != ctx->txn_ctx->custom_err ) ) {
FD_LOG_WARNING(( "Expected custom error %d, got %d",
expected->custom_err, ctx->txn_ctx->custom_err ));
return 0;
}


/* TODO detect accounts that were modified locally but not in fixture */

return 1;
}

int
fd_exec_instr_fixture_run( fd_exec_instr_test_runner_t * runner,
fd_exec_test_instr_fixture_t const * test ) {

fd_exec_instr_ctx_t ctx[1];
if( FD_UNLIKELY( !_context_create( runner, ctx, &test->input ) ) )
return 0;

fd_pubkey_t program_id[1]; memcpy( program_id, test->input.program_id, sizeof(fd_pubkey_t) );
fd_exec_instr_fn_t native_prog_fn = fd_executor_lookup_native_program( program_id );

if( FD_UNLIKELY( !native_prog_fn ) ) {
FD_LOG_WARNING(( "TODO: User deployed programs not yet supported" ));
_context_destroy( runner, ctx );
return 0;
}

int exec_result = native_prog_fn( *ctx );

int ok = _diff_effects( ctx, &test->output, exec_result );

_context_destroy( runner, ctx );
return ok;
}

ulong
fd_exec_instr_test_run( fd_exec_instr_test_runner_t * runner,
fd_exec_test_instr_context_t const * input,
fd_exec_test_instr_effects_t ** output,
void * output_buf,
ulong output_bufsz ) {

fd_exec_instr_ctx_t ctx[1];
_context_create( runner, ctx, input );

FD_LOG_WARNING(( "TODO" ));
(void)output; (void)output_buf; (void)output_bufsz;

_context_destroy( runner, ctx );
return 0UL;
}
74 changes: 74 additions & 0 deletions src/flamenco/runtime/tests/fd_exec_instr_test.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#ifndef HEADER_fd_src_flamenco_runtime_tests_fd_exec_instr_test_h
#define HEADER_fd_src_flamenco_runtime_tests_fd_exec_instr_test_h

/* fd_exec_instr_test.h provides APIs for running instruction processor
tests. */

#include "fd_exec_test.pb.h"
#include "../../../funk/fd_funk.h"

/* fd_exec_instr_test_runner_t provides fake fd_exec_instr_ctx_t to
test processing of individual instructions. */

struct fd_exec_instr_test_runner_private;
typedef struct fd_exec_instr_test_runner_private fd_exec_instr_test_runner_t;

FD_PROTOTYPES_BEGIN

/* Constructors */

ulong
fd_exec_instr_test_runner_align( void );

ulong
fd_exec_instr_test_runner_footprint( void );

/* fd_exec_instr_test_runner_new formats a memory region for use as an
instruction test runner. mem must be part of an fd_wksp. Does
additional wksp allocs. wksp_tag is the tag used for wksp allocs
managed by the runner. Returns newly created runner on success. On
failure, returns NULL and logs reason for error. */

fd_exec_instr_test_runner_t *
fd_exec_instr_test_runner_new( void * mem,
ulong wksp_tag );

/* fd_exec_instr_test_runner_delete frees wksp allocations managed by
runner and returns the memory region backing runner itself back to
the caller. */

void *
fd_exec_instr_test_runner_delete( fd_exec_instr_test_runner_t * runner );

/* User API */

/* fd_exec_instr_fixture_run executes the given instruction processing
fixture and validates that the actual result matches the expected.
Returns 1 on success. On failure, returns 0 and logs reason for
error to warning log. Uses fd_scratch. */

int
fd_exec_instr_fixture_run( fd_exec_instr_test_runner_t * runner,
fd_exec_test_instr_fixture_t const * test );

/* fd_exec_instr_test_run executes a given instruction context (input)
and returns the effects of executing that instruction to the caller.
output_buf points to a memory region of output_bufsz bytes where the
result is allocated into. On successful execution, *output points
to a newly created instruction effects object, and returns the number
of bytes allocated at output_buf. (The caller can use this to shrink
the output buffer) Note that an instruction that errored (in the
runtime) is also considered a successful execution. On failure to
execute, returns 0UL and leaves *object undefined and logs reason
for failure. Reasons for failure include insufficient output_bufsz. */

ulong
fd_exec_instr_test_run( fd_exec_instr_test_runner_t * runner,
fd_exec_test_instr_context_t const * input,
fd_exec_test_instr_effects_t ** output,
void * output_buf,
ulong output_bufsz );

FD_PROTOTYPES_END

#endif /* HEADER_fd_src_flamenco_runtime_tests_fd_exec_instr_test_h */
Loading

0 comments on commit b8ad346

Please sign in to comment.