Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions src/state_transition/block/process_consolidation_request.zig
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,290 @@ fn isValidSwitchToCompoundRequest(

return true;
}

// ─── Tests ──────────────────────────────────────────────────────────────────

const testing = std.testing;
const Node = @import("persistent_merkle_tree").Node;
const TestCachedBeaconState = @import("../test_utils/generate_state.zig").TestCachedBeaconState;

fn makeConsolidationRequest(
source_pubkey: [48]u8,
target_pubkey: [48]u8,
source_address: [20]u8,
) ConsolidationRequest {
return ConsolidationRequest{
.source_address = source_address,
.source_pubkey = source_pubkey,
.target_pubkey = target_pubkey,
};
}

fn getValidatorPubkey(state: anytype, index: u64) ![48]u8 {
var validators = try state.validators();
var validator = try validators.get(index);
var pubkey_view = try validator.get("pubkey");
var pubkey: [48]u8 = undefined;
_ = try pubkey_view.getAllInto(&pubkey);
return pubkey;
}

fn setExecutionCredentials(state: anytype, index: u64, address: [20]u8) !void {
var validators = try state.validators();
var validator = try validators.get(index);
var wc: [32]u8 = [_]u8{0} ** 32;
wc[0] = 1; // ETH1_ADDRESS_WITHDRAWAL_PREFIX
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid magic numbers and improve code clarity, please use the named constant ETH1_ADDRESS_WITHDRAWAL_PREFIX from constants.zig instead of the literal 1.

    wc[0] = @import("constants").ETH1_ADDRESS_WITHDRAWAL_PREFIX;

@memcpy(wc[12..32], &address);
try validator.setValue("withdrawal_credentials", &wc);
}

fn setCompoundingCredentials(state: anytype, index: u64) !void {
var validators = try state.validators();
var validator = try validators.get(index);
var wc: [32]u8 = [_]u8{0} ** 32;
wc[0] = 2; // COMPOUNDING_WITHDRAWAL_PREFIX
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid magic numbers and improve code clarity, please use the named constant COMPOUNDING_WITHDRAWAL_PREFIX from constants.zig instead of the literal 2.

    wc[0] = @import("constants").COMPOUNDING_WITHDRAWAL_PREFIX;

try validator.setValue("withdrawal_credentials", &wc);
}

test "consolidation request - unknown source pubkey" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
Comment on lines +207 to +214
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a lot of boilerplate for test setup repeated across all 9 tests. To improve maintainability and reduce code duplication, consider extracting this logic into a TestContext struct with init and deinit methods.

For example:

const TestContext = struct {
    allocator: std.mem.Allocator,
    pool: Node.Pool,
    test_state: TestCachedBeaconState,
    state: *BeaconState(.electra),

    fn init(comptime validator_count: usize) !TestContext {
        const allocator = testing.allocator;
        var pool = try Node.Pool.init(allocator, validator_count * 5);
        errdefer pool.deinit();

        var test_state = try TestCachedBeaconState.init(allocator, &pool, validator_count);
        errdefer test_state.deinit();

        return .{
            .allocator = allocator,
            .pool = pool,
            .test_state = test_state,
            .state = test_state.cached_state.state.castToFork(.electra),
        };
    }

    fn deinit(self: *TestContext) void {
        self.test_state.deinit();
        self.pool.deinit();
    }
};

Then each test could be simplified to:

test "consolidation request - unknown source pubkey" {
    var ctx = try TestContext.init(256);
    defer ctx.deinit();

    const target_pubkey = try getValidatorPubkey(ctx.state, 1);
    // ... rest of the test
}

const target_pubkey = try getValidatorPubkey(state, 1);
const unknown_pubkey: [48]u8 = [_]u8{0xFF} ** 48;
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

const request = makeConsolidationRequest(unknown_pubkey, target_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line exceeds the 100-column limit specified in the style guide. Please wrap the arguments to processConsolidationRequest to adhere to the line length limit. This applies to other similar calls in the tests as well.

    try processConsolidationRequest(
        .electra,
        test_state.config,
        test_state.cached_state.epoch_cache,
        state,
        &request,
    );
References
  1. Hard limit all line lengths, without exception, to at most 100 columns for a good typographic "measure". (link)


// No-op: pending consolidations should remain empty
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the style guide (line 304), comments that are sentences should start with a capital letter and end with a period. Please update this comment and others in this file to follow this rule.

    // No-op: Pending consolidations should remain empty.
References
  1. Comments are sentences, with a space after the slash, with a capital letter and a full stop, or a colon if they relate to something that follows. Comments after the end of a line can be phrases, with no punctuation. (link)

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - unknown target pubkey" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const unknown_pubkey: [48]u8 = [_]u8{0xFF} ** 48;
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

try setExecutionCredentials(state, 0, source_address);

const request = makeConsolidationRequest(source_pubkey, unknown_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - valid switch to compounding" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

// Set ETH1 credentials with matching address (source == target triggers switch-to-compounding)
try setExecutionCredentials(state, 0, source_address);

const request = makeConsolidationRequest(source_pubkey, source_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

// Should have switched to compounding credentials (0x02 prefix)
var validators = try state.validators();
var validator = try validators.get(0);
const wc = try validator.getFieldRoot("withdrawal_credentials");
try testing.expectEqual(@as(u8, 2), wc[0]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid magic numbers and improve code clarity, please use the named constant COMPOUNDING_WITHDRAWAL_PREFIX from constants.zig for the expected value.

    try testing.expectEqual(@as(u8, @import("constants").COMPOUNDING_WITHDRAWAL_PREFIX), wc[0]);


// No pending consolidation added (early return after switch)
var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - source equals target without eth1 credentials" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

// Default credentials are BLS (0x00) — isValidSwitchToCompoundRequest will fail,
// then source_index == target_index check causes return
const request = makeConsolidationRequest(source_pubkey, source_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

// No-op
var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - incorrect source address" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const target_pubkey = try getValidatorPubkey(state, 1);
const correct_address: [20]u8 = [_]u8{0xAA} ** 20;
const wrong_address: [20]u8 = [_]u8{0xBB} ** 20;

try setExecutionCredentials(state, 0, correct_address);
try setCompoundingCredentials(state, 1);

// Request uses wrong_address
const request = makeConsolidationRequest(source_pubkey, target_pubkey, wrong_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - target without compounding credentials" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const target_pubkey = try getValidatorPubkey(state, 1);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

try setExecutionCredentials(state, 0, source_address);
// Target has ETH1 (0x01) instead of compounding (0x02)
try setExecutionCredentials(state, 1, [_]u8{0xBB} ** 20);

const request = makeConsolidationRequest(source_pubkey, target_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - source already exited" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const target_pubkey = try getValidatorPubkey(state, 1);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

try setExecutionCredentials(state, 0, source_address);
try setCompoundingCredentials(state, 1);

// Set source exit_epoch to non-FAR_FUTURE
var validators = try state.validators();
var source_validator = try validators.get(0);
const current_epoch = test_state.cached_state.epoch_cache.epoch;
try source_validator.set("exit_epoch", current_epoch + 100);

const request = makeConsolidationRequest(source_pubkey, target_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - source not active long enough" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const target_pubkey = try getValidatorPubkey(state, 1);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

try setExecutionCredentials(state, 0, source_address);
try setCompoundingCredentials(state, 1);

// Set source activation_epoch to current_epoch so SHARD_COMMITTEE_PERIOD not met
var validators = try state.validators();
var source_validator = try validators.get(0);
const current_epoch = test_state.cached_state.epoch_cache.epoch;
try source_validator.set("activation_epoch", current_epoch);

const request = makeConsolidationRequest(source_pubkey, target_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 0), try pending.length());
}

test "consolidation request - valid consolidation" {
const allocator = testing.allocator;
var pool = try Node.Pool.init(allocator, 256 * 5);
defer pool.deinit();

var test_state = try TestCachedBeaconState.init(allocator, &pool, 256);
defer test_state.deinit();

var state = test_state.cached_state.state.castToFork(.electra);
const source_pubkey = try getValidatorPubkey(state, 0);
const target_pubkey = try getValidatorPubkey(state, 1);
const source_address: [20]u8 = [_]u8{0xAA} ** 20;

// Source: execution credentials with matching address
try setExecutionCredentials(state, 0, source_address);
// Target: compounding credentials
try setCompoundingCredentials(state, 1);

// Override total_active_balance_increments so consolidation churn limit > MIN_ACTIVATION_BALANCE.
// With mainnet preset and only 256 validators, the churn limit is 0 (not enough stake).
// In production this requires ~500k+ validators; for tests we fake the balance.
test_state.cached_state.epoch_cache.total_active_balance_increments = 20_000_000;

const request = makeConsolidationRequest(source_pubkey, target_pubkey, source_address);
try processConsolidationRequest(.electra, test_state.config, test_state.cached_state.epoch_cache, state, &request);

// Source should have exit_epoch set (no longer FAR_FUTURE)
var validators = try state.validators();
var source_validator = try validators.get(0);
const exit_epoch = try source_validator.get("exit_epoch");
try testing.expect(exit_epoch != FAR_FUTURE_EPOCH);

// Pending consolidation should be added
var pending = try state.pendingConsolidations();
try testing.expectEqual(@as(u64, 1), try pending.length());

// Verify the pending consolidation has correct source/target
var consolidation = try pending.get(0);
try testing.expectEqual(@as(u64, 0), try consolidation.get("source_index"));
try testing.expectEqual(@as(u64, 1), try consolidation.get("target_index"));
}
Loading