Skip to content

Conversation

@GrapeBaBa
Copy link
Member

@GrapeBaBa GrapeBaBa commented Dec 22, 2025

Fix #432

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the aggregated signature specification, addressing issue #432. The changes refactor the attestation system from individual validator attestations to aggregated attestations with bitlists, aligning with the consensus specification pattern where multiple validators can attest to the same data efficiently.

Key changes include:

  • Transitioning from Attestations (list of individual attestations) to AggregatedAttestations (list of aggregated attestations with bitlists)
  • Restructuring BlockSignatures from a flat list to a nested structure with attestation signature groups and a separate proposer signature
  • Adding aggregation logic to combine attestations with identical data roots

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkgs/types/src/attestation.zig Adds aggregation bit manipulation utilities and renames attestation_bits to aggregation_bits
pkgs/types/src/block.zig Refactors block signatures structure and adds aggregateSignedAttestations function
pkgs/types/src/state.zig Updates attestation processing to handle aggregated attestations and adds duplicate detection
pkgs/types/src/utils.zig Adds DuplicateAttestationData error type
pkgs/types/src/lib.zig Exports new aggregation-related functions and types
pkgs/state-transition/src/transition.zig Updates signature verification to handle aggregated attestations
pkgs/state-transition/src/mock.zig Adapts mock chain generation to use aggregated attestations
pkgs/node/src/chain.zig Updates block processing to aggregate attestations and handle signature groups
pkgs/node/src/validator_client.zig Adjusts proposer signature attachment to new structure
pkgs/spectest/src/runner/*.zig Updates test fixtures to parse aggregated attestations with bitlists
pkgs/database/src/*.zig Adapts test helpers and verification logic for new signature structure
pkgs/key-manager/src/lib.zig Changes signing to hash AttestationData instead of full Attestation
build.zig.zon Updates ssz dependency to version with required bug fix

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 358 to 362
if (groups.pop()) |removed| {
var cleanup = removed;
cleanup.bits.deinit();
cleanup.signatures.deinit();
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The error handling pattern uses an if statement with groups.pop() which returns an optional. However, since we just appended to groups on line 355, the pop should never return null. The cleanup logic would be clearer as var cleanup = groups.pop().?; with the assertion that it must exist, or restructure to use the errdefer pattern consistently.

Suggested change
if (groups.pop()) |removed| {
var cleanup = removed;
cleanup.bits.deinit();
cleanup.signatures.deinit();
}
var cleanup = groups.pop().?;
cleanup.bits.deinit();
cleanup.signatures.deinit();

Copilot uses AI. Check for mistakes.
Comment on lines 92 to 93
try obj.put("validator_id", json.Value{ .integer = @as(i64, @intCast(self.message.validator_id)) });
try obj.put("message", try self.message.data.toJson(allocator));
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The JSON structure for SignedAttestation has changed from {\"message\": <Attestation>} to {\"validator_id\": <id>, \"message\": <AttestationData>}. This is a breaking change to the API format that flattens the attestation structure. If this JSON is consumed by external systems or stored persistently, this could cause compatibility issues.

Suggested change
try obj.put("validator_id", json.Value{ .integer = @as(i64, @intCast(self.message.validator_id)) });
try obj.put("message", try self.message.data.toJson(allocator));
try obj.put("message", try self.message.toJson(allocator));

Copilot uses AI. Check for mistakes.
},
.attestations = aggregation.attestations,
},
};
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The errdefer block.deinit() on line 207 will double-free resources. If an error occurs after this point, block.deinit() will be called, and then the caller will likely also try to clean up the block since it was assigned aggregation.attestations on line 204. The aggregation.attestations ownership is transferred to block, so the errdefer aggregation.deinit() on line 192 could cause issues if both errdefers trigger.

Suggested change
};
};
// Ownership of `aggregation.attestations` has been transferred to `block`,
// so clear it from `aggregation` to avoid double-free in `aggregation.deinit()`.
aggregation.attestations = std.mem.zeroes(@TypeOf(aggregation.attestations));

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 54
for (self.attestations.constSlice()) |att_value| {
var att = att_value;
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Creating a mutable copy var att = att_value and calling att.deinit() on the copy does not actually clean up the original att_value in the slice. The deinit() should be called on a mutable reference to the original, or the attestations list should store pointers. This pattern will leak memory from the aggregation_bits.

Suggested change
for (self.attestations.constSlice()) |att_value| {
var att = att_value;
for (self.attestations.slice()) |*att| {

Copilot uses AI. Check for mistakes.
Comment on lines 194 to 195
for (self.attestation_signatures.constSlice()) |group_value| {
var group = group_value;
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Same issue as with attestations deinit: calling deinit() on a copy var group = group_value does not clean up the original value in the slice. This will leak memory from the signature lists.

Suggested change
for (self.attestation_signatures.constSlice()) |group_value| {
var group = group_value;
// Deinitialize each group in-place to ensure their resources are freed.
for (self.attestation_signatures.slice()) |*group| {

Copilot uses AI. Check for mistakes.
state,
&signed_block.message.proposer_attestation,
&signatures[signatures.len - 1],
@intCast(proposer_attestation.validator_id),
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The @intCast is redundant here since validator_id is already of type ValidatorIndex which should be compatible with usize. On line 108, the function parameter is directly assigned without casting. This inconsistency suggests either the cast is unnecessary or the parameter type should be ValidatorIndex.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 1, 2026 11:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This reverts commit f8d59070b9dc31f7f07cdb74c8f1e29c442683f1.

Signed-off-by: Chen Kai <[email protected]>
Copilot AI review requested due to automatic review settings January 3, 2026 14:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

is_target_already_justified or
!has_correct_source_root or
!has_correct_target_root or
!has_known_root or
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

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

The variable name has_known_root is misleading. The logic checks if either source OR target root is correct, but the variable name suggests that at least one root is known/correct. However, the negation !has_known_root in the validation check implies we're skipping when NO roots match. Consider renaming to has_matching_root or has_valid_root for clarity.

Copilot uses AI. Check for mistakes.
Comment on lines 358 to 362
if (groups.pop()) |removed| {
var cleanup = removed;
cleanup.bits.deinit();
cleanup.signatures.deinit();
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

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

The error handling in this block is duplicating cleanup logic that already exists in the errdefer at line 338-341. When put fails, the errdefer will automatically handle cleanup of all groups including the one just appended. The explicit pop() and cleanup here is redundant and could lead to double-free if the errdefer also runs.

Suggested change
if (groups.pop()) |removed| {
var cleanup = removed;
cleanup.bits.deinit();
cleanup.signatures.deinit();
}

Copilot uses AI. Check for mistakes.
}
};

pub const AggregatedAttestation = struct {
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

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

Field renamed from attestation_bits to aggregation_bits without a corresponding comment in the diff. While aggregation_bits is clearer and follows the spec naming, this is a breaking change to the public API.

Suggested change
pub const AggregatedAttestation = struct {
pub const AggregatedAttestation = struct {
// NOTE: Field renamed from `attestation_bits` to `aggregation_bits` to match the spec.
// This is a breaking change to the public API and is kept for clarity with spec naming.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 7, 2026 14:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@GrapeBaBa GrapeBaBa requested review from Copilot and g11tech January 7, 2026 16:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

};

target_justifications[validator_id] = 1;
for (validator_indices.items) |validator_index| {
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

Missing check for duplicate validator indices before processing. If the same validator appears multiple times in the aggregation bits, their justification will be counted multiple times, potentially leading to incorrect justification counts.

Copilot uses AI. Check for mistakes.
Comment on lines 364 to 367
const new_group = try AggregationGroup.init(allocator, signed_attestation);
try groups.append(new_group);
const inserted_index = groups.items.len - 1;
root_indices.put(root, inserted_index) catch |err| {
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

If root_indices.put fails, the cleanup code only removes the last item from groups, but the hashmap may still contain a stale entry for this root pointing to a now-invalid index. This could cause issues if the same root is encountered again.

Copilot uses AI. Check for mistakes.
@GrapeBaBa GrapeBaBa requested a review from g11tech January 9, 2026 07:30
@anshalshukla
Copy link
Collaborator

I see some discrepancies and have some suggestions, please wait for my review

Comment on lines 208 to 210
try std.testing.expect(encoded.items.len > 0);

// Convert to hex and compare with expected value
// Expected value is "0" * 6496 (6496 hex characters = 3248 bytes)
const expected_hex_len = 6496;
const expected_value = try std.testing.allocator.alloc(u8, expected_hex_len);
defer std.testing.allocator.free(expected_value);
@memset(expected_value, '0');

const encoded_hex = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{std.fmt.fmtSliceHexLower(encoded.items)});
defer std.testing.allocator.free(encoded_hex);
try std.testing.expectEqualStrings(expected_value, encoded_hex);

// Decode
var decoded: SignedAttestation = undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I think we should maintain the hardcoded byte number as it was proxy to verify that the signature length is right.

Comment on lines +53 to +55
for (self.attestations.slice()) |*att| {
att.deinit();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think self.attestations.deinit() should handle this logic as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, they are sszlist we cannot have a deep deinit there, you can skip this

Comment on lines +293 to +296
errdefer {
for (attestation_signatures.slice()) |*sig_group| {
sig_group.deinit();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

can attestation_signatures.deinit() do a deep deinit?

Copy link
Collaborator

Choose a reason for hiding this comment

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

not required

Signed-off-by: grapebaba <[email protected]>
Copilot AI review requested due to automatic review settings January 9, 2026 13:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const new_group = try AggregationGroup.init(allocator, signed_attestation);
try groups.append(new_group);
const inserted_index = groups.items.len - 1;
root_indices.put(root, inserted_index) catch |err| return err;
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The error handling pattern here is redundant. The catch |err| return err is equivalent to just using try. Use try root_indices.put(root, inserted_index); instead for cleaner code.

Suggested change
root_indices.put(root, inserted_index) catch |err| return err;
try root_indices.put(root, inserted_index);

Copilot uses AI. Check for mistakes.
Comment on lines +272 to +276
var aggregation_opt: ?types.AggregatedAttestationsResult = try types.aggregateSignedAttestations(self.allocator, signed_attestations);
errdefer if (aggregation_opt) |*aggregation| aggregation.deinit();

const aggregation = &aggregation_opt.?;

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The optional wrapping pattern for cleanup tracking is overly complex. Consider using a simpler approach where you call aggregation.deinit() directly in the errdefer and set ownership transfer more explicitly without the optional wrapper.

Suggested change
var aggregation_opt: ?types.AggregatedAttestationsResult = try types.aggregateSignedAttestations(self.allocator, signed_attestations);
errdefer if (aggregation_opt) |*aggregation| aggregation.deinit();
const aggregation = &aggregation_opt.?;
var aggregation = try types.aggregateSignedAttestations(self.allocator, signed_attestations);
errdefer aggregation.deinit();

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

if error happen after aggregation ownership transferred, we shouldn't call deinit.

Signed-off-by: grapebaba <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the new spec

4 participants