-
Notifications
You must be signed in to change notification settings - Fork 1
fix: unit tests for process_epoch #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: unit tests for process_epoch #28
Conversation
Performance Report✔️ no performance regression detected Full benchmark results
|
…te/fix_process_epoch
src/state_transition/epoch/process_participation_flag_updates.zig
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some general thoughts on the state of the repo:
un-OOP the repo?
I notice in many places we have getters and setters into everything; I think this can be simplified a lot more. For example, when operating on pending deposits, we have these following functions:
getPendingDeposit(...) *const PendingDeposit { ... }
getPendingDeposits(...) []PendingDeposit { ... }
getPendingDepositCount(...) usize { ... }
setPendingDeposit(...) void { ... }
appendPendingDeposit(...) !void { ... }
setPendingDeposits(...) void { ... }
When really, this could just be all done with a single function:
pub fn pendingDeposits() *std.ArrayListUnmanaged(PendingDeposit) { ... }
Then, to replicate the above behaviour (in order):
_ = state.pendingDeposits().items[i];
_ = state.pendingDeposits().items;
_ = state.pendingDeposits().items.len;
state.pendingDeposits().items[i] = new_pending_deposit;
try state.pendingDeposits().append(new_pending_deposit);
state.pendingDeposits().items = new_pending_deposits.items;
Arguing that this is 'idiomatic zig' is lazy; instead I'll argue that: (1) it is cleaner that we have only one way to access a certain field in state
, and then manipulating it through stdlib methods; and (2) reduce the amount of boilerplate code we already have due to types and tagged unions.
Another related point is that I noticed in some functions, the getter function is doing extra work other than just simply returning; I think this inconsistency might be harmful.
types - do they belong here?
I think it's odd that consensus types are within ssz-z
- though i suppose this should be raised in that repo instead. It almost feels like ssz-z
should just contain the primitives and not have knowledge of the consensus types that consume it.
testing
Testing can be simplified in this PR, as mentioned in my comment
|
||
/// Utility method to return EpochShuffling so that consumers don't have to deal with ".get()" call | ||
/// Consumers borrow value, so they must not either modify or deinit it. | ||
pub fn getPreviousShuffling(self: *const EpochCache) *const EpochShuffling { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I almost feel that we have too many of such getters/setters, and that we should probably just do self.previous_shuffling.get()
(for example) at the callsite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not a regular getter like in BeaconState
consumer does not need to know about ReferenceCount
here
will leave a TODO for it to revisit again to avoid redoing it multiple times
pub fn addPubkey(self: *EpochCache, allocator: Allocator, index: ValidatorIndex, pubkey: Publickey) !void { | ||
try self.pubkey_to_index.set(pubkey[0..], index); | ||
// this is deinit() by application | ||
const pk_ptr = try allocator.create(BLSPubkey); | ||
pk_ptr.* = try BLSPubkey.fromBytes(&pubkey); | ||
self.index_to_pubkey.items[index] = pk_ptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Consider instead a doc comment that tells the consumer where the allocation happens, since this function does two things (though I suppose we can come back for doc comments at a later time)
pub fn addPubkey(self: *EpochCache, allocator: Allocator, index: ValidatorIndex, pubkey: Publickey) !void { | |
try self.pubkey_to_index.set(pubkey[0..], index); | |
// this is deinit() by application | |
const pk_ptr = try allocator.create(BLSPubkey); | |
pk_ptr.* = try BLSPubkey.fromBytes(&pubkey); | |
self.index_to_pubkey.items[index] = pk_ptr; | |
/// Sets `index` at `PublicKey` within the index to pubkey map and allocates and puts a new `PublicKey` at `index` within the set of validators. | |
pub fn addPubkey(self: *EpochCache, allocator: Allocator, index: ValidatorIndex, pubkey: Publickey) !void { | |
try self.pubkey_to_index.set(pubkey[0..], index); | |
const pk_ptr = try allocator.create(BLSPubkey); | |
pk_ptr.* = try BLSPubkey.fromBytes(&pubkey); | |
self.index_to_pubkey.items[index] = pk_ptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added the comment
self.previous_shuffling.get() | ||
else if (epoch == self.epoch) self.current_shuffling.get() else if (epoch == self.next_epoch) | ||
self.next_shuffling.get() | ||
self.getPreviousShuffling() | ||
else if (epoch == self.epoch) self.getCurrentShuffling() else if (epoch == self.next_epoch) | ||
self.getNextEpochShuffling() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually prefer the deleted lines because IMO we don't need an extra function for the same effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a TODO for it to resolve later
const ssz = @import("consensus_types"); | ||
const PublicKey = blst.PublicKey; | ||
const PubkeyIndexMap = @import("../utils/pubkey_index_map.zig").PubkeyIndexMap; | ||
const ValidatorIndex = ssz.primitive.ValidatorIndex.Type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: We currently use the same ValidatorIndex
across a few different files. We should probably consolidate types in a later PR - this is not important atm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opened #35
@memset(rewards, 0); | ||
@memset(penalties, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we're setting them to 0 anyway, should we not just pass them in as such to begin with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure I understand this, these are output params that will set later
do you mean do not set it and only override in the for loop below?
switch (self.*) { | ||
.phase0 => @panic("inactivity_scores is not available in phase0"), | ||
else => |state| state.inactivity_scores.append(score), | ||
inline .altair, .bellatrix, .capella, .deneb, .electra => |state| try state.inactivity_scores.append(allocator, score), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline .altair, .bellatrix, .capella, .deneb, .electra => |state| try state.inactivity_scores.append(allocator, score), | |
inline else => |state| try state.inactivity_scores.append(allocator, score), |
We can also do inline else (unless it's a conscious decision to explicitly name all the forks)
if (start_index >= state.pending_consolidations.items.len) return error.IndexOutOfBounds; | ||
const new_array = try std.ArrayListUnmanaged(ssz.electra.PendingConsolidation).initCapacity(state.pending_consolidations.items.len - start_index); | ||
try new_array.appendSlice(state.pending_consolidations.items[start_index..]); | ||
var new_array = try std.ArrayListUnmanaged(ssz.electra.PendingConsolidation.Type).initCapacity(allocator, state.pending_consolidations.items.len - start_index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var new_array = try std.ArrayListUnmanaged(ssz.electra.PendingConsolidation.Type).initCapacity(allocator, state.pending_consolidations.items.len - start_index); | |
var new_array = try std.ArrayListUnmanaged(PendingConsolidation).initCapacity(allocator, state.pending_consolidations.items.len - start_index); |
Use already imported PendingConsolidation
.
} | ||
|
||
pub fn setPendingConsolidations(self: *BeaconStateAllForks, consolidations: std.ArrayListUnmanaged(ssz.electra.PendingConsolidation)) void { | ||
pub fn setPendingConsolidations(self: *BeaconStateAllForks, consolidations: std.ArrayListUnmanaged(ssz.electra.PendingConsolidation.Type)) void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub fn setPendingConsolidations(self: *BeaconStateAllForks, consolidations: std.ArrayListUnmanaged(ssz.electra.PendingConsolidation.Type)) void { | |
pub fn setPendingConsolidations(self: *BeaconStateAllForks, consolidations: std.ArrayListUnmanaged(PendingConsolidation)) void { |
pub fn set(self: *@This(), key: []const u8, value: Val) !void { | ||
if (key.len != PUBKEY_INDEX_MAP_KEY_SIZE) { | ||
return error.InvalidKeyLen; | ||
} | ||
var fixed_key: Key = undefined; | ||
@memcpy(&fixed_key, key); | ||
try self.map.put(fixed_key, value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems to me that, given that we know keys are of a fixed length PUBKEY_INDEX_MAP_KEY_SIZE
, we can make this take a Key
instead of []const u8
:
pub fn set(self: *@This(), key: []const u8, value: Val) !void { | |
if (key.len != PUBKEY_INDEX_MAP_KEY_SIZE) { | |
return error.InvalidKeyLen; | |
} | |
var fixed_key: Key = undefined; | |
@memcpy(&fixed_key, key); | |
try self.map.put(fixed_key, value); | |
pub fn set(self: *@This(), key: Key, value: Val) !void { | |
try self.map.put(key, value); | |
} |
Which has 2 benefits:
- puts the burden of ensuring that
key
is of the right length on the consumer, getting rid of the if statements here - makes the memcpys here explicit (in the sense that the burden of copying is on the conumer side)
The same suggestions apply for the rest of the method here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
created an issue for it #34
test/int/epoch/process_epoch.zig
Outdated
const state_transition = @import("state_transition"); | ||
const ReusedEpochTransitionCache = state_transition.ReusedEpochTransitionCache; | ||
const EpochTransitionCache = state_transition.EpochTransitionCache; | ||
const testProcessEpoch = @import("./process_epoch_fn.zig").getTestProcessFn(state_transition.processEpoch, false, false, false).testProcessEpochFn; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't particularly like this approach for tests because of a few reasons:
- boolean blindness, and
- 'hiding' the parameters to the function via an import
- splitting many small test functions in separate files rather than one file
tackling boolean blindness and params being hidden
To tackle 1, we can have a ProcessEpochTestOpt
struct that looks roughly like this:
pub const ProcessEpochTestOpt = struct {
no_alloc: bool = false,
no_err_return: bool = false,
no_void_return: bool = false,
};
Then, we can do:
const testProcessEpoch = @import("./process_epoch_fn.zig").getTestProcessFn;
test "processEth1DataReset - sanity" {
try testProcessEpoch(state_transition.processEth1DataReset, .{
.no_alloc = true,
.no_err_return = true,
// leave no_void_return empty; defaults to false
});
}
with this pattern we also avoid directly importing and using a function with empty params. IMO, it's better to be explicit than implicit for such small tests.
The 3rd point might be more contentious. I do see the benefit of splitting tests by the function name (it's easy to tell from fuzzyfinder/file explorer that a test for a function is missing if the corresponding test/int/epoch/my_fn_name.zig
does not exist), but it's also nice to have all the tests in a single file. I'm undecided on this 3rd point.
Feel free to copy the types here. I want to keep the types in the ssz repo for testing purposes for now (they exercise many types that aren't tested by the generic tests -- and its hard to manually build up a test suite with the same level of coverage), but we don't have to use them here. |
definitely agree with the points you raised |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some more testing comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other files are named process_**.zig
(which fits the spec as well), consider renaming this to something other than process...
for accessibility (perhaps test_runner.zig
)?
test/int/epoch/process_epoch_fn.zig
Outdated
const ReusedEpochTransitionCache = state_transition.ReusedEpochTransitionCache; | ||
const EpochTransitionCache = state_transition.EpochTransitionCache; | ||
|
||
pub fn getTestProcessFn(process_epoch_fn: anytype, no_alloc: bool, no_err_return: bool, no_void_return: bool) type { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub fn getTestProcessFn(process_epoch_fn: anytype, no_alloc: bool, no_err_return: bool, no_void_return: bool) type { | |
const TestOpt = struct { | |
alloc: bool = false, | |
err_return: bool = false, | |
void_return: bool = false, | |
}; | |
pub fn TestRunner(process_epoch_fn: anytype, opt: TestOpt) type { |
Two things here:
-
IMO, let's not overload the terminology
get
- this is doing a bunch of logic to decide how and what to run. Also nit: PascalCase forTestRunner
since it's a type. -
Consider not using the negative terms
no_alloc
,no_void_return
,no_err_return
and instead usealloc
,void_return
,err_return
. These are already boolean values - we can avoid double negatives here
opened an issue for it at #33 |
created an issue for it here #35 |
Description
unit tests all functions in process_epoch plus:
append*
instead ofadd*