fix(types): clone participants bitlist when building aggregation bits#875
Conversation
… in compactAttestations
There was a problem hiding this comment.
Pull request overview
Fixes incorrect construction of attestation aggregation_bits by cloning the underlying participants bitlist, preserving SSZ bitlist length semantics (including trailing false bits) when producing aggregated attestations/proofs.
Changes:
- In
pkgs/types, replace “rebuild bits by setting true indices” withsszClonewhen derivingAggregationBitsfrom proof participants. - In
pkgs/node, update forkchoice proposal attestation building to clone participants intoAggregationBitsinstead of rebuilding.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pkgs/types/src/block.zig |
Switches aggregation-bit derivation to cloning the participants bitlist (multiple call sites). |
pkgs/node/src/forkchoice.zig |
Clones AggregationBits from selected proofs’ participants during proposal attestation selection. |
Comments suppressed due to low confidence (1)
pkgs/node/src/forkchoice.zig:1076
- This function still rebuilds
AggregationBitselsewhere by initializing an empty bitlist and setting only thetrueindices, which can change the bitlist’s encoded length (dropping trailingfalsebits). Since this hunk switches to cloning viasszClone, it would be more consistent/safer to use the same approach for otherAggregationBitscopies in this path (e.g., when buildingcandidate_atts).
var att_bits: types.AggregationBits = undefined;
try types.sszClone(self.allocator, types.AggregationBits, cloned_proof.participants, &att_bits);
errdefer att_bits.deinit();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var att_bits: types.AggregationBits = undefined; | ||
| try types.sszClone(self.allocator, types.AggregationBits, cloned_proof.participants, &att_bits); | ||
| errdefer att_bits.deinit(); | ||
|
|
||
| for (0..cloned_proof.participants.len()) |i| { |
| var att_bits_val: attestation.AggregationBits = undefined; | ||
| try utils.sszClone(allocator, attestation.AggregationBits, child.participants, &att_bits_val); | ||
| var att_bits: ?attestation.AggregationBits = att_bits_val; | ||
| defer if (att_bits) |*ab| ab.deinit(); |
|
Thanks for this @shariqnaiyer , few comments:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
pkgs/types/src/block.zig:646
att_bitsis declared as a non-optionalattestation.AggregationBits, but it’s still used as optional (att_bits.?) and later assignednull. This won’t compile, and it also breaks the intended ownership-transfer pattern (avoidingdeferdeinit after appending intoself.attestations). Either keepatt_bitsoptional (as before) or switch to a non-optional value and use a cleanup flag / scopeddeferso the bitlist isn’t deinitialized after ownership is transferred.
This issue also appears on line 713 of the same file.
var att_bits: attestation.AggregationBits = undefined;
try utils.sszClone(allocator, attestation.AggregationBits, child.participants, &att_bits);
defer att_bits.deinit();
// Clone the child proof for the result (original will be freed by deferred cleanup)
var cloned_child: aggregation.AggregatedSignatureProof = undefined;
try utils.sszClone(allocator, aggregation.AggregatedSignatureProof, child.*, &cloned_child);
errdefer cloned_child.deinit();
try self.attestations.append(.{ .aggregation_bits = att_bits.?, .data = data });
att_bits = null; // ownership transferred to self.attestations
pkgs/types/src/block.zig:718
- Same issue as above:
att_bitsis non-optional but is used asatt_bits.?and later set tonull. Beyond the compile error, the currentdefer att_bits.deinit()will also deinit memory that has been moved intoself.attestationsunless you explicitly disable it after the append (e.g., via optional +null, a cleanup flag, or a scopeddefer).
var att_bits: attestation.AggregationBits = undefined;
try utils.sszClone(allocator, attestation.AggregationBits, proof.participants, &att_bits);
defer att_bits.deinit();
try self.attestations.append(.{ .aggregation_bits = att_bits.?, .data = data });
att_bits = null; // ownership transferred to self.attestations
pkgs/state-transition/src/mock.zig:316
att_bitshaserrdefer att_bits.deinit()but is appended intoagg_attestationsbefore another fallible call (agg_signatures.append). If that second append fails, the function’s outeragg_att_cleanupwill deinit the appended attestation (freeingatt_bits) and this localerrdeferwill also run, risking a double free. Consider scoping theerrdeferto only cover the pre-append region, or rely solely on the outer list cleanup / roll back on partial append.
var att_bits: types.AggregationBits = undefined;
try types.sszClone(allocator, types.AggregationBits, proof.participants, &att_bits);
errdefer att_bits.deinit();
try agg_attestations.append(.{ .aggregation_bits = att_bits, .data = att_data });
try agg_signatures.append(proof);
}
| var att_bits: types.AggregationBits = undefined; | ||
| try types.sszClone(self.allocator, types.AggregationBits, cloned_proof.participants, &att_bits); | ||
| errdefer att_bits.deinit(); | ||
|
|
||
| for (0..cloned_proof.participants.len()) |i| { | ||
| if (cloned_proof.participants.get(i) catch false) { | ||
| try types.aggregationBitsSet(&att_bits, i, true); | ||
| if (i >= covered.capacity()) { |
| // Clone participant bits into proof. | ||
| var cloned_bits: types.AggregationBits = undefined; | ||
| types.sszClone(ctx.allocator, types.AggregationBits, aggregation_bits, &cloned_bits) catch |err| { | ||
| std.debug.print( | ||
| "fixture {s} case {s}{f}: failed to clone aggregation bits ({s})\n", |
|
@anshalshukla @ch4r10t33r Apart from this |
|
@anshalshukla Feel free to add any commits and merge this. I am going to be afk for a bit. |
When running a devnet with ream zeam ethlambda in 2 subnets we get the following error and ethlambda rejects zeams blocks at certain slots which lead to no finality in ethlambda during these devnets.
Some AI context:
I have tested this in the devnets and it fixes the issue. However feel free to request changes or close this PR.