Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
.hash = "thread_pool-0.1.0-m6B8f9j4AAAWx5c9ytLrH_NlntTMxSg049v76xVsgoSw",
},
.zig_libp2p = .{
.url = "https://github.com/blockblaz/zig-libp2p/archive/refs/tags/v0.2.87.tar.gz",
.hash = "zig_libp2p-0.2.87-lil2hEVQKgA40r0yFW6uau6U5hAPQd1q2S-0TJKASnic",
.url = "https://github.com/blockblaz/zig-libp2p/archive/refs/tags/v0.2.89.tar.gz",
.hash = "zig_libp2p-0.2.89-lil2hMltKgA7YdmHAS_eavWH0unkgsQhybBFEWqukws4",
},
},
.paths = .{""},
Expand Down
2 changes: 1 addition & 1 deletion leanSpec
Submodule leanSpec updated 469 files
2 changes: 1 addition & 1 deletion pkgs/cli/src/node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2023,7 +2023,7 @@ test "compare roots from genGensisBlock and genGenesisState and genStateBlockHea
// Verify the state root matches the expected value
const state_root_from_genesis_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{&state_root_from_genesis});
defer allocator.free(state_root_from_genesis_hex);
try std.testing.expectEqualStrings(state_root_from_genesis_hex, "0x228ecb2f88891fab88a05a104ccac95f1513e138d53469340b9ce04f70fa1019");
try std.testing.expectEqualStrings(state_root_from_genesis_hex, "0x9a77892fc5afa43bbfa1462bd9c96d239ee8b0fd863b1c3a08e69b6f7efbb8d2");
}

test "populateNodeNameRegistry" {
Expand Down
18 changes: 9 additions & 9 deletions pkgs/cli/test/fixtures/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ VALIDATOR_COUNT: 9
# List of Genesis Validators' Public Keys (attestation + proposal)
GENESIS_VALIDATORS:
- attestation_pubkey: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233"
proposal_pubkey: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233"
proposal_pubkey: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132ee"
- attestation_pubkey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334"
proposal_pubkey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334"
proposal_pubkey: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233ee"
- attestation_pubkey: "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435"
proposal_pubkey: "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435"
proposal_pubkey: "02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334ee"
- attestation_pubkey: "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536"
proposal_pubkey: "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536"
proposal_pubkey: "030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435ee"
- attestation_pubkey: "0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637"
proposal_pubkey: "0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637"
proposal_pubkey: "0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536ee"
- attestation_pubkey: "05060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"
proposal_pubkey: "05060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"
proposal_pubkey: "05060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637ee"
- attestation_pubkey: "060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839"
proposal_pubkey: "060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839"
proposal_pubkey: "060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738ee"
- attestation_pubkey: "0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a"
proposal_pubkey: "0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a"
proposal_pubkey: "0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839ee"
- attestation_pubkey: "08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b"
proposal_pubkey: "08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b"
proposal_pubkey: "08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393aee"
22 changes: 22 additions & 0 deletions pkgs/configs/src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const GenesisConfigError = error{
InvalidGenesisTime,
MissingValidatorConfig,
InvalidValidatorPubkeys,
ValidatorSigningKeysMustDiffer,
};

/// Parses genesis configuration from YAML.
Expand Down Expand Up @@ -152,6 +153,9 @@ fn parseValidatorEntriesFromYaml(
return GenesisConfigError.InvalidValidatorPubkeys;
if (prop_node != .scalar) return GenesisConfigError.InvalidValidatorPubkeys;
proposal_pubkeys[idx] = try hexToBytes52(prop_node.scalar);
if (std.mem.eql(u8, &attestation_pubkeys[idx], &proposal_pubkeys[idx])) {
return GenesisConfigError.ValidatorSigningKeysMustDiffer;
}
}

return ValidatorEntries{
Expand All @@ -175,6 +179,24 @@ fn hexToBytes52(input: []const u8) !types.Bytes52 {
return bytes;
}

test "genesisConfigFromYAML rejects reused validator signing key" {
const yaml_content =
\\GENESIS_TIME: 1
\\GENESIS_VALIDATORS:
\\ - attestation_pubkey: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233"
\\ proposal_pubkey: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233"
;

var yaml: Yaml = .{ .source = yaml_content };
defer yaml.deinit(std.testing.allocator);
try yaml.load(std.testing.allocator);

try std.testing.expectError(
GenesisConfigError.ValidatorSigningKeysMustDiffer,
genesisConfigFromYAML(std.testing.allocator, yaml, null),
);
}

// TODO: Enable and update this test once the YAML parsing for public keys PR is added
// test "load genesis config from yaml" {
// const yaml_content =
Expand Down
9 changes: 9 additions & 0 deletions pkgs/node/src/chain.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4746,6 +4746,13 @@ pub const BeamChain = struct {
});
return BlockValidationError.SlotNotAfterParent;
}
if (block.slot - parent_block.?.slot > params.HISTORICAL_ROOTS_LIMIT) {
self.logger.debug("block validation failed: slot gap {d} exceeds HISTORICAL_ROOTS_LIMIT {d}", .{
block.slot - parent_block.?.slot,
params.HISTORICAL_ROOTS_LIMIT,
});
return BlockValidationError.BlockSlotGapTooLarge;
}

// 5. Finalized-descendant check - reject forks that branch off from
// pre-finalization ancestors. This is the gossip-level DoS defense that
Expand Down Expand Up @@ -6008,6 +6015,8 @@ pub const BlockValidationError = error{
InvalidProposerIndex,
/// Block slot is not greater than parent slot
SlotNotAfterParent,
/// Block slot is too far beyond parent slot to process boundedly
BlockSlotGapTooLarge,
/// Block's parent chain does not descend from the finalized checkpoint
NotFinalizedDescendant,
};
Expand Down
88 changes: 64 additions & 24 deletions pkgs/node/src/forkchoice.zig
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,23 @@ const ProtoAttestation = struct {
attestation_data: ?types.AttestationData = null,
};

fn attestationDataRootGreater(allocator: Allocator, a: types.AttestationData, b: types.AttestationData) !bool {
var a_root: types.Root = undefined;
var b_root: types.Root = undefined;
try zeam_utils.hashTreeRoot(types.AttestationData, a, &a_root, allocator);
try zeam_utils.hashTreeRoot(types.AttestationData, b, &b_root, allocator);
return std.mem.order(u8, &a_root, &b_root) == .gt;
}

fn shouldReplaceLatestAttestation(allocator: Allocator, current: ?ProtoAttestation, candidate: ProtoAttestation) !bool {
const existing = current orelse return true;
if (candidate.slot != existing.slot) return candidate.slot > existing.slot;

const existing_data = existing.attestation_data orelse return false;
const candidate_data = candidate.attestation_data orelse return false;
return attestationDataRootGreater(allocator, candidate_data, existing_data);
}

const AttestationTracker = struct {
// prev latest attestation applied index null if not applied
appliedIndex: ?usize = null,
Expand Down Expand Up @@ -1086,13 +1103,12 @@ pub const ForkChoice = struct {
// fork's weight (head then falls to the lexicographic tie-break / deeper fork).
// accept_new_attestations unions latest_new into latest_known
// (head reads the highest-slot vote per validator), so do the same: keep the
// fresher of the two, gossip winning ties as it reflects the validator's
// current view, and never drop a known vote when latestNew is null/older.
// fresher of the two, resolving equal-slot ties by canonical attestation
// data root so arrival order cannot steer fork-choice weight.
for (0..self.config.genesis.numValidators()) |validator_id| {
var tracker = self.attestations.get(validator_id) orelse continue;
const new_vote = tracker.latestNew orelse continue;
const known_slot = (tracker.latestKnown orelse ProtoAttestation{}).slot;
if (tracker.latestKnown == null or new_vote.slot >= known_slot) {
if (try shouldReplaceLatestAttestation(self.allocator, tracker.latestKnown, new_vote)) {
tracker.latestKnown = new_vote;
try self.attestations.put(validator_id, tracker);
}
Expand Down Expand Up @@ -1440,13 +1456,13 @@ pub const ForkChoice = struct {

// Tie-break (all tiers): higher target.slot (user rule 6 for
// justify; deepest-target for build), then more voters, then newer
// att slot, then lexicographic for determinism.
// att slot, then larger canonical attestation-data root.
const replace = if (best_idx) |bi| blk: {
if (tier != best_tier) break :blk tier < best_tier;
if (ad.target.slot != best_target_slot) break :blk ad.target.slot > best_target_slot;
if (count != best_count) break :blk count > best_count;
if (ad.slot != best_att_slot) break :blk ad.slot > best_att_slot;
break :blk types.attestationDataLessThan({}, ad, candidates[bi].att_data);
break :blk try attestationDataRootGreater(allocator, ad, candidates[bi].att_data);
} else true;

if (replace) {
Expand Down Expand Up @@ -2031,13 +2047,13 @@ pub const ForkChoice = struct {
var attestation_tracker = self.attestations.get(validator_id) orelse AttestationTracker{};
// update latest known attested head of the validator if already included on chain
if (is_from_block) {
const attestation_tracker_latest_known_slot = (attestation_tracker.latestKnown orelse ProtoAttestation{}).slot;
if (attestation_slot > attestation_tracker_latest_known_slot) {
attestation_tracker.latestKnown = .{
.index = new_head_index,
.slot = attestation_slot,
.attestation_data = attestation_data,
};
const candidate = ProtoAttestation{
.index = new_head_index,
.slot = attestation_slot,
.attestation_data = attestation_data,
};
if (try shouldReplaceLatestAttestation(self.allocator, attestation_tracker.latestKnown, candidate)) {
attestation_tracker.latestKnown = candidate;

// The gossip ("new") and on-chain
// ("known") pools stay strictly separate: `update_safe_target`
Expand All @@ -2055,14 +2071,13 @@ pub const ForkChoice = struct {
if (attestation_slot > self.fcStore.slot_clock.timeSlots.load(.monotonic) + 1) {
return ForkChoiceError.InvalidFutureAttestation;
}
// just update latest new attested head of the validator
const attestation_tracker_latest_new_slot = (attestation_tracker.latestNew orelse ProtoAttestation{}).slot;
if (attestation_slot > attestation_tracker_latest_new_slot) {
attestation_tracker.latestNew = .{
.index = new_head_index,
.slot = attestation_slot,
.attestation_data = attestation_data,
};
const candidate = ProtoAttestation{
.index = new_head_index,
.slot = attestation_slot,
.attestation_data = attestation_data,
};
if (try shouldReplaceLatestAttestation(self.allocator, attestation_tracker.latestNew, candidate)) {
attestation_tracker.latestNew = candidate;
}
}
try self.attestations.put(validator_id, attestation_tracker);
Expand Down Expand Up @@ -3030,15 +3045,15 @@ pub const ForkChoice = struct {

const parent_block_or_null = self.getBlockUnlocked(parent_root);
if (parent_block_or_null) |parent_block| {
// we will use parent block later as per the finalization gadget
_ = parent_block;

// Block admission only requires a known parent and a slot above
// the finalized boundary; STF and signature verification are the
// gating layers.
if (slot < self.fcStore.latest_finalized.slot) {
return ForkChoiceError.PreFinalizedSlot;
}
if (slot > parent_block.slot and slot - parent_block.slot > params.HISTORICAL_ROOTS_LIMIT) {
return ForkChoiceError.BlockSlotGapTooLarge;
}

// Per store.process_block: a block may include at most
// MAX_ATTESTATIONS_DATA (8) distinct AttestationData entries, and
Expand Down Expand Up @@ -3595,6 +3610,8 @@ pub const ForkChoiceError = error{
InvalidSafeTargetCompute,
DuplicateAttestationData,
TooManyAttestationData,
BlockSlotGapTooLarge,
BlockTooFarInFuture,
};

/// Errors raised by the shared gossip attestation-data validator.
Expand All @@ -3621,6 +3638,29 @@ pub const GossipAttestationValidationError = error{
AttestationTooFarInFuture,
};

test "shouldReplaceLatestAttestation breaks equal-slot ties by canonical data root" {
const allocator = std.testing.allocator;
const a = types.AttestationData{
.slot = 7,
.source = .{ .root = [_]u8{1} ** 32, .slot = 3 },
.target = .{ .root = [_]u8{2} ** 32, .slot = 5 },
.head = .{ .root = [_]u8{3} ** 32, .slot = 7 },
};
var b = a;
b.head.root = [_]u8{4} ** 32;

const a_wins = try attestationDataRootGreater(allocator, a, b);
const larger = if (a_wins) a else b;
const smaller = if (a_wins) b else a;

const current = ProtoAttestation{ .index = 1, .slot = 7, .attestation_data = smaller };
const replacement = ProtoAttestation{ .index = 2, .slot = 7, .attestation_data = larger };
const stale = ProtoAttestation{ .index = 3, .slot = 7, .attestation_data = smaller };

try std.testing.expect(try shouldReplaceLatestAttestation(allocator, current, replacement));
try std.testing.expect(!try shouldReplaceLatestAttestation(allocator, replacement, stale));
}

fn setupTestPrimitives() !*ThreadPool {
return @import("./testing.zig").setupTestPrimitives(std.testing.allocator);
}
Expand Down
Loading
Loading