diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index f47d26fb..e7dca007 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -317,25 +317,24 @@ pub const BeamChain = struct { } }, .vote => |signed_vote| { - const vote = signed_vote.message; - const hasHead = self.forkChoice.hasBlock(vote.head.root); const signed_vote_json = try signed_vote.toJson(self.allocator); // Convert JSON value to string for proper logging const signed_vote_str = try jsonToString(self.allocator, signed_vote_json); defer self.allocator.free(signed_vote_str); - self.module_logger.debug("chain received vote onGossip cb at slot={any} hasHead={any}", .{ - // - signed_vote_str, - hasHead, - }); + self.module_logger.debug("chain received vote onGossip cb: {s}", .{signed_vote_str}); - if (hasHead) { - self.onAttestation(signed_vote) catch |err| { - self.module_logger.debug(" ^^^^^^^^ Attestation processing error ^^^^^^ {any}", .{err}); - }; - } + // Validate attestation before processing (gossip = not from block) + self.validateAttestation(signed_vote, false) catch |err| { + self.module_logger.debug("Gossip attestation validation failed: {any}", .{err}); + return; // Drop invalid gossip attestations + }; + + // Process validated attestation + self.onAttestation(signed_vote) catch |err| { + self.module_logger.debug("Attestation processing error: {any}", .{err}); + }; }, } } @@ -390,6 +389,13 @@ pub const BeamChain = struct { block.slot, }); for (block.body.attestations.constSlice()) |signed_vote| { + // Validate attestation before processing (from block = true) + self.validateAttestation(signed_vote, true) catch |e| { + self.module_logger.err("invalid attestation in block: validator={d} error={any}", .{ signed_vote.validator_id, e }); + // Skip invalid attestations but continue processing the block + continue; + }; + self.forkChoice.onAttestation(signed_vote, true) catch |e| { self.module_logger.err("error processing block attestation={any} e={any}", .{ signed_vote, e }); }; @@ -461,7 +467,114 @@ pub const BeamChain = struct { }); } + /// Validate incoming attestation before processing. + /// + /// is_from_block: true if attestation came from a block, false if from network gossip + /// + /// Per leanSpec: + /// - Gossip attestations (is_from_block=false): vote.slot <= current_slot (no future tolerance) + /// - Block attestations (is_from_block=true): vote.slot <= current_slot + 1 (lenient) + pub fn validateAttestation(self: *Self, signed_vote: types.SignedVote, is_from_block: bool) !void { + const vote = signed_vote.message; + + // 1. Validate that source, target, and head blocks exist in proto array + const source_idx = self.forkChoice.protoArray.indices.get(vote.source.root) orelse { + self.module_logger.debug("Attestation validation failed: unknown source block root=0x{s}", .{ + std.fmt.fmtSliceHexLower(&vote.source.root), + }); + return AttestationValidationError.UnknownSourceBlock; + }; + + const target_idx = self.forkChoice.protoArray.indices.get(vote.target.root) orelse { + self.module_logger.debug("Attestation validation failed: unknown target block root=0x{s}", .{ + std.fmt.fmtSliceHexLower(&vote.target.root), + }); + return AttestationValidationError.UnknownTargetBlock; + }; + + const head_idx = self.forkChoice.protoArray.indices.get(vote.head.root) orelse { + self.module_logger.debug("Attestation validation failed: unknown head block root=0x{s}", .{ + std.fmt.fmtSliceHexLower(&vote.head.root), + }); + return AttestationValidationError.UnknownHeadBlock; + }; + + const source_block = self.forkChoice.protoArray.nodes.items[source_idx]; + const target_block = self.forkChoice.protoArray.nodes.items[target_idx]; + const head_block = self.forkChoice.protoArray.nodes.items[head_idx]; + _ = head_block; // Will be used in future validations + + // 2. Validate slot relationships + if (source_block.slot > target_block.slot) { + self.module_logger.debug("Attestation validation failed: source slot {d} > target slot {d}", .{ + source_block.slot, + target_block.slot, + }); + return AttestationValidationError.SourceSlotExceedsTarget; + } + + // This corresponds to leanSpec's: assert vote.source.slot <= vote.target.slot + if (vote.source.slot > vote.target.slot) { + self.module_logger.debug("Attestation validation failed: source checkpoint slot {d} > target checkpoint slot {d}", .{ + vote.source.slot, + vote.target.slot, + }); + return AttestationValidationError.SourceCheckpointExceedsTarget; + } + + // 3. Validate checkpoint slots match block slots + if (source_block.slot != vote.source.slot) { + self.module_logger.debug("Attestation validation failed: source block slot {d} != source checkpoint slot {d}", .{ + source_block.slot, + vote.source.slot, + }); + return AttestationValidationError.SourceCheckpointSlotMismatch; + } + + // This corresponds to leanSpec's: assert target_block.slot == vote.target.slot + if (target_block.slot != vote.target.slot) { + self.module_logger.debug("Attestation validation failed: target block slot {d} != target checkpoint slot {d}", .{ + target_block.slot, + vote.target.slot, + }); + return AttestationValidationError.TargetCheckpointSlotMismatch; + } + + // 4. Validate attestation is not too far in the future + // Per leanSpec store.py: + // - General validation (line 140): assert vote.slot <= Slot(current_slot + Slot(1)) + // - Gossip processing (line 177): assert vote.slot <= time_slots + // + // Gossip attestations must be for current or past slots only. Validators vote + // in interval 1 of the current slot, so they cannot vote for future slots. + // Block attestations can be more lenient since the block itself was validated. + const current_slot = self.forkChoice.fcStore.timeSlots; + const max_allowed_slot = if (is_from_block) + current_slot + constants.MAX_FUTURE_SLOT_TOLERANCE // Block attestations: allow +1 + else + current_slot; // Gossip attestations: no future slots allowed + + if (vote.slot > max_allowed_slot) { + self.module_logger.debug("Attestation validation failed: vote slot {d} > max allowed slot {d} (is_from_block={any})", .{ + vote.slot, + max_allowed_slot, + is_from_block, + }); + return AttestationValidationError.AttestationTooFarInFuture; + } + + self.module_logger.debug("Attestation validation passed: validator={d} slot={d} source={d} target={d} is_from_block={any}", .{ + signed_vote.validator_id, + vote.slot, + vote.source.slot, + vote.target.slot, + is_from_block, + }); + } + pub fn onAttestation(self: *Self, signedVote: types.SignedVote) !void { + // Validate attestation before processing (gossip = not from block) + try self.validateAttestation(signedVote, false); return self.forkChoice.onAttestation(signedVote, false); } @@ -480,6 +593,16 @@ pub const BeamChain = struct { const BlockProcessingError = error{MissingPreState}; const BlockProductionError = error{ NotImplemented, MissingPreState }; +const AttestationValidationError = error{ + UnknownSourceBlock, + UnknownTargetBlock, + UnknownHeadBlock, + SourceSlotExceedsTarget, + SourceCheckpointExceedsTarget, + SourceCheckpointSlotMismatch, + TargetCheckpointSlotMismatch, + AttestationTooFarInFuture, +}; test "process and add mock blocks into a node's chain" { var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); @@ -620,3 +743,439 @@ test "printSlot output demonstration" { try std.testing.expect(beam_chain.forkChoice.protoArray.nodes.items.len == mock_chain.blocks.len); try std.testing.expect(beam_chain.registered_validator_ids.len == 3); } + +// Attestation Validation Tests +// These tests align with leanSpec's test_attestation_processing.py + +test "attestation validation - comprehensive" { + // Comprehensive test covering all attestation validation rules + // This consolidates multiple validation checks into one test to avoid redundant setup + var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_allocator.deinit(); + const allocator = arena_allocator.allocator(); + + const chain_spec = + \\{"preset": "mainnet", "name": "beamdev", "genesis_time": 1234, "num_validators": 4} + ; + const options = json.ParseOptions{ + .ignore_unknown_fields = true, + .allocate = .alloc_if_needed, + }; + const parsed_chain_spec = (try json.parseFromSlice(configs.ChainOptions, allocator, chain_spec, options)).value; + const chain_config = try configs.ChainConfig.init(configs.Chain.custom, parsed_chain_spec); + + const mock_chain = try stf.genMockChain(allocator, 3, chain_config.genesis); + var beam_state = mock_chain.genesis_state; + var zeam_logger_config = zeam_utils.getTestLoggerConfig(); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + const data_dir = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(data_dir); + + var db = try database.Db.open(allocator, zeam_logger_config.logger(.database_test), data_dir); + defer db.deinit(); + + const connected_peers = try allocator.create(std.StringHashMap(PeerInfo)); + connected_peers.* = std.StringHashMap(PeerInfo).init(allocator); + + var beam_chain = try BeamChain.init(allocator, ChainOpts{ .config = chain_config, .anchorState = &beam_state, .nodeId = 0, .logger_config = &zeam_logger_config, .db = db }, connected_peers); + defer beam_chain.deinit(); + + // Add blocks to chain (slots 1 and 2) + for (1..mock_chain.blocks.len) |i| { + const block = mock_chain.blocks[i]; + try beam_chain.forkChoice.onInterval(block.message.slot * constants.INTERVALS_PER_SLOT, false); + try beam_chain.onBlock(block, .{}); + } + + // Test 1: Valid attestation (baseline - should pass) + { + const source_slot: types.Slot = 1; + const target_slot: types.Slot = 2; + const valid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = target_slot, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[target_slot], + .slot = target_slot, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[source_slot], + .slot = source_slot, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[target_slot], + .slot = target_slot, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + // Should pass validation + try beam_chain.validateAttestation(valid_vote, false); + } + + // Test 2: Unknown source block + { + const unknown_root = [_]u8{0xFF} ** 32; + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = unknown_root, // Unknown block + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.UnknownSourceBlock, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 3: Unknown target block + { + const unknown_root = [_]u8{0xEE} ** 32; + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = unknown_root, // Unknown block + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.UnknownTargetBlock, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 4: Unknown head block + { + const unknown_root = [_]u8{0xDD} ** 32; + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = unknown_root, // Unknown block + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.UnknownHeadBlock, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 5: Source slot exceeds target slot (block slots) + { + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], // Slot 2 block + .slot = 2, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], // Slot 1 block + .slot = 1, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.SourceSlotExceedsTarget, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 6: Source checkpoint slot exceeds target checkpoint slot + { + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], // Block at slot 1 (passes block slot check) + .slot = 2, // Checkpoint claims slot 2 + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], // Block at slot 2 (passes block slot check) + .slot = 1, // Checkpoint claims slot 1 (2 > 1 fails checkpoint check) + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.SourceCheckpointExceedsTarget, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 7: Source checkpoint slot mismatch + { + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], // Block at slot 1 + .slot = 0, // Checkpoint claims slot 0 (mismatch) + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.SourceCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 8: Target checkpoint slot mismatch + { + const invalid_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], // Block at slot 2 + .slot = 1, // Checkpoint claims slot 1 (mismatch) + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.TargetCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_vote, false)); + } + + // Test 9: Attestation too far in future (for gossip) + { + const future_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 100, // Way too far in the future + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(future_vote, false)); + } +} + +test "attestation validation - gossip vs block future slot handling" { + // Test that gossip and block attestations have different future slot tolerances + // Gossip: must be <= current_slot + // Block: can be <= current_slot + 1 + var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_allocator.deinit(); + const allocator = arena_allocator.allocator(); + + const chain_spec = + \\{"preset": "mainnet", "name": "beamdev", "genesis_time": 1234, "num_validators": 4} + ; + const options = json.ParseOptions{ + .ignore_unknown_fields = true, + .allocate = .alloc_if_needed, + }; + const parsed_chain_spec = (try json.parseFromSlice(configs.ChainOptions, allocator, chain_spec, options)).value; + const chain_config = try configs.ChainConfig.init(configs.Chain.custom, parsed_chain_spec); + + const mock_chain = try stf.genMockChain(allocator, 2, chain_config.genesis); + var beam_state = mock_chain.genesis_state; + var zeam_logger_config = zeam_utils.getTestLoggerConfig(); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + const data_dir = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(data_dir); + + var db = try database.Db.open(allocator, zeam_logger_config.logger(.database_test), data_dir); + defer db.deinit(); + + const connected_peers = try allocator.create(std.StringHashMap(PeerInfo)); + connected_peers.* = std.StringHashMap(PeerInfo).init(allocator); + + var beam_chain = try BeamChain.init(allocator, ChainOpts{ .config = chain_config, .anchorState = &beam_state, .nodeId = 0, .logger_config = &zeam_logger_config, .db = db }, connected_peers); + defer beam_chain.deinit(); + + // Add one block (slot 1) + const block = mock_chain.blocks[1]; + try beam_chain.forkChoice.onInterval(block.message.slot * constants.INTERVALS_PER_SLOT, false); + try beam_chain.onBlock(block, .{}); + + // Current time is at slot 1, create attestation for slot 2 (next slot) + const next_slot_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 2, // Next slot + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[0], + .slot = 0, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + + // Gossip attestations: should FAIL for next slot (current + 1) + // Per spec store.py:177: assert vote.slot <= time_slots + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(next_slot_vote, false)); + + // Block attestations: should PASS for next slot (current + 1) + // Per spec store.py:140: assert vote.slot <= Slot(current_slot + Slot(1)) + try beam_chain.validateAttestation(next_slot_vote, true); + + // Both should still reject attestations > current_slot + 1 + const too_far_vote = types.SignedVote{ + .validator_id = 0, + .message = types.Mini3SFVote{ + .slot = 3, // Too far in future + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[0], + .slot = 0, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + + // Both should fail for slot 3 when current is slot 1 + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(too_far_vote, false)); + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(too_far_vote, true)); +} + +test "attestation processing - valid block attestation" { + // Test that valid attestations from blocks are processed correctly + var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_allocator.deinit(); + const allocator = arena_allocator.allocator(); + + const chain_spec = + \\{"preset": "mainnet", "name": "beamdev", "genesis_time": 1234, "num_validators": 4} + ; + const options = json.ParseOptions{ + .ignore_unknown_fields = true, + .allocate = .alloc_if_needed, + }; + const parsed_chain_spec = (try json.parseFromSlice(configs.ChainOptions, allocator, chain_spec, options)).value; + const chain_config = try configs.ChainConfig.init(configs.Chain.custom, parsed_chain_spec); + + const mock_chain = try stf.genMockChain(allocator, 3, chain_config.genesis); + var beam_state = mock_chain.genesis_state; + var zeam_logger_config = zeam_utils.getTestLoggerConfig(); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + const data_dir = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(data_dir); + + var db = try database.Db.open(allocator, zeam_logger_config.logger(.database_test), data_dir); + defer db.deinit(); + + const connected_peers = try allocator.create(std.StringHashMap(PeerInfo)); + connected_peers.* = std.StringHashMap(PeerInfo).init(allocator); + + var beam_chain = try BeamChain.init(allocator, ChainOpts{ .config = chain_config, .anchorState = &beam_state, .nodeId = 0, .logger_config = &zeam_logger_config, .db = db }, connected_peers); + defer beam_chain.deinit(); + + // Add blocks to chain + for (1..mock_chain.blocks.len) |i| { + const block = mock_chain.blocks[i]; + try beam_chain.forkChoice.onInterval(block.message.slot * constants.INTERVALS_PER_SLOT, false); + try beam_chain.onBlock(block, .{}); + } + + // Create a valid attestation + const valid_vote = types.SignedVote{ + .validator_id = 1, + .message = types.Mini3SFVote{ + .slot = 2, + .head = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Mini3SFCheckpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + }, + .signature = [_]u8{0} ** types.SIGSIZE, + }; + + // Process attestation through chain (this validates and then processes) + try beam_chain.onAttestation(valid_vote); + + // Verify the vote was recorded in latestKnown + const vote_tracker = beam_chain.forkChoice.votes.get(1); + try std.testing.expect(vote_tracker != null); + try std.testing.expect(vote_tracker.?.latestNew != null); + try std.testing.expect(vote_tracker.?.latestNew.?.slot == 2); +} diff --git a/pkgs/node/src/constants.zig b/pkgs/node/src/constants.zig index f8f3e5e1..9345cf4e 100644 --- a/pkgs/node/src/constants.zig +++ b/pkgs/node/src/constants.zig @@ -4,3 +4,7 @@ const params = @import("@zeam/params"); // a constant fixed only relevant to node operation and hence not in the config or preset pub const INTERVALS_PER_SLOT = 4; pub const SECONDS_PER_INTERVAL_MS: isize = @divFloor(params.SECONDS_PER_SLOT * std.time.ms_per_s, INTERVALS_PER_SLOT); + +// Maximum number of slots in the future that an attestation is allowed to reference +// This prevents accepting attestations that are too far ahead of the current slot +pub const MAX_FUTURE_SLOT_TOLERANCE = 1; diff --git a/pkgs/node/src/forkchoice.zig b/pkgs/node/src/forkchoice.zig index 5494610c..1744ab81 100644 --- a/pkgs/node/src/forkchoice.zig +++ b/pkgs/node/src/forkchoice.zig @@ -485,9 +485,13 @@ pub const ForkChoice = struct { } pub fn onAttestation(self: *Self, signed_vote: types.SignedVote, is_from_block: bool) !void { + // Attestation validation is done by the caller (chain layer) + // This function assumes the attestation has already been validated + // vote has to be of an ancestor of the current slot const validator_id = signed_vote.validator_id; const vote = signed_vote.message; + // This get should never fail after validation, but we keep the check for safety const new_head_index = self.protoArray.indices.get(vote.head.root) orelse return ForkChoiceError.InvalidAttestation; var vote_tracker = self.votes.get(validator_id) orelse VoteTracker{}; @@ -583,11 +587,21 @@ pub const ForkChoice = struct { } }; -const ForkChoiceError = error{ NotImplemented, UnknownParent, FutureSlot, InvalidFutureAttestation, - // - InvalidOnChainAttestation, PreFinalizedSlot, NotFinalizedDesendant, InvalidAttestation, InvalidDeltas, - // - InvalidJustifiedRoot, InvalidBestDescendant, InvalidHeadIndex, InvalidTargetSearch }; +const ForkChoiceError = error{ + NotImplemented, + UnknownParent, + FutureSlot, + InvalidFutureAttestation, + InvalidOnChainAttestation, + PreFinalizedSlot, + NotFinalizedDesendant, + InvalidAttestation, + InvalidDeltas, + InvalidJustifiedRoot, + InvalidBestDescendant, + InvalidHeadIndex, + InvalidTargetSearch, +}; test "forkchoice block tree" { var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);