From ff9ded90613aa9e1dcdb608d139502a7c724facc Mon Sep 17 00:00:00 2001 From: Emily Wang Date: Tue, 17 Dec 2024 20:23:39 +0000 Subject: [PATCH] ghost: verify invariants + unit tests --- src/choreo/ghost/fd_ghost.c | 47 ++++++- src/choreo/ghost/fd_ghost.h | 4 + src/choreo/ghost/test_ghost.c | 223 ++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 4 deletions(-) diff --git a/src/choreo/ghost/fd_ghost.c b/src/choreo/ghost/fd_ghost.c index a9a5557072..4d676cdb55 100644 --- a/src/choreo/ghost/fd_ghost.c +++ b/src/choreo/ghost/fd_ghost.c @@ -140,6 +140,45 @@ fd_ghost_init( fd_ghost_t * ghost, ulong root, ulong total_stake ) { } /* clang-format on */ +bool +fd_ghost_verify(fd_ghost_t const * ghost){ + if( FD_UNLIKELY( !ghost ) ) { + FD_LOG_WARNING(( "NULL ghost" )); + return false; + } + + fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); + fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); + + /* every element that exists in pool exists in map */ + + if(!fd_ghost_node_map_verify( node_map, + fd_ghost_node_pool_used( node_pool), + node_pool )) { + return false; + } + + /* check invariant that each parent weight is >= sum of children weights */ + fd_ghost_node_t const * parent = fd_ghost_root_node( ghost ); + + while( parent ) { + ulong child_idx = parent->child_idx; + ulong total_weight = 0; + while( child_idx != fd_ghost_node_pool_idx_null( node_pool ) ) { + fd_ghost_node_t const * child = fd_ghost_node_pool_ele( node_pool, child_idx ); + total_weight += child->weight; + child_idx = child->sibling_idx; + } + if( FD_UNLIKELY( total_weight > parent->weight ) ) { + FD_LOG_WARNING(( "root %lu has total stake %lu but sum of children is %lu", parent->slot, parent->weight, total_weight )); + return false; + } + + parent = fd_ghost_node_pool_ele( node_pool, parent->next ); + } + + return true; +} fd_ghost_node_t * fd_ghost_insert( fd_ghost_t * ghost, ulong slot, ulong parent_slot ) { @@ -273,7 +312,6 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey fd_ghost_node_map_t * node_map = fd_ghost_node_map( ghost ); fd_ghost_node_t * node_pool = fd_ghost_node_pool( ghost ); fd_ghost_node_t const * root = fd_ghost_root_node( ghost ); - ulong null_idx = fd_ghost_node_pool_idx_null( node_pool ); #if FD_GHOST_USE_HANDHOLDING if( FD_UNLIKELY( slot < root->slot ) ) { @@ -350,8 +388,9 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey latest_vote->stake )); node->stake = 0; } + fd_ghost_node_t * ancestor = node; - while( ancestor->parent_idx != null_idx ) { + while( ancestor ) { int cf = __builtin_usubl_overflow( ancestor->weight, latest_vote->stake, &ancestor->weight ); @@ -389,7 +428,6 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey /* Propagate the vote stake up the ancestry, including updating the head. */ - FD_LOG_DEBUG(( "[ghost] adding (%s, %lu, %lu)", FD_BASE58_ENC_32_ALLOCA( pubkey ), stake, latest_vote->slot )); int cf = __builtin_uaddl_overflow( node->stake, latest_vote->stake, &node->stake ); if( FD_UNLIKELY( cf ) ) { @@ -398,8 +436,9 @@ fd_ghost_replay_vote( fd_ghost_t * ghost, ulong slot, fd_pubkey_t const * pubkey node->stake, latest_vote->stake )); } + fd_ghost_node_t * ancestor = node; - while( ancestor->parent_idx != null_idx ) { + while( ancestor ) { int cf = __builtin_uaddl_overflow( ancestor->weight, latest_vote->stake, &ancestor->weight ); if( FD_UNLIKELY( cf ) ) { FD_LOG_ERR(( "[%s] add overflow. ancestor->weight %lu latest_vote->stake %lu", diff --git a/src/choreo/ghost/fd_ghost.h b/src/choreo/ghost/fd_ghost.h index 7be7d91c82..c5718f7b13 100644 --- a/src/choreo/ghost/fd_ghost.h +++ b/src/choreo/ghost/fd_ghost.h @@ -31,6 +31,7 @@ not prerequisite reading for understanding this implementation. */ #include "../fd_choreo_base.h" +#include /* FD_GHOST_USE_HANDHOLDING: Define this to non-zero at compile time to turn on additional runtime checks and logging. */ @@ -246,6 +247,9 @@ fd_ghost_vote_map( fd_ghost_t const * ghost ) { /* Operations */ +bool +fd_ghost_verify( fd_ghost_t const * ghost ); + /* fd_ghost_node_insert inserts a new node with slot as the key into the ghost. Assumes slot >= ghost->smr, slot is not already in ghost, parent_slot is already in ghost, and the node pool has a free element diff --git a/src/choreo/ghost/test_ghost.c b/src/choreo/ghost/test_ghost.c index eaa3eec04d..6a92ea7f80 100644 --- a/src/choreo/ghost/test_ghost.c +++ b/src/choreo/ghost/test_ghost.c @@ -1,4 +1,5 @@ #include "fd_ghost.h" +#include #include #define INSERT( c, p ) \ @@ -110,19 +111,28 @@ test_ghost_publish_left( fd_wksp_t * wksp ) { INSERT( 5, 3 ); INSERT( 6, 5 ); + FD_TEST( fd_ghost_verify( ghost ) ); + fd_pubkey_t pk1 = { .key = { 1 } }; ulong key2 = 2; fd_ghost_replay_vote( ghost, key2, &pk1, 1 ); + fd_ghost_print( ghost ); + + FD_TEST( fd_ghost_verify( ghost ) ); + ulong key3 = 3; fd_ghost_replay_vote( ghost, key3, &pk1, 1 ); fd_ghost_node_t const * node2 = fd_ghost_query( ghost, key2 ); FD_TEST( node2 ); + FD_TEST( fd_ghost_verify( ghost ) ); fd_ghost_print( ghost ); fd_ghost_publish( ghost, key2 ); fd_ghost_node_t * root = fd_ghost_node_pool_ele( node_pool, ghost->root_idx ); FD_TEST( root->slot == 2 ); + FD_TEST( fd_ghost_verify( ghost ) ); + FD_TEST( fd_ghost_node_pool_ele( node_pool, root->child_idx )->slot == 4 ); FD_TEST( fd_ghost_node_pool_free( node_pool ) == node_max - 2 ); fd_ghost_print( ghost ); @@ -173,18 +183,22 @@ test_ghost_publish_right( fd_wksp_t * wksp ) { INSERT( 4, 2 ); INSERT( 5, 3 ); INSERT( 6, 5 ); + FD_TEST( fd_ghost_verify( ghost ) ); fd_pubkey_t pk1 = { .key = { 1 } }; ulong key2 = 2; fd_ghost_replay_vote( ghost, key2, &pk1, 1 ); + FD_TEST( fd_ghost_verify( ghost ) ); ulong key3 = 3; fd_ghost_replay_vote( ghost, key3, &pk1, 1 ); + FD_TEST( fd_ghost_verify( ghost ) ); fd_ghost_node_t const * node3 = fd_ghost_query( ghost, key3 ); FD_TEST( node3 ); fd_ghost_print( ghost ); fd_ghost_publish( ghost, key3 ); + FD_TEST( fd_ghost_verify( ghost ) ); fd_ghost_node_t * root = fd_ghost_node_pool_ele( node_pool, ghost->root_idx ); FD_TEST( root->slot == 3 ); @@ -219,6 +233,7 @@ test_ghost_gca( fd_wksp_t * wksp ) { INSERT( 4, 2 ); INSERT( 5, 3 ); INSERT( 6, 5 ); + FD_TEST( fd_ghost_verify( ghost ) ); fd_ghost_print( ghost ); @@ -296,6 +311,7 @@ test_ghost_print( fd_wksp_t * wksp ) { query = 268538761; node = query_mut( ghost, query ); node->weight = 10; + FD_TEST( fd_ghost_verify( ghost ) ); fd_ghost_slot_print( ghost, query, 8 ); fd_ghost_print( ghost ); @@ -303,6 +319,209 @@ test_ghost_print( fd_wksp_t * wksp ) { fd_wksp_free_laddr( mem ); } + +/* + slot 10 + / \ + slot 11 | + | slot 12 + slot 13 +*/ + +void +test_ghost_head( fd_wksp_t * wksp ){ + ulong node_max = 16; + ulong vote_max = 16; + void * mem = fd_wksp_alloc_laddr( wksp, + fd_ghost_align(), + fd_ghost_footprint( node_max, vote_max ), + 1UL ); + FD_TEST( mem ); + fd_ghost_t * ghost = fd_ghost_join( fd_ghost_new( mem, node_max, vote_max, 0UL ) ); + + ulong slots[node_max]; + ulong parent_slots[node_max]; + ulong i = 0; + + fd_ghost_init( ghost, 10, 150 ); + INSERT( 11, 10 ); + INSERT( 12, 10 ); + INSERT( 13, 11 ); + + fd_pubkey_t pk1 = { .key = { 1 } }; + ulong key11 = 11; + fd_ghost_replay_vote( ghost, key11, &pk1, 50 ); + FD_TEST( fd_ghost_verify( ghost ) ); + + fd_pubkey_t pk2 = { .key = { 2 } }; + ulong key12 = 12; + fd_ghost_replay_vote( ghost, key12, &pk2, 100); + FD_TEST( fd_ghost_verify( ghost ) ); + + fd_ghost_node_t const * head = fd_ghost_head( ghost ); + FD_TEST( head->slot == 12 ); + + ulong key13 = 13; + fd_ghost_replay_vote( ghost, key13, &pk1, 75); // different stake than it was inserted with... + FD_TEST( fd_ghost_verify( ghost ) ); + + fd_ghost_node_t const * head2 = fd_ghost_head( ghost ); + FD_TEST( head2->slot == 12 ); + + fd_ghost_print( ghost ); + + fd_wksp_free_laddr( mem ); +} + +void test_ghost_vote_leaves( fd_wksp_t * wksp ){ + ulong node_max = 8; + ulong vote_max = 8; + int depth = 3; + + void * mem = fd_wksp_alloc_laddr( wksp, + fd_ghost_align(), + fd_ghost_footprint( node_max, vote_max ), + 1UL ); + FD_TEST( mem ); + fd_ghost_t * ghost = fd_ghost_join( fd_ghost_new( mem, node_max, vote_max, 0UL ) ); + + fd_ghost_init( ghost, 0, 40 ); + fd_pubkey_t pk = { .key = { 0 } }; + + /* make a full binary tree */ + for( ulong i = 1; i < node_max - 1; i++){ + fd_ghost_insert( ghost, i, (i - 1) / 2 ); + } + + /* one validator changes votes along leaves */ + ulong first_leaf = (ulong) (pow(2, (depth - 1)) - 1); + for( ulong i = first_leaf; i < node_max - 1; i++){ + fd_ghost_replay_vote( ghost, i, &pk, 10); + } + + fd_ghost_print( ghost ); + + ulong path[depth]; + ulong leaf = node_max - 2; + for( int i = depth - 1; i >= 0; i--){ + path[i] = leaf; + leaf = (leaf - 1) / 2; + } + + /* check weights and stakes */ + int j = 0; + for( ulong i = 0; i < node_max - 1; i++){ + fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); + if ( i == node_max - 2) FD_TEST( node->stake == 10 ); + else FD_TEST( node->stake == 0 ); + + if ( i == path[j] ){ // if on fork + FD_TEST( node->weight == 10 ); + j++; + } else { + FD_TEST( node->weight == 0 ); + } + } + + /* have other validators vote for rest of leaves */ + for ( ulong i = first_leaf; i < node_max - 2; i++){ + fd_pubkey_t pk = { .key = { (uchar)i } }; + fd_ghost_replay_vote( ghost, i, &pk, 10); + } + + /* check weights and stakes */ + for( ulong i = 0; i < node_max - 1; i++){ + fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); + if ( i >= first_leaf){ + FD_TEST( node->stake == 10 ); + FD_TEST( node->weight == 10 ); + } else { + FD_TEST( node->stake == 0 ); + FD_TEST( node->weight > 10); + } + } + + FD_TEST( fd_ghost_verify( ghost ) ); + fd_ghost_print( ghost ); +} + +void +test_ghost_head_full_tree( fd_wksp_t * wksp ){ + ulong node_max = 16; + ulong vote_max = 16; + void * mem = fd_wksp_alloc_laddr( wksp, + fd_ghost_align(), + fd_ghost_footprint( node_max, vote_max ), + 1UL ); + FD_TEST( mem ); + fd_ghost_t * ghost = fd_ghost_join( fd_ghost_new( mem, node_max, vote_max, 0UL ) ); + + fd_ghost_init( ghost, 0, 120 ); + FD_LOG_NOTICE(( "ghost node max: %lu", fd_ghost_node_pool_max( fd_ghost_node_pool( ghost ) ) )); + + for ( ulong i = 1; i < node_max - 1; i++ ) { + fd_ghost_insert( ghost, i, (i - 1)/2); + fd_pubkey_t pk = { .key = { ( uchar )i } }; + fd_ghost_replay_vote( ghost, i, &pk, i); + } + + for ( ulong i = 0; i < node_max - 1; i++ ) { + fd_ghost_node_t const * node = fd_ghost_query( ghost, i ); + FD_TEST( node->stake == i ); + } + + FD_TEST( fd_ghost_verify( ghost ) ); + + fd_ghost_print( ghost ); + fd_ghost_node_t const * head = fd_ghost_head( ghost ); + + FD_LOG_NOTICE(( "head slot %lu", head->slot )); + + // head will always be rightmost node in this complete binary tree + + FD_TEST( head->slot == 14 ); + + // add one more node + + fd_ghost_insert( ghost, node_max - 1, (node_max - 2)/2 ); + fd_pubkey_t pk = { .key = { (uchar)(node_max - 1) } }; + fd_ghost_replay_vote( ghost, node_max - 1, &pk, node_max - 1); + + FD_TEST( fd_ghost_verify( ghost ) ); + head = fd_ghost_head( ghost ); + FD_TEST( head->slot == 14 ); + + // adding one more node would fail. +} + +void +test_rooted_vote( fd_wksp_t * wksp ){ + ulong node_max = 16; + ulong vote_max = 16; + void * mem = fd_wksp_alloc_laddr( wksp, + fd_ghost_align(), + fd_ghost_footprint( node_max, vote_max ), + 1UL ); + FD_TEST( mem ); + fd_ghost_t * ghost = fd_ghost_join( fd_ghost_new( mem, node_max, vote_max, 0UL ) ); + + fd_ghost_init( ghost, 0, 120 ); + + fd_ghost_insert( ghost, 1, 0); + fd_pubkey_t pk = { .key = { 1 } }; + fd_ghost_replay_vote( ghost, 1, &pk, 20); + + fd_pubkey_t pk2 = { .key = { 2 } }; + fd_ghost_rooted_vote( ghost, 1, &pk2, 10 ); + + fd_ghost_node_t const * node = fd_ghost_query( ghost, 1 ); + FD_TEST( node->stake == 20 ); + FD_TEST( node->weight == 20 ); + FD_TEST( node->rooted_stake == 10 ); + + fd_ghost_verify( ghost ); +} + int main( int argc, char ** argv ) { fd_boot( &argc, &argv ); @@ -326,6 +545,10 @@ main( int argc, char ** argv ) { test_ghost_publish_left( wksp ); test_ghost_publish_right( wksp ); test_ghost_gca( wksp ); + test_ghost_vote_leaves( wksp ); + test_ghost_head_full_tree( wksp ); + test_ghost_head( wksp ); + test_rooted_vote( wksp ); fd_halt(); return 0;