diff --git a/build.zig.zon b/build.zig.zon index 1d9a462d..471ce4c7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +4,8 @@ .version = "0.0.0", .dependencies = .{ .ssz = .{ - .url = "https://github.com/blockblaz/ssz.zig/archive/refs/tags/v0.0.7.tar.gz", - .hash = "ssz-0.0.4-Lfwd65-iAgBkwCIZmHkrOvD3oCjooiBD73Nmp4gCNMG6", + .url = "git+https://github.com/blockblaz/ssz.zig#5ce7322fc45cab4f215021cae2579d1343e05d55", + .hash = "ssz-0.0.9-Lfwd61PEAgAUPJfUQiK4R5gRX_lTWOd_qYwNT-KAhRLA", }, .zigcli = .{ .url = "git+https://github.com/jiacai2050/zigcli?ref=main#dcbc59d70b4787671c8a4e484ffd2b725aa17af5", diff --git a/leanSpec b/leanSpec index d717c2d5..5eb47083 160000 --- a/leanSpec +++ b/leanSpec @@ -1 +1 @@ -Subproject commit d717c2d5f7fbdb22aa2c7aea4e1cb6054504d9a6 +Subproject commit 5eb470832733e26087c633081b51f875fc4c7102 diff --git a/pkgs/database/src/rocksdb.zig b/pkgs/database/src/rocksdb.zig index f8a98588..7a30d61a 100644 --- a/pkgs/database/src/rocksdb.zig +++ b/pkgs/database/src/rocksdb.zig @@ -1012,11 +1012,14 @@ test "save and load block" { try std.testing.expect(loaded.block.body.attestations.len() == 0); // Verify signatures match - try std.testing.expect(loaded_block.?.signature.len() == 2); - const loaded_sig1 = try loaded_block.?.signature.get(0); - const loaded_sig2 = try loaded_block.?.signature.get(1); - try std.testing.expect(std.mem.eql(u8, &loaded_sig1, &test_sig1)); - try std.testing.expect(std.mem.eql(u8, &loaded_sig2, &test_sig2)); + const signature_groups = loaded_block.?.signature.attestation_signatures; + try std.testing.expect(signature_groups.len() == 1); + const loaded_group = try signature_groups.get(0); + try std.testing.expect(loaded_group.len() == test_signatures.len); + for (test_signatures, 0..) |expected_sig, idx| { + const loaded_sig = try loaded_group.get(idx); + try std.testing.expect(std.mem.eql(u8, &loaded_sig, &expected_sig)); + } // Test loading a non-existent block const non_existent_root = test_helpers.createDummyRoot(0xFF); @@ -1134,13 +1137,14 @@ test "batch write and commit" { try std.testing.expect(std.mem.eql(u8, &loaded_block_data.block.state_root, &signed_block.message.block.state_root)); // Verify signatures match - try std.testing.expect(loaded_block.?.signature.len() == 3); - const loaded_sig1 = try loaded_block.?.signature.get(0); - const loaded_sig2 = try loaded_block.?.signature.get(1); - const loaded_sig3 = try loaded_block.?.signature.get(2); - try std.testing.expect(std.mem.eql(u8, &loaded_sig1, &test_sig1)); - try std.testing.expect(std.mem.eql(u8, &loaded_sig2, &test_sig2)); - try std.testing.expect(std.mem.eql(u8, &loaded_sig3, &test_sig3)); + const batch_signature_groups = loaded_block.?.signature.attestation_signatures; + try std.testing.expect(batch_signature_groups.len() == 1); + const loaded_group = try batch_signature_groups.get(0); + try std.testing.expect(loaded_group.len() == test_signatures.len); + for (test_signatures, 0..) |expected_sig, idx| { + const loaded_sig = try loaded_group.get(idx); + try std.testing.expect(std.mem.eql(u8, &loaded_sig, &expected_sig)); + } // Verify state was saved and can be loaded const loaded_state = db.loadState(database.DbStatesNamespace, test_state_root); diff --git a/pkgs/database/src/test_helpers.zig b/pkgs/database/src/test_helpers.zig index 689b1698..6da59c9a 100644 --- a/pkgs/database/src/test_helpers.zig +++ b/pkgs/database/src/test_helpers.zig @@ -4,13 +4,15 @@ const types = @import("@zeam/types"); /// Helper function to create a dummy block for testing pub fn createDummyBlock(allocator: Allocator, slot: u64, proposer_index: u64, parent_root_fill: u8, state_root_fill: u8, signatures: []const types.SIGBYTES) !types.SignedBlockWithAttestation { + const attestations_list = try types.AggregatedAttestations.init(allocator); + var test_block = types.BeamBlock{ .slot = slot, .proposer_index = proposer_index, .parent_root = undefined, .state_root = undefined, .body = types.BeamBlockBody{ - .attestations = try types.Attestations.init(allocator), + .attestations = attestations_list, }, }; @memset(&test_block.parent_root, parent_root_fill); @@ -40,14 +42,29 @@ pub fn createDummyBlock(allocator: Allocator, slot: u64, proposer_index: u64, pa .proposer_attestation = proposer_attestation, }; - var block_signatures = try types.BlockSignatures.init(allocator); - errdefer block_signatures.deinit(); + var attestation_signatures = try types.AttestationSignatures.init(allocator); + var attestation_signatures_cleanup = true; + errdefer if (attestation_signatures_cleanup) attestation_signatures.deinit(); + + if (signatures.len > 0) { + var validator_signatures = try types.NaiveAggregatedSignature.init(allocator); + var validator_signatures_cleanup = true; + errdefer if (validator_signatures_cleanup) validator_signatures.deinit(); + + for (signatures) |sig| { + try validator_signatures.append(sig); + } - // Add all provided signatures to the list - for (signatures) |sig| { - try block_signatures.append(sig); + try attestation_signatures.append(validator_signatures); + validator_signatures_cleanup = false; } + const block_signatures = types.BlockSignatures{ + .attestation_signatures = attestation_signatures, + .proposer_signature = types.ZERO_SIGBYTES, + }; + attestation_signatures_cleanup = false; + const signed_block = types.SignedBlockWithAttestation{ .message = block_with_attestation, .signature = block_signatures, diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index 5a6cd4d7..977bbe3b 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -97,7 +97,7 @@ pub const KeyManager = struct { const signing_timer = zeam_metrics.lean_pq_signature_attestation_signing_time_seconds.start(); var message: [32]u8 = undefined; - try ssz.hashTreeRoot(types.Attestation, attestation.*, &message, allocator); + try ssz.hashTreeRoot(types.AttestationData, attestation.data, &message, allocator); const epoch: u32 = @intCast(attestation.data.slot); diff --git a/pkgs/network/src/mock.zig b/pkgs/network/src/mock.zig index 9e727895..b21fb5c7 100644 --- a/pkgs/network/src/mock.zig +++ b/pkgs/network/src/mock.zig @@ -656,7 +656,7 @@ test "Mock messaging across two subscribers" { try network.gossip.subscribe(&topics, subscriber2.getCallbackHandler()); // Create a simple block message - var attestations = try types.Attestations.init(allocator); + var attestations = try types.AggregatedAttestations.init(allocator); const block_message = try allocator.create(interface.GossipMessage); defer allocator.destroy(block_message); diff --git a/pkgs/node/src/chain.zig b/pkgs/node/src/chain.zig index 7a498153..8156af2a 100644 --- a/pkgs/node/src/chain.zig +++ b/pkgs/node/src/chain.zig @@ -16,7 +16,6 @@ const event_broadcaster = api.event_broadcaster; const zeam_utils = @import("@zeam/utils"); const keymanager = @import("@zeam/key-manager"); -const jsonToString = zeam_utils.jsonToString; const utils = @import("./utils.zig"); pub const fcFactory = @import("./forkchoice.zig"); @@ -60,8 +59,17 @@ pub const GossipProcessingResult = struct { pub const ProducedBlock = struct { block: types.BeamBlock, blockRoot: types.Root, - // signatures corresponding to attestations in the blockbody - signatures: types.BlockSignatures, + + // Aggregated signatures corresponding to attestations in the block body. + attestation_signatures: types.AttestationSignatures, + + pub fn deinit(self: *ProducedBlock) void { + self.block.deinit(); + for (self.attestation_signatures.slice()) |*sig_group| { + sig_group.deinit(); + } + self.attestation_signatures.deinit(); + } }; pub const BeamChain = struct { @@ -256,9 +264,19 @@ pub const BeamChain = struct { const parent_root = chainHead.blockRoot; const pre_state = self.states.get(parent_root) orelse return BlockProductionError.MissingPreState; - const post_state = try self.allocator.create(types.BeamState); + var post_state_opt: ?*types.BeamState = try self.allocator.create(types.BeamState); + errdefer if (post_state_opt) |post_state_ptr| { + post_state_ptr.deinit(); + self.allocator.destroy(post_state_ptr); + }; + const post_state = post_state_opt.?; try types.sszClone(self.allocator, types.BeamState, pre_state.*, post_state); + var aggregation_opt: ?types.AggregatedAttestationsResult = try types.aggregateSignedAttestations(self.allocator, signed_attestations); + errdefer if (aggregation_opt) |*aggregation| aggregation.deinit(); + + const aggregation = &aggregation_opt.?; + // keeping for later when execution will be integrated into lean // const timestamp = self.config.genesis.genesis_time + opts.slot * params.SECONDS_PER_SLOT; @@ -269,18 +287,23 @@ pub const BeamChain = struct { .state_root = undefined, .body = types.BeamBlockBody{ // .execution_payload_header = .{ .timestamp = timestamp }, - .attestations = blk: { - var attestations_list = try types.Attestations.init(self.allocator); - for (signed_attestations) |signed_attestation| { - try attestations_list.append(signed_attestation.message); - } - break :blk attestations_list; - }, + .attestations = aggregation.attestations, }, }; + errdefer block.deinit(); + + var attestation_signatures = aggregation.attestation_signatures; + errdefer { + for (attestation_signatures.slice()) |*sig_group| { + sig_group.deinit(); + } + attestation_signatures.deinit(); + } + + // Ownership moved into `block` + `attestation_signatures`. + aggregation_opt = null; - var block_json = try block.toJson(self.allocator); - const block_str = try jsonToString(self.allocator, block_json); + const block_str = try block.toJsonString(self.allocator); defer self.allocator.free(block_str); self.module_logger.debug("node-{d}::going for block production opts={any} raw block={s}", .{ self.nodeId, opts, block_str }); @@ -288,8 +311,7 @@ pub const BeamChain = struct { // 2. apply STF to get post state & update post state root & cache it try stf.apply_raw_block(self.allocator, post_state, &block, self.block_building_logger); - block_json = try block.toJson(self.allocator); - const block_str_2 = try jsonToString(self.allocator, block_json); + const block_str_2 = try block.toJsonString(self.allocator); defer self.allocator.free(block_str_2); self.module_logger.debug("applied raw block opts={any} raw block={s}", .{ opts, block_str_2 }); @@ -297,7 +319,17 @@ pub const BeamChain = struct { // 3. cache state to save recompute while adding the block on publish var block_root: [32]u8 = undefined; try ssz.hashTreeRoot(types.BeamBlock, block, &block_root, self.allocator); + try self.states.put(block_root, post_state); + post_state_opt = null; + + var forkchoice_added = false; + errdefer if (!forkchoice_added) { + if (self.states.fetchRemove(block_root)) |entry| { + entry.value.deinit(); + self.allocator.destroy(entry.value); + } + }; // 4. Add the block to directly forkchoice as this proposer will next need to construct its vote // note - attestations packed in the block are already in the knownVotes so we don't need to re-import @@ -309,18 +341,13 @@ pub const BeamChain = struct { // confirmed in publish .confirmed = false, }); + forkchoice_added = true; _ = try self.forkChoice.updateHead(); return .{ .block = block, .blockRoot = block_root, - .signatures = blk: { - var signatures_list = try types.BlockSignatures.init(self.allocator); - for (signed_attestations) |signed_attestation| { - try signatures_list.append(signed_attestation.signature); - } - break :blk signatures_list; - }, + .attestation_signatures = attestation_signatures, }; } @@ -481,8 +508,8 @@ pub const BeamChain = struct { return .{}; }, .attestation => |signed_attestation| { - const slot = signed_attestation.message.data.slot; - const validator_id = signed_attestation.message.validator_id; + const slot = signed_attestation.message.slot; + const validator_id = signed_attestation.validator_id; const validator_node_name = self.node_registry.getNodeNameFromValidatorIndex(validator_id); const sender_node_name = self.node_registry.getNodeNameFromPeerId(sender_peer_id); @@ -495,7 +522,7 @@ pub const BeamChain = struct { }); // Validate attestation before processing (gossip = not from block) - self.validateAttestation(signed_attestation.message, false) catch |err| { + self.validateAttestation(signed_attestation.toAttestation(), false) catch |err| { self.module_logger.warn("gossip attestation validation failed: {any}", .{err}); zeam_metrics.metrics.lean_attestations_invalid_total.incr(.{ .source = "gossip" }) catch {}; return .{}; // Drop invalid gossip attestations @@ -526,7 +553,6 @@ pub const BeamChain = struct { const onblock_timer = zeam_metrics.chain_onblock_duration_seconds.start(); const block = signedBlock.message.block; - const signatures = signedBlock.signature.constSlice(); const block_root: types.Root = blockInfo.blockRoot orelse computedroot: { var cblock_root: [32]u8 = undefined; @@ -571,30 +597,67 @@ pub const BeamChain = struct { block.slot, }); - for (block.body.attestations.constSlice(), 0..) |attestation, index| { - // Validate attestation before processing (from block = true) - self.validateAttestation(attestation, true) catch |e| { - zeam_metrics.metrics.lean_attestations_invalid_total.incr(.{ .source = "block" }) catch {}; - if (e == AttestationValidationError.UnknownHeadBlock) { - try missing_roots.append(attestation.data.head.root); - } + const aggregated_attestations = block.body.attestations.constSlice(); + const signature_groups = signedBlock.signature.attestation_signatures.constSlice(); - self.module_logger.err("invalid attestation in block: validator={d} error={any}", .{ - attestation.validator_id, - e, - }); - // Skip invalid attestations but continue processing the block - continue; - }; + if (aggregated_attestations.len != signature_groups.len) { + self.module_logger.err( + "signature group count mismatch for block root=0x{s}: attestations={d} signature_groups={d}", + .{ std.fmt.fmtSliceHexLower(&freshFcBlock.blockRoot), aggregated_attestations.len, signature_groups.len }, + ); + } - const signed_attestation = types.SignedAttestation{ .message = attestation, .signature = signatures[index] }; + for (aggregated_attestations, 0..) |aggregated_attestation, index| { + var validator_indices = try types.aggregationBitsToValidatorIndices(&aggregated_attestation.aggregation_bits, self.allocator); + defer validator_indices.deinit(); - self.forkChoice.onAttestation(signed_attestation, true) catch |e| { + const group_signatures = if (index < signature_groups.len) + signature_groups[index].constSlice() + else + &[_]types.SIGBYTES{}; + + if (validator_indices.items.len != group_signatures.len) { zeam_metrics.metrics.lean_attestations_invalid_total.incr(.{ .source = "block" }) catch {}; - self.module_logger.err("error processing block attestation={any} e={any}", .{ signed_attestation, e }); + self.module_logger.err( + "attestation signature mismatch index={d} validators={d} signatures={d}", + .{ index, validator_indices.items.len, group_signatures.len }, + ); continue; - }; - zeam_metrics.metrics.lean_attestations_valid_total.incr(.{ .source = "block" }) catch {}; + } + + for (validator_indices.items, group_signatures) |validator_index, signature| { + const validator_id: types.ValidatorIndex = @intCast(validator_index); + const attestation = types.Attestation{ + .validator_id = validator_id, + .data = aggregated_attestation.data, + }; + + self.validateAttestation(attestation, true) catch |e| { + zeam_metrics.metrics.lean_attestations_invalid_total.incr(.{ .source = "block" }) catch {}; + if (e == AttestationValidationError.UnknownHeadBlock) { + try missing_roots.append(attestation.data.head.root); + } + + self.module_logger.err("invalid attestation in block: validator={d} error={any}", .{ + validator_index, + e, + }); + continue; + }; + + const signed_attestation = types.SignedAttestation{ + .validator_id = validator_id, + .message = attestation.data, + .signature = signature, + }; + + self.forkChoice.onAttestation(signed_attestation, true) catch |e| { + zeam_metrics.metrics.lean_attestations_invalid_total.incr(.{ .source = "block" }) catch {}; + self.module_logger.err("error processing block attestation={any} e={any}", .{ signed_attestation, e }); + continue; + }; + zeam_metrics.metrics.lean_attestations_valid_total.incr(.{ .source = "block" }) catch {}; + } } // 5. fc update head @@ -604,10 +667,11 @@ pub const BeamChain = struct { }; try self.states.put(fcBlock.blockRoot, post_state); - // 6. import proposer attestation as if it was transmitted on network after block - const proposer_signature = signatures[block.body.attestations.len()]; + // 6. proposer attestation + const proposer_signature = signedBlock.signature.proposer_signature; const signed_proposer_attestation = types.SignedAttestation{ - .message = signedBlock.message.proposer_attestation, + .validator_id = signedBlock.message.proposer_attestation.validator_id, + .message = signedBlock.message.proposer_attestation.data, .signature = proposer_signature, }; self.forkChoice.onAttestation(signed_proposer_attestation, false) catch |e| { @@ -989,12 +1053,18 @@ pub const BeamChain = struct { pub fn onAttestation(self: *Self, signedAttestation: types.SignedAttestation) !void { // Validate attestation before processing (gossip = not from block) - try self.validateAttestation(signedAttestation.message, false); + const attestation = signedAttestation.toAttestation(); + try self.validateAttestation(attestation, false); - const attestation = signedAttestation.message; const state = self.states.get(attestation.data.target.root) orelse return AttestationValidationError.MissingState; - try stf.verifySingleAttestation(self.allocator, state, &attestation, &signedAttestation.signature); + try stf.verifySingleAttestation( + self.allocator, + state, + @intCast(signedAttestation.validator_id), + &signedAttestation.message, + &signedAttestation.signature, + ); return self.forkChoice.onAttestation(signedAttestation, false); } @@ -1290,238 +1360,220 @@ test "attestation validation - comprehensive" { const source_slot: types.Slot = 1; const target_slot: types.Slot = 2; const valid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = target_slot, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[target_slot], + .slot = target_slot, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[source_slot], + .slot = source_slot, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[target_slot], .slot = target_slot, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[target_slot], - .slot = target_slot, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[source_slot], - .slot = source_slot, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[target_slot], - .slot = target_slot, - }, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; // Should pass validation - try beam_chain.validateAttestation(valid_attestation.message, false); + try beam_chain.validateAttestation(valid_attestation.toAttestation(), false); } // Test 2: Unknown source block { const unknown_root = [_]u8{0xFF} ** 32; const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Checkpoint{ + .root = unknown_root, // Unknown block + .slot = 1, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = unknown_root, // Unknown block - .slot = 1, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.UnknownSourceBlock, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.UnknownSourceBlock, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 3: Unknown target block { const unknown_root = [_]u8{0xEE} ** 32; const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Checkpoint{ + .root = unknown_root, // Unknown block .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .target = types.Checkpoint{ - .root = unknown_root, // Unknown block - .slot = 2, - }, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.UnknownTargetBlock, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.UnknownTargetBlock, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 4: Unknown head block { const unknown_root = [_]u8{0xDD} ** 32; const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = unknown_root, // Unknown block + .slot = 2, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = unknown_root, // Unknown block - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.UnknownHeadBlock, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.UnknownHeadBlock, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 5: Source slot exceeds target slot (block slots) { const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.SourceSlotExceedsTarget, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.SourceSlotExceedsTarget, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 6: Source checkpoint slot exceeds target checkpoint slot { const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.SourceSlotExceedsTarget, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.SourceSlotExceedsTarget, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 7: Source checkpoint slot mismatch { const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 0, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 0, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.SourceCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.SourceCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 8: Target checkpoint slot mismatch { const invalid_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 1, // Checkpoint claims slot 1 (mismatch) - }, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 1, // Checkpoint claims slot 1 (mismatch) }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.TargetCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_attestation.message, false)); + try std.testing.expectError(error.TargetCheckpointSlotMismatch, beam_chain.validateAttestation(invalid_attestation.toAttestation(), false)); } // Test 9: Attestation too far in future (for gossip) { const future_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ - .slot = 3, // Future slot (current is 2) - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[2], - .slot = 2, - }, + .slot = 3, // Future slot (current is 2) + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[2], + .slot = 2, }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; - try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(future_attestation.message, false)); + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(future_attestation.toAttestation(), false)); } } @@ -1576,22 +1628,20 @@ test "attestation validation - gossip vs block future slot handling" { // Current time is at slot 1, create attestation for slot 2 (next slot) const next_slot_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ - .slot = 2, - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[0], - .slot = 0, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, + .slot = 2, + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[0], + .slot = 0, + }, + .target = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, }, }, .signature = [_]u8{0} ** types.SIGSIZE, @@ -1599,35 +1649,33 @@ test "attestation validation - gossip vs block future slot handling" { // Gossip attestations: should FAIL for next slot (current + 1) // Per spec store.py:177: assert attestation.slot <= time_slots - try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(next_slot_attestation.message, false)); + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(next_slot_attestation.toAttestation(), false)); // Block attestations: should PASS for next slot (current + 1) // Per spec store.py:140: assert attestation.slot <= Slot(current_slot + Slot(1)) - try beam_chain.validateAttestation(next_slot_attestation.message, true); + try beam_chain.validateAttestation(next_slot_attestation.toAttestation(), true); const too_far_attestation: types.SignedAttestation = .{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ - .slot = 3, // Too far in future - .head = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, - .source = types.Checkpoint{ - .root = mock_chain.blockRoots[0], - .slot = 0, - }, - .target = types.Checkpoint{ - .root = mock_chain.blockRoots[1], - .slot = 1, - }, + .slot = 3, // Too far in future + .head = types.Checkpoint{ + .root = mock_chain.blockRoots[1], + .slot = 1, + }, + .source = types.Checkpoint{ + .root = mock_chain.blockRoots[0], + .slot = 0, + }, + .target = types.Checkpoint{ + .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_attestation.message, false)); - try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(too_far_attestation.message, true)); + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(too_far_attestation.toAttestation(), false)); + try std.testing.expectError(error.AttestationTooFarInFuture, beam_chain.validateAttestation(too_far_attestation.toAttestation(), true)); } // TODO: Enable and update this test once the keymanager file-reading PR is added // JSON parsing for chain config needs to support validator_pubkeys instead of num_validators @@ -1704,7 +1752,8 @@ test "attestation processing - valid block attestation" { const signature = try key_manager.signAttestation(&message, allocator); const valid_attestation: types.SignedAttestation = .{ - .message = message, + .validator_id = message.validator_id, + .message = message.data, .signature = signature, }; diff --git a/pkgs/node/src/forkchoice.zig b/pkgs/node/src/forkchoice.zig index 1726b5b2..1169ce53 100644 --- a/pkgs/node/src/forkchoice.zig +++ b/pkgs/node/src/forkchoice.zig @@ -653,7 +653,7 @@ pub const ForkChoice = struct { .latestKnown orelse ProtoAttestation{}).attestation; if (validator_attestation) |signed_attestation| { - if (std.mem.eql(u8, &latest_justified.root, &signed_attestation.message.data.source.root)) { + if (std.mem.eql(u8, &latest_justified.root, &signed_attestation.message.source.root)) { try included_attestations.append(signed_attestation); } } @@ -766,11 +766,11 @@ pub const ForkChoice = struct { // attestation has to be of an ancestor of the current slot const attestation = signed_attestation.message; - const validator_id = attestation.validator_id; - const attestation_slot = attestation.data.slot; + const validator_id = signed_attestation.validator_id; + const attestation_slot = attestation.slot; // This get should never fail after validation, but we keep the check for safety - const new_head_index = self.protoArray.indices.get(attestation.data.head.root) orelse { + const new_head_index = self.protoArray.indices.get(attestation.head.root) orelse { // Track whether this is from gossip or block processing return ForkChoiceError.InvalidAttestation; }; @@ -1340,14 +1340,12 @@ test "getCanonicalAncestorAtDepth and getCanonicalityAnalysis" { // Helper function to create a SignedAttestation for testing fn createTestSignedAttestation(validator_id: usize, head_root: types.Root, slot: types.Slot) types.SignedAttestation { return types.SignedAttestation{ + .validator_id = @intCast(validator_id), .message = .{ - .validator_id = validator_id, - .data = .{ - .slot = slot, - .head = .{ .root = head_root, .slot = slot }, - .target = .{ .root = head_root, .slot = slot }, - .source = .{ .root = createTestRoot(0xAA), .slot = 0 }, - }, + .slot = slot, + .head = .{ .root = head_root, .slot = slot }, + .target = .{ .root = head_root, .slot = slot }, + .source = .{ .root = createTestRoot(0xAA), .slot = 0 }, }, .signature = [_]u8{0} ** types.SIGSIZE, }; diff --git a/pkgs/node/src/node.zig b/pkgs/node/src/node.zig index 49676a70..157838cc 100644 --- a/pkgs/node/src/node.zig +++ b/pkgs/node/src/node.zig @@ -144,8 +144,8 @@ pub const BeamNode = struct { } }, .attestation => |signed_attestation| { - const slot = signed_attestation.message.data.slot; - const validator_id = signed_attestation.message.validator_id; + const slot = signed_attestation.message.slot; + const validator_id = signed_attestation.validator_id; const validator_node_name = self.node_registry.getNodeNameFromValidatorIndex(validator_id); const sender_node_name = self.node_registry.getNodeNameFromPeerId(sender_peer_id); @@ -825,13 +825,13 @@ pub const BeamNode = struct { } pub fn publishAttestation(self: *Self, signed_attestation: types.SignedAttestation) !void { - const message = signed_attestation.message; - const data = message.data; + const data = signed_attestation.message; + const validator_id = signed_attestation.validator_id; // 1. Process locally through chain self.logger.info("adding locally produced attestation to chain: slot={d} validator={d}", .{ data.slot, - message.validator_id, + validator_id, }); try self.chain.onAttestation(signed_attestation); @@ -841,8 +841,8 @@ pub const BeamNode = struct { self.logger.info("published attestation to network: slot={d} validator={d}{}", .{ data.slot, - message.validator_id, - self.node_registry.getNodeNameFromValidatorIndex(message.validator_id), + validator_id, + self.node_registry.getNodeNameFromValidatorIndex(validator_id), }); } @@ -1036,7 +1036,7 @@ test "Node: fetched blocks cache and deduplication" { .proposer_index = 0, .state_root = [_]u8{0} ** 32, .body = .{ - .attestations = try ssz.utils.List(types.Attestation, params.VALIDATOR_REGISTRY_LIMIT).init(allocator), + .attestations = try types.AggregatedAttestations.init(allocator), }, }, .proposer_attestation = .{ @@ -1049,7 +1049,7 @@ test "Node: fetched blocks cache and deduplication" { }, }, }, - .signature = try types.BlockSignatures.init(allocator), + .signature = try types.createBlockSignatures(allocator, 0), }; const block2_ptr = try allocator.create(types.SignedBlockWithAttestation); @@ -1061,7 +1061,7 @@ test "Node: fetched blocks cache and deduplication" { .proposer_index = 0, .state_root = [_]u8{0} ** 32, .body = .{ - .attestations = try ssz.utils.List(types.Attestation, params.VALIDATOR_REGISTRY_LIMIT).init(allocator), + .attestations = try types.AggregatedAttestations.init(allocator), }, }, .proposer_attestation = .{ @@ -1074,7 +1074,7 @@ test "Node: fetched blocks cache and deduplication" { }, }, }, - .signature = try types.BlockSignatures.init(allocator), + .signature = try types.createBlockSignatures(allocator, 0), }; // Cache blocks @@ -1196,7 +1196,7 @@ fn makeTestSignedBlockWithParent( .proposer_index = 0, .state_root = [_]u8{0} ** 32, .body = .{ - .attestations = try ssz.utils.List(types.Attestation, params.VALIDATOR_REGISTRY_LIMIT).init(allocator), + .attestations = try types.AggregatedAttestations.init(allocator), }, }, .proposer_attestation = .{ @@ -1209,7 +1209,7 @@ fn makeTestSignedBlockWithParent( }, }, }, - .signature = try types.BlockSignatures.init(allocator), + .signature = try types.createBlockSignatures(allocator, 0), }; return block_ptr; diff --git a/pkgs/node/src/testing.zig b/pkgs/node/src/testing.zig index f85b9f5e..4da4fac8 100644 --- a/pkgs/node/src/testing.zig +++ b/pkgs/node/src/testing.zig @@ -152,20 +152,36 @@ pub const NodeTestContext = struct { allocator: Allocator, block: *types.SignedBlockWithAttestation, ) !void { - var signatures = try types.BlockSignatures.init(allocator); - var signatures_initialized = false; - defer if (!signatures_initialized) signatures.deinit(); - - for (block.message.block.body.attestations.constSlice()) |attestation| { - const signature = try self.key_manager.signAttestation(&attestation, allocator); - try signatures.append(signature); + var attestation_signatures = try types.AttestationSignatures.init(allocator); + errdefer attestation_signatures.deinit(); + + for (block.message.block.body.attestations.constSlice()) |aggregated_attestation| { + var validator_signatures = try types.NaiveAggregatedSignature.init(allocator); + errdefer validator_signatures.deinit(); + + var indices = try types.aggregationBitsToValidatorIndices(&aggregated_attestation.aggregation_bits, allocator); + defer indices.deinit(); + + for (indices.items) |validator_index| { + var attestation = types.Attestation{ + .validator_id = @intCast(validator_index), + .data = aggregated_attestation.data, + }; + const signature = try self.key_manager.signAttestation(&attestation, allocator); + try validator_signatures.append(signature); + } + + try attestation_signatures.append(validator_signatures); } const proposer_signature = try self.key_manager.signAttestation(&block.message.proposer_attestation, allocator); - try signatures.append(proposer_signature); + + const signatures = types.BlockSignatures{ + .attestation_signatures = attestation_signatures, + .proposer_signature = proposer_signature, + }; block.signature.deinit(); block.signature = signatures; - signatures_initialized = true; } }; diff --git a/pkgs/node/src/validator_client.zig b/pkgs/node/src/validator_client.zig index 4939a946..59df2c83 100644 --- a/pkgs/node/src/validator_client.zig +++ b/pkgs/node/src/validator_client.zig @@ -138,13 +138,13 @@ pub const ValidatorClient = struct { .proposer_attestation = proposer_attestation, }; - // 4. Prepare signatures by adding the proposer signature to the already received list of - // attestation signatures - var signatures = produced_block.signatures; - - // 5. Sign proposer attestation (last signature) + // 4. Sign proposer attestation and build block signatures from the already-aggregated + // attestation signatures returned by block production. const proposer_signature = try self.key_manager.signAttestation(&proposer_attestation, self.allocator); - try signatures.append(proposer_signature); + const signatures = types.BlockSignatures{ + .attestation_signatures = produced_block.attestation_signatures, + .proposer_signature = proposer_signature, + }; const signed_block = types.SignedBlockWithAttestation{ .message = block_with_attestation, @@ -205,7 +205,8 @@ pub const ValidatorClient = struct { const signature = try self.key_manager.signAttestation(&attestation, self.allocator); const signed_attestation: types.SignedAttestation = .{ - .message = attestation, + .validator_id = validator_id, + .message = attestation_data, .signature = signature, }; diff --git a/pkgs/spectest/src/runner/fork_choice_runner.zig b/pkgs/spectest/src/runner/fork_choice_runner.zig index 371e97ff..0a099623 100644 --- a/pkgs/spectest/src/runner/fork_choice_runner.zig +++ b/pkgs/spectest/src/runner/fork_choice_runner.zig @@ -691,7 +691,8 @@ fn processBlockStep( } const signed_attestation = types.SignedAttestation{ - .message = proposer_attestation, + .validator_id = proposer_attestation.validator_id, + .message = proposer_attestation.data, .signature = types.ZERO_SIGBYTES, }; try ctx.fork_choice.onAttestation(signed_attestation, false); @@ -950,7 +951,7 @@ fn verifyAttestationChecks( if (obj.get("attestationSlot")) |slot_value| { const expected = try expectU64Value(slot_value, fixture_path, case_name, step_index, "attestationSlot"); - if (attestation.message.data.slot != expected) { + if (attestation.message.slot != expected) { std.debug.print( "fixture {s} case {s}{}: validator {d} attestation slot mismatch\n", .{ fixture_path, case_name, formatStep(step_index), validator }, @@ -961,7 +962,7 @@ fn verifyAttestationChecks( if (obj.get("headSlot")) |slot_value| { const expected = try expectU64Value(slot_value, fixture_path, case_name, step_index, "headSlot"); - if (attestation.message.data.head.slot != expected) { + if (attestation.message.head.slot != expected) { std.debug.print( "fixture {s} case {s}{}: validator {d} head slot mismatch\n", .{ fixture_path, case_name, formatStep(step_index), validator }, @@ -972,7 +973,7 @@ fn verifyAttestationChecks( if (obj.get("sourceSlot")) |slot_value| { const expected = try expectU64Value(slot_value, fixture_path, case_name, step_index, "sourceSlot"); - if (attestation.message.data.source.slot != expected) { + if (attestation.message.source.slot != expected) { std.debug.print( "fixture {s} case {s}{}: validator {d} source slot mismatch\n", .{ fixture_path, case_name, formatStep(step_index), validator }, @@ -983,7 +984,7 @@ fn verifyAttestationChecks( if (obj.get("targetSlot")) |slot_value| { const expected = try expectU64Value(slot_value, fixture_path, case_name, step_index, "targetSlot"); - if (attestation.message.data.target.slot != expected) { + if (attestation.message.target.slot != expected) { std.debug.print( "fixture {s} case {s}{}: validator {d} target slot mismatch\n", .{ fixture_path, case_name, formatStep(step_index), validator }, @@ -1201,12 +1202,12 @@ fn parseAttestations( case_name: []const u8, step_index: ?usize, value: JsonValue, -) FixtureError!types.Attestations { +) FixtureError!types.AggregatedAttestations { switch (value) { - .null => return try types.Attestations.init(allocator), + .null => return types.AggregatedAttestations.init(allocator) catch return FixtureError.InvalidFixture, .object => |obj| { const data_value = obj.get("data") orelse { - return try types.Attestations.init(allocator); + return types.AggregatedAttestations.init(allocator) catch return FixtureError.InvalidFixture; }; const arr = switch (data_value) { .array => |array| array, @@ -1219,8 +1220,8 @@ fn parseAttestations( }, }; - var attestations = try types.Attestations.init(allocator); - errdefer attestations.deinit(); + var aggregated_attestations = types.AggregatedAttestations.init(allocator) catch return FixtureError.InvalidFixture; + errdefer aggregated_attestations.deinit(); for (arr.items, 0..) |item, idx| { const att_obj = switch (item) { @@ -1234,16 +1235,57 @@ fn parseAttestations( }, }; - var validator_ctx_buf: [96]u8 = undefined; - const validator_ctx = std.fmt.bufPrint(&validator_ctx_buf, "attestations[{d}].validator_id", .{idx}) catch "attestations.validator_id"; - const validator_id = try expectU64Field( - att_obj, - &.{ "validator_id", "validatorIndex", "validatorId" }, - fixture_path, - case_name, - step_index, - validator_ctx, - ); + const bits_value = att_obj.get("aggregationBits") orelse { + std.debug.print( + "fixture {s} case {s}{}: attestation #{} missing aggregationBits\n", + .{ fixture_path, case_name, formatStep(step_index), idx }, + ); + return FixtureError.InvalidFixture; + }; + const bits_obj = switch (bits_value) { + .object => |map| map, + else => { + std.debug.print( + "fixture {s} case {s}{}: attestation #{} aggregationBits must be object\n", + .{ fixture_path, case_name, formatStep(step_index), idx }, + ); + return FixtureError.InvalidFixture; + }, + }; + const bits_data_value = bits_obj.get("data") orelse { + std.debug.print( + "fixture {s} case {s}{}: attestation #{} aggregationBits missing data\n", + .{ fixture_path, case_name, formatStep(step_index), idx }, + ); + return FixtureError.InvalidFixture; + }; + const bits_arr = switch (bits_data_value) { + .array => |array| array, + else => { + std.debug.print( + "fixture {s} case {s}{}: attestation #{} aggregationBits.data must be array\n", + .{ fixture_path, case_name, formatStep(step_index), idx }, + ); + return FixtureError.InvalidFixture; + }, + }; + + var aggregation_bits = types.AggregationBits.init(allocator) catch return FixtureError.InvalidFixture; + errdefer aggregation_bits.deinit(); + + for (bits_arr.items) |bit_value| { + const bit = switch (bit_value) { + .bool => |b| b, + else => { + std.debug.print( + "fixture {s} case {s}{}: attestation #{} aggregationBits element must be bool\n", + .{ fixture_path, case_name, formatStep(step_index), idx }, + ); + return FixtureError.InvalidFixture; + }, + }; + aggregation_bits.append(bit) catch return FixtureError.InvalidFixture; + } const data_obj = try expectObject(att_obj, "data", fixture_path, case_name, step_index); @@ -1276,8 +1318,8 @@ fn parseAttestations( const source_slot_ctx = std.fmt.bufPrint(&source_slot_ctx_buf, "attestations[{d}].data.source.slot", .{idx}) catch "attestations.source.slot"; const source_slot = try expectU64Field(source_obj, &.{"slot"}, fixture_path, case_name, step_index, source_slot_ctx); - const attestation = types.Attestation{ - .validator_id = validator_id, + const aggregated_attestation = types.AggregatedAttestation{ + .aggregation_bits = aggregation_bits, .data = .{ .slot = att_slot, .head = .{ .root = head_root, .slot = head_slot }, @@ -1286,7 +1328,7 @@ fn parseAttestations( }, }; - attestations.append(attestation) catch |err| { + aggregated_attestations.append(aggregated_attestation) catch |err| { std.debug.print( "fixture {s} case {s}{}: attestation #{} append failed: {s}\n", .{ fixture_path, case_name, formatStep(step_index), idx, @errorName(err) }, @@ -1295,7 +1337,7 @@ fn parseAttestations( }; } - return attestations; + return aggregated_attestations; }, else => { std.debug.print( diff --git a/pkgs/spectest/src/runner/state_transition_runner.zig b/pkgs/spectest/src/runner/state_transition_runner.zig index f52e3857..e043516f 100644 --- a/pkgs/spectest/src/runner/state_transition_runner.zig +++ b/pkgs/spectest/src/runner/state_transition_runner.zig @@ -466,7 +466,7 @@ fn buildBlock( } } - const attestations = try types.Attestations.init(allocator); + const attestations = try types.AggregatedAttestations.init(allocator); return types.BeamBlock{ .slot = slot, .proposer_index = proposer_index, diff --git a/pkgs/state-transition/src/mock.zig b/pkgs/state-transition/src/mock.zig index 3de354c5..db227fae 100644 --- a/pkgs/state-transition/src/mock.zig +++ b/pkgs/state-transition/src/mock.zig @@ -104,13 +104,13 @@ pub fn genMockChain(allocator: Allocator, numBlocks: usize, from_genesis: ?types const gen_signed_block = types.SignedBlockWithAttestation{ .message = gen_block_with_attestation, .signature = blk: { - var sigs = try types.BlockSignatures.init(allocator); + var signatures = try types.createBlockSignatures(allocator, gen_block_with_attestation.block.body.attestations.len()); const proposer_sig = try key_manager.signAttestation( &gen_block_with_attestation.proposer_attestation, allocator, ); - try sigs.append(proposer_sig); - break :blk sigs; + signatures.proposer_signature = proposer_sig; + break :blk signatures; }, }; var block_root: types.Root = undefined; @@ -276,6 +276,24 @@ pub fn genMockChain(allocator: Allocator, numBlocks: usize, from_genesis: ?types else => unreachable, } + var signed_attestations = std.ArrayList(types.SignedAttestation).init(allocator); + defer signed_attestations.deinit(); + + for (attestations.items) |attestation| { + const signature = try key_manager.signAttestation(&attestation, allocator); + try signed_attestations.append(.{ + .validator_id = attestation.validator_id, + .message = attestation.data, + .signature = signature, + }); + } + + var aggregation = try types.aggregateSignedAttestations(allocator, signed_attestations.items); + var agg_att_cleanup = true; + var agg_sig_cleanup = true; + errdefer if (agg_att_cleanup) aggregation.attestations.deinit(); + errdefer if (agg_sig_cleanup) aggregation.attestation_signatures.deinit(); + const proposer_index = slot % genesis_config.numValidators(); var block = types.BeamBlock{ .slot = slot, @@ -283,16 +301,10 @@ pub fn genMockChain(allocator: Allocator, numBlocks: usize, from_genesis: ?types .parent_root = parent_root, .state_root = state_root, .body = types.BeamBlockBody{ - // .execution_payload_header = .{ .timestamp = timestamp }, - .attestations = blk: { - var attestations_list = try types.Attestations.init(allocator); - for (attestations.items) |attestation| { - try attestations_list.append(attestation); - } - break :blk attestations_list; - }, + .attestations = aggregation.attestations, }, }; + agg_att_cleanup = false; // prepare pre state to process block for that slot, may be rename prepare_pre_state try transition.apply_raw_block(allocator, &beam_state, &block, block_building_logger); @@ -319,24 +331,20 @@ pub fn genMockChain(allocator: Allocator, numBlocks: usize, from_genesis: ?types }, }; - const signed_block = types.SignedBlockWithAttestation{ - .message = block_with_attestation, - .signature = blk: { - var sigs = try types.BlockSignatures.init(allocator); + const proposer_sig = try key_manager.signAttestation( + &block_with_attestation.proposer_attestation, + allocator, + ); - for (block.body.attestations.constSlice()) |attestation| { - const sig = try key_manager.signAttestation(&attestation, allocator); - try sigs.append(sig); - } - - const proposer_sig = try key_manager.signAttestation( - &block_with_attestation.proposer_attestation, - allocator, - ); - try sigs.append(proposer_sig); + const block_signatures = types.BlockSignatures{ + .attestation_signatures = aggregation.attestation_signatures, + .proposer_signature = proposer_sig, + }; + agg_sig_cleanup = false; - break :blk sigs; - }, + const signed_block = types.SignedBlockWithAttestation{ + .message = block_with_attestation, + .signature = block_signatures, }; try blockList.append(signed_block); try blockRootList.append(block_root); diff --git a/pkgs/state-transition/src/transition.zig b/pkgs/state-transition/src/transition.zig index f9c07117..ba7a602c 100644 --- a/pkgs/state-transition/src/transition.zig +++ b/pkgs/state-transition/src/transition.zig @@ -62,39 +62,50 @@ pub fn verifySignatures( signed_block: *const types.SignedBlockWithAttestation, ) !void { const attestations = signed_block.message.block.body.attestations.constSlice(); - const signatures = signed_block.signature.constSlice(); + const signature_groups = signed_block.signature.attestation_signatures.constSlice(); - // Must have exactly one signature per attestation plus one for proposer - if (attestations.len + 1 != signatures.len) { + if (attestations.len != signature_groups.len) { return StateTransitionError.InvalidBlockSignatures; } - // Verify all body attestations - for (attestations, 0..) |attestation, i| { - try verifySingleAttestation( - allocator, - state, - &attestation, - &signatures[i], - ); + for (attestations, 0..) |aggregated_attestation, i| { + var validator_indices = try types.aggregationBitsToValidatorIndices(&aggregated_attestation.aggregation_bits, allocator); + defer validator_indices.deinit(); + + const signatures = signature_groups[i].constSlice(); + if (validator_indices.items.len != signatures.len) { + return StateTransitionError.InvalidBlockSignatures; + } + + for (validator_indices.items, signatures) |validator_index, signature| { + try verifySingleAttestation( + allocator, + state, + validator_index, + &aggregated_attestation.data, + &signature, + ); + } } - // Verify proposer attestation (last signature in the list) + const proposer_attestation = signed_block.message.proposer_attestation; try verifySingleAttestation( allocator, state, - &signed_block.message.proposer_attestation, - &signatures[signatures.len - 1], + @intCast(proposer_attestation.validator_id), + &proposer_attestation.data, + &signed_block.signature.proposer_signature, ); } pub fn verifySingleAttestation( allocator: Allocator, state: *const types.BeamState, - attestation: *const types.Attestation, + validator_index: usize, + attestation_data: *const types.AttestationData, signatureBytes: *const types.SIGBYTES, ) !void { - const validatorIndex: usize = @intCast(attestation.validator_id); + const validatorIndex = validator_index; const validators = state.validators.constSlice(); if (validatorIndex >= validators.len) { return StateTransitionError.InvalidValidatorId; @@ -105,9 +116,9 @@ pub fn verifySingleAttestation( const verification_timer = zeam_metrics.lean_pq_signature_attestation_verification_time_seconds.start(); var message: [32]u8 = undefined; - try ssz.hashTreeRoot(types.Attestation, attestation.*, &message, allocator); + try ssz.hashTreeRoot(types.AttestationData, attestation_data.*, &message, allocator); - const epoch: u32 = @intCast(attestation.data.slot); + const epoch: u32 = @intCast(attestation_data.slot); try xmss.verifySsz(pubkey, &message, epoch, signatureBytes); _ = verification_timer.observe(); diff --git a/pkgs/testing/test_xmss_cycle.zig b/pkgs/testing/test_xmss_cycle.zig index 00200b3e..3ef07ff4 100644 --- a/pkgs/testing/test_xmss_cycle.zig +++ b/pkgs/testing/test_xmss_cycle.zig @@ -67,7 +67,7 @@ test "TestKeyManager: sign and verify attestation" { // Hash the attestation var message: [32]u8 = undefined; - try ssz.hashTreeRoot(types.Attestation, attestation, &message, allocator); + try ssz.hashTreeRoot(types.AttestationData, attestation.data, &message, allocator); // Verify try xmss.verifySsz( diff --git a/pkgs/types/src/attestation.zig b/pkgs/types/src/attestation.zig index 7598db1b..d8919620 100644 --- a/pkgs/types/src/attestation.zig +++ b/pkgs/types/src/attestation.zig @@ -40,7 +40,7 @@ fn freeJsonValue(val: *json.Value, allocator: Allocator) void { // Types pub const AggregationBits = ssz.utils.Bitlist(params.VALIDATOR_REGISTRY_LIMIT); -pub const AggregatedSignatures = ssz.utils.List(SIGBYTES, params.VALIDATOR_REGISTRY_LIMIT); +pub const NaiveAggregatedSignature = ssz.utils.List(SIGBYTES, params.VALIDATOR_REGISTRY_LIMIT); pub const AttestationData = struct { slot: Slot, @@ -48,6 +48,12 @@ pub const AttestationData = struct { target: Checkpoint, source: Checkpoint, + pub fn sszRoot(self: *const AttestationData, allocator: Allocator) !Root { + var root: Root = undefined; + try ssz.hashTreeRoot(AttestationData, self.*, &root, allocator); + return root; + } + pub fn toJson(self: *const AttestationData, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); try obj.put("slot", json.Value{ .integer = @as(i64, @intCast(self.slot)) }); @@ -83,11 +89,13 @@ pub const Attestation = struct { }; pub const SignedAttestation = struct { - message: Attestation, + validator_id: ValidatorIndex, + message: AttestationData, signature: SIGBYTES, pub fn toJson(self: *const SignedAttestation, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); + try obj.put("validator_id", json.Value{ .integer = @as(i64, @intCast(self.validator_id)) }); try obj.put("message", try self.message.toJson(allocator)); try obj.put("signature", json.Value{ .string = try bytesToHex(allocator, &self.signature) }); return json.Value{ .object = obj }; @@ -98,21 +106,28 @@ pub const SignedAttestation = struct { defer freeJsonValue(&json_value, allocator); return utils.jsonToString(allocator, json_value); } + + pub fn toAttestation(self: *const SignedAttestation) Attestation { + return .{ .validator_id = self.validator_id, .data = self.message }; + } }; pub const AggregatedAttestation = struct { - attestation_bits: AggregationBits, + aggregation_bits: AggregationBits, data: AttestationData, + pub fn deinit(self: *AggregatedAttestation) void { + self.aggregation_bits.deinit(); + } + pub fn toJson(self: *const AggregatedAttestation, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); - // Serialize attestation_bits as array of booleans var bits_array = json.Array.init(allocator); - for (0..self.attestation_bits.len()) |i| { - try bits_array.append(json.Value{ .bool = try self.attestation_bits.get(i) }); + for (0..self.aggregation_bits.len()) |i| { + try bits_array.append(json.Value{ .bool = try self.aggregation_bits.get(i) }); } - try obj.put("attestation_bits", json.Value{ .array = bits_array }); + try obj.put("aggregation_bits", json.Value{ .array = bits_array }); try obj.put("data", try self.data.toJson(allocator)); return json.Value{ .object = obj }; } @@ -126,13 +141,12 @@ pub const AggregatedAttestation = struct { pub const SignedAggregatedAttestation = struct { message: AggregatedAttestation, - signature: AggregatedSignatures, + signature: NaiveAggregatedSignature, pub fn toJson(self: *const SignedAggregatedAttestation, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); try obj.put("message", try self.message.toJson(allocator)); - // Serialize signature list as array of hex strings var sig_array = json.Array.init(allocator); errdefer sig_array.deinit(); @@ -150,36 +164,49 @@ pub const SignedAggregatedAttestation = struct { } }; +pub fn aggregationBitsEnsureLength(bits: *AggregationBits, target_len: usize) !void { + while (bits.len() < target_len) { + try bits.append(false); + } +} + +pub fn aggregationBitsSet(bits: *AggregationBits, index: usize, value: bool) !void { + try aggregationBitsEnsureLength(bits, index + 1); + try bits.set(index, value); +} + +pub fn aggregationBitsToValidatorIndices(bits: *const AggregationBits, allocator: Allocator) !std.ArrayList(usize) { + var indices = std.ArrayList(usize).init(allocator); + errdefer indices.deinit(); + + for (0..bits.len()) |validator_index| { + if (try bits.get(validator_index)) { + try indices.append(validator_index); + } + } + + return indices; +} + test "encode decode signed attestation roundtrip" { const signed_attestation = SignedAttestation{ + .validator_id = 0, .message = .{ - .validator_id = 0, - .data = .{ - .slot = 0, - .head = .{ - .root = ZERO_HASH, - .slot = 0, - }, - .target = .{ - .root = ZERO_HASH, - .slot = 0, - }, - .source = .{ - .root = ZERO_HASH, - .slot = 0, - }, - }, + .slot = 0, + .head = .{ .root = ZERO_HASH, .slot = 0 }, + .target = .{ .root = ZERO_HASH, .slot = 0 }, + .source = .{ .root = ZERO_HASH, .slot = 0 }, }, .signature = ZERO_SIGBYTES, }; - // Encode var encoded = std.ArrayList(u8).init(std.testing.allocator); defer encoded.deinit(); try ssz.serialize(SignedAttestation, signed_attestation, &encoded); + 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) + // 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); @@ -189,18 +216,16 @@ test "encode decode signed attestation roundtrip" { defer std.testing.allocator.free(encoded_hex); try std.testing.expectEqualStrings(expected_value, encoded_hex); - // Decode var decoded: SignedAttestation = undefined; try ssz.deserialize(SignedAttestation, encoded.items[0..], &decoded, std.testing.allocator); - // Verify roundtrip - try std.testing.expect(decoded.message.validator_id == signed_attestation.message.validator_id); - try std.testing.expect(decoded.message.data.slot == signed_attestation.message.data.slot); - try std.testing.expect(decoded.message.data.head.slot == signed_attestation.message.data.head.slot); - try std.testing.expect(std.mem.eql(u8, &decoded.message.data.head.root, &signed_attestation.message.data.head.root)); - try std.testing.expect(decoded.message.data.target.slot == signed_attestation.message.data.target.slot); - try std.testing.expect(std.mem.eql(u8, &decoded.message.data.target.root, &signed_attestation.message.data.target.root)); - try std.testing.expect(decoded.message.data.source.slot == signed_attestation.message.data.source.slot); - try std.testing.expect(std.mem.eql(u8, &decoded.message.data.source.root, &signed_attestation.message.data.source.root)); + try std.testing.expect(decoded.validator_id == signed_attestation.validator_id); + try std.testing.expect(decoded.message.slot == signed_attestation.message.slot); + try std.testing.expect(decoded.message.head.slot == signed_attestation.message.head.slot); + try std.testing.expect(std.mem.eql(u8, &decoded.message.head.root, &signed_attestation.message.head.root)); + try std.testing.expect(decoded.message.target.slot == signed_attestation.message.target.slot); + try std.testing.expect(std.mem.eql(u8, &decoded.message.target.root, &signed_attestation.message.target.root)); + try std.testing.expect(decoded.message.source.slot == signed_attestation.message.source.slot); + try std.testing.expect(std.mem.eql(u8, &decoded.message.source.root, &signed_attestation.message.source.root)); try std.testing.expect(std.mem.eql(u8, &decoded.signature, &signed_attestation.signature)); } diff --git a/pkgs/types/src/block.zig b/pkgs/types/src/block.zig index 78585f34..07ae04ad 100644 --- a/pkgs/types/src/block.zig +++ b/pkgs/types/src/block.zig @@ -9,13 +9,18 @@ const state = @import("./state.zig"); const utils = @import("./utils.zig"); const Allocator = std.mem.Allocator; +const AggregatedAttestation = attestation.AggregatedAttestation; +pub const AggregatedAttestations = ssz.utils.List(AggregatedAttestation, params.VALIDATOR_REGISTRY_LIMIT); const Attestation = attestation.Attestation; +pub const AttestationSignatures = ssz.utils.List(attestation.NaiveAggregatedSignature, params.VALIDATOR_REGISTRY_LIMIT); const Slot = utils.Slot; const ValidatorIndex = utils.ValidatorIndex; const Bytes32 = utils.Bytes32; const SIGBYTES = utils.SIGBYTES; +const SIGSIZE = utils.SIGSIZE; const Root = utils.Root; const ZERO_HASH = utils.ZERO_HASH; +const ZERO_SIGBYTES = utils.ZERO_SIGBYTES; const bytesToHex = utils.BytesToHex; const json = std.json; @@ -41,21 +46,22 @@ fn freeJsonValue(val: *json.Value, allocator: Allocator) void { } // Types -pub const Attestations = ssz.utils.List(attestation.Attestation, params.VALIDATOR_REGISTRY_LIMIT); -pub const BlockSignatures = ssz.utils.List(SIGBYTES, params.VALIDATOR_REGISTRY_LIMIT); - pub const BeamBlockBody = struct { - attestations: Attestations, + attestations: AggregatedAttestations, pub fn deinit(self: *BeamBlockBody) void { + for (self.attestations.slice()) |*att| { + att.deinit(); + } self.attestations.deinit(); } pub fn toJson(self: *const BeamBlockBody, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); - // Serialize attestations list var attestations_array = json.Array.init(allocator); + errdefer attestations_array.deinit(); + for (self.attestations.constSlice()) |att| { try attestations_array.append(try att.toJson(allocator)); } @@ -118,7 +124,7 @@ pub const BeamBlock = struct { const Self = @This(); pub fn setToDefault(self: *Self, allocator: Allocator) !void { - const attestations = try Attestations.init(allocator); + const attestations = try AggregatedAttestations.init(allocator); errdefer attestations.deinit(); self.* = .{ @@ -141,17 +147,15 @@ pub const BeamBlock = struct { allocator, ); - const header = BeamBlockHeader{ + return BeamBlockHeader{ .slot = self.slot, .proposer_index = self.proposer_index, .parent_root = self.parent_root, .state_root = self.state_root, .body_root = body_root, }; - return header; } - // computing latest block header to be assigned to the state for processing the block pub fn blockToLatestBlockHeader(self: *const Self, allocator: Allocator, header: *BeamBlockHeader) !void { var body_root: [32]u8 = undefined; try ssz.hashTreeRoot( @@ -185,7 +189,49 @@ pub const BeamBlock = struct { } pub fn toJsonString(self: *const BeamBlock, allocator: Allocator) ![]const u8 { - const json_value = try self.toJson(allocator); + var json_value = try self.toJson(allocator); + defer freeJsonValue(&json_value, allocator); + return utils.jsonToString(allocator, json_value); + } +}; + +pub const BlockSignatures = struct { + proposer_signature: SIGBYTES, + attestation_signatures: AttestationSignatures, + + pub fn deinit(self: *BlockSignatures) void { + for (self.attestation_signatures.slice()) |*group| { + group.deinit(); + } + self.attestation_signatures.deinit(); + } + + pub fn toJson(self: *const BlockSignatures, allocator: Allocator) !json.Value { + var obj = json.ObjectMap.init(allocator); + + var groups_array = json.Array.init(allocator); + errdefer groups_array.deinit(); + + for (self.attestation_signatures.constSlice()) |group| { + var sig_array = json.Array.init(allocator); + var success = false; + defer if (!success) sig_array.deinit(); + + for (group.constSlice()) |sig| { + try sig_array.append(json.Value{ .string = try bytesToHex(allocator, &sig) }); + } + + try groups_array.append(json.Value{ .array = sig_array }); + success = true; + } + + try obj.put("attestation_signatures", json.Value{ .array = groups_array }); + try obj.put("proposer_signature", json.Value{ .string = try bytesToHex(allocator, &self.proposer_signature) }); + return json.Value{ .object = obj }; + } + + pub fn toJsonString(self: *const BlockSignatures, allocator: Allocator) ![]const u8 { + var json_value = try self.toJson(allocator); defer freeJsonValue(&json_value, allocator); return utils.jsonToString(allocator, json_value); } @@ -203,7 +249,6 @@ pub const BlockWithAttestation = struct { var obj = json.ObjectMap.init(allocator); try obj.put("block", try self.block.toJson(allocator)); try obj.put("proposer_attestation", try self.proposer_attestation.toJson(allocator)); - return json.Value{ .object = obj }; } @@ -226,14 +271,7 @@ pub const SignedBlockWithAttestation = struct { pub fn toJson(self: *const SignedBlockWithAttestation, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); try obj.put("message", try self.message.toJson(allocator)); - - // Serialize signatures list as array of hex strings - var sig_array = json.Array.init(allocator); - for (self.signature.constSlice()) |sig| { - try sig_array.append(json.Value{ .string = try bytesToHex(allocator, &sig) }); - } - try obj.put("signatures", json.Value{ .array = sig_array }); - + try obj.put("signature", try self.signature.toJson(allocator)); return json.Value{ .object = obj }; } @@ -244,23 +282,115 @@ pub const SignedBlockWithAttestation = struct { } }; -// Creates a BlockSignatures list with zero signatures for all attestations plus the proposer attestation -pub fn createBlockSignatures(allocator: Allocator, num_attestations: usize) !BlockSignatures { - var signatures = try BlockSignatures.init(allocator); - // +1 for proposer attestation - for (0..(num_attestations + 1)) |_| { - try signatures.append(utils.ZERO_SIGBYTES); +pub fn createBlockSignatures(allocator: Allocator, num_aggregated_attestations: usize) !BlockSignatures { + var groups = try AttestationSignatures.init(allocator); + errdefer groups.deinit(); + + for (0..num_aggregated_attestations) |_| { + const signatures = try attestation.NaiveAggregatedSignature.init(allocator); + try groups.append(signatures); } - return signatures; + + return .{ + .attestation_signatures = groups, + .proposer_signature = utils.ZERO_SIGBYTES, + }; +} + +const AggregationGroup = struct { + data: attestation.AttestationData, + bits: attestation.AggregationBits, + signatures: attestation.NaiveAggregatedSignature, + + fn init(allocator: Allocator, signed_attestation: attestation.SignedAttestation) !AggregationGroup { + var bits = try attestation.AggregationBits.init(allocator); + errdefer bits.deinit(); + + const validator_index: usize = @intCast(signed_attestation.validator_id); + try attestation.aggregationBitsSet(&bits, validator_index, true); + + var signatures = try attestation.NaiveAggregatedSignature.init(allocator); + errdefer signatures.deinit(); + try signatures.append(signed_attestation.signature); + + return AggregationGroup{ + .data = signed_attestation.message, + .bits = bits, + .signatures = signatures, + }; + } +}; + +pub const AggregatedAttestationsResult = struct { + attestations: AggregatedAttestations, + attestation_signatures: AttestationSignatures, + + pub fn deinit(self: *AggregatedAttestationsResult) void { + for (self.attestations.slice()) |*att| { + att.deinit(); + } + self.attestations.deinit(); + + for (self.attestation_signatures.slice()) |*sig_group| { + sig_group.deinit(); + } + self.attestation_signatures.deinit(); + } +}; + +pub fn aggregateSignedAttestations( + allocator: Allocator, + signed_attestations: []const attestation.SignedAttestation, +) !AggregatedAttestationsResult { + var aggregated_attestations = try AggregatedAttestations.init(allocator); + errdefer aggregated_attestations.deinit(); + + var attestation_signatures = try AttestationSignatures.init(allocator); + errdefer attestation_signatures.deinit(); + + var groups = std.ArrayList(AggregationGroup).init(allocator); + defer groups.deinit(); + errdefer for (groups.items) |*group| { + group.bits.deinit(); + group.signatures.deinit(); + }; + + var root_indices = std.AutoHashMap(Root, usize).init(allocator); + defer root_indices.deinit(); + + for (signed_attestations) |signed_attestation| { + const root = try signed_attestation.message.sszRoot(allocator); + if (root_indices.get(root)) |group_index| { + var group = &groups.items[group_index]; + const validator_index: usize = @intCast(signed_attestation.validator_id); + try attestation.aggregationBitsSet(&group.bits, validator_index, true); + try group.signatures.append(signed_attestation.signature); + } else { + const new_group = try AggregationGroup.init(allocator, signed_attestation); + try groups.append(new_group); + const inserted_index = groups.items.len - 1; + try root_indices.put(root, inserted_index); + } + } + + for (groups.items) |group| { + try aggregated_attestations.append(.{ .aggregation_bits = group.bits, .data = group.data }); + try attestation_signatures.append(group.signatures); + } + + return .{ + .attestations = aggregated_attestations, + .attestation_signatures = attestation_signatures, + }; } -// some p2p containers pub const BlockByRootRequest = struct { roots: ssz.utils.List(utils.Root, params.MAX_REQUEST_BLOCKS), pub fn toJson(self: *const BlockByRootRequest, allocator: Allocator) !json.Value { var obj = json.ObjectMap.init(allocator); var roots_array = json.Array.init(allocator); + errdefer roots_array.deinit(); for (self.roots.constSlice()) |root| { try roots_array.append(json.Value{ .string = try bytesToHex(allocator, &root) }); } @@ -299,13 +429,12 @@ pub const ProtoBlock = struct { } pub fn toJsonString(self: *const ProtoBlock, allocator: Allocator) ![]const u8 { - const json_value = try self.toJson(allocator); + var json_value = try self.toJson(allocator); defer freeJsonValue(&json_value, allocator); return utils.jsonToString(allocator, json_value); } }; -// basic payload header for some sort of APS pub const ExecutionPayloadHeader = struct { timestamp: u64, @@ -316,14 +445,17 @@ pub const ExecutionPayloadHeader = struct { } pub fn toJsonString(self: *const ExecutionPayloadHeader, allocator: Allocator) ![]const u8 { - const json_value = try self.toJson(allocator); + var json_value = try self.toJson(allocator); defer json_value.object.deinit(); return utils.jsonToString(allocator, json_value); } }; test "ssz seralize/deserialize signed beam block" { - var attestations = try Attestations.init(std.testing.allocator); + var attestations = try AggregatedAttestations.init(std.testing.allocator); + + var signatures = try createBlockSignatures(std.testing.allocator, attestations.len()); + errdefer signatures.deinit(); var signed_block = SignedBlockWithAttestation{ .message = .{ @@ -332,57 +464,37 @@ test "ssz seralize/deserialize signed beam block" { .proposer_index = 3, .parent_root = [_]u8{ 199, 128, 9, 253, 240, 127, 197, 106, 17, 241, 34, 55, 6, 88, 163, 83, 170, 165, 66, 237, 99, 228, 76, 75, 193, 95, 244, 205, 16, 90, 179, 60 }, .state_root = [_]u8{ 81, 12, 244, 147, 45, 160, 28, 192, 208, 78, 159, 151, 165, 43, 244, 44, 103, 197, 231, 128, 122, 15, 182, 90, 109, 10, 229, 68, 229, 60, 50, 231 }, - .body = .{ - // - // .execution_payload_header = ExecutionPayloadHeader{ .timestamp = 23 }, - .attestations = attestations, - }, + .body = .{ .attestations = attestations }, }, .proposer_attestation = .{ .validator_id = 3, .data = .{ .slot = 9, - .head = .{ - .slot = 9, - .root = [_]u8{1} ** 32, - }, - .source = .{ - .slot = 0, - .root = [_]u8{0} ** 32, - }, - .target = .{ - .slot = 9, - .root = [_]u8{1} ** 32, - }, + .head = .{ .slot = 9, .root = [_]u8{1} ** 32 }, + .source = .{ .slot = 0, .root = [_]u8{0} ** 32 }, + .target = .{ .slot = 9, .root = [_]u8{1} ** 32 }, }, }, }, - .signature = try createBlockSignatures(std.testing.allocator, attestations.len()), + .signature = signatures, }; defer signed_block.deinit(); - // check BlockWithAttestation serialization/deserialization var serialized_signed_block = std.ArrayList(u8).init(std.testing.allocator); defer serialized_signed_block.deinit(); + try ssz.serialize(SignedBlockWithAttestation, signed_block, &serialized_signed_block); - std.debug.print("\n\n\nserialized_signed_block ({d})", .{serialized_signed_block.items.len}); + try std.testing.expect(serialized_signed_block.items.len > 0); var deserialized_signed_block: SignedBlockWithAttestation = undefined; try ssz.deserialize(SignedBlockWithAttestation, serialized_signed_block.items[0..], &deserialized_signed_block, std.testing.allocator); defer deserialized_signed_block.deinit(); - // try std.testing.expect(signed_block.message.body.execution_payload_header.timestamp == deserialized_signed_block.message.body.execution_payload_header.timestamp); try std.testing.expect(std.mem.eql(u8, &signed_block.message.block.state_root, &deserialized_signed_block.message.block.state_root)); try std.testing.expect(std.mem.eql(u8, &signed_block.message.block.parent_root, &deserialized_signed_block.message.block.parent_root)); - // successful merklization var block_root: [32]u8 = undefined; - try ssz.hashTreeRoot( - BeamBlock, - signed_block.message.block, - &block_root, - std.testing.allocator, - ); + try ssz.hashTreeRoot(BeamBlock, signed_block.message.block, &block_root, std.testing.allocator); } test "blockToLatestBlockHeader and blockToHeader" { @@ -391,22 +503,16 @@ test "blockToLatestBlockHeader and blockToHeader" { .proposer_index = 3, .parent_root = [_]u8{ 199, 128, 9, 253, 240, 127, 197, 106, 17, 241, 34, 55, 6, 88, 163, 83, 170, 165, 66, 237, 99, 228, 76, 75, 193, 95, 244, 205, 16, 90, 179, 60 }, .state_root = [_]u8{ 81, 12, 244, 147, 45, 160, 28, 192, 208, 78, 159, 151, 165, 43, 244, 44, 103, 197, 231, 128, 122, 15, 182, 90, 109, 10, 229, 68, 229, 60, 50, 231 }, - .body = .{ - // - // .execution_payload_header = ExecutionPayloadHeader{ .timestamp = 23 }, - .attestations = try Attestations.init(std.testing.allocator), - }, + .body = .{ .attestations = try AggregatedAttestations.init(std.testing.allocator) }, }; defer block.deinit(); - // test blockToLatestBlockHeader var lastest_block_header: BeamBlockHeader = undefined; try block.blockToLatestBlockHeader(std.testing.allocator, &lastest_block_header); try std.testing.expect(lastest_block_header.proposer_index == block.proposer_index); try std.testing.expect(std.mem.eql(u8, &block.parent_root, &lastest_block_header.parent_root)); try std.testing.expect(std.mem.eql(u8, &ZERO_HASH, &lastest_block_header.state_root)); - // test blockToHeader var block_header: BeamBlockHeader = try block.blockToHeader(std.testing.allocator); try std.testing.expect(block_header.proposer_index == block.proposer_index); try std.testing.expect(std.mem.eql(u8, &block.parent_root, &block_header.parent_root)); @@ -414,10 +520,10 @@ test "blockToLatestBlockHeader and blockToHeader" { } test "encode decode signed block with attestation roundtrip" { - var attestations = try Attestations.init(std.testing.allocator); + var attestations = try AggregatedAttestations.init(std.testing.allocator); errdefer attestations.deinit(); - var signatures = try BlockSignatures.init(std.testing.allocator); + var signatures = try createBlockSignatures(std.testing.allocator, attestations.len()); errdefer signatures.deinit(); var signed_block_with_attestation = SignedBlockWithAttestation{ @@ -427,26 +533,15 @@ test "encode decode signed block with attestation roundtrip" { .proposer_index = 0, .parent_root = ZERO_HASH, .state_root = ZERO_HASH, - .body = .{ - .attestations = attestations, - }, + .body = .{ .attestations = attestations }, }, .proposer_attestation = .{ .validator_id = 0, .data = .{ .slot = 0, - .head = .{ - .root = ZERO_HASH, - .slot = 0, - }, - .target = .{ - .root = ZERO_HASH, - .slot = 0, - }, - .source = .{ - .root = ZERO_HASH, - .slot = 0, - }, + .head = .{ .root = ZERO_HASH, .slot = 0 }, + .target = .{ .root = ZERO_HASH, .slot = 0 }, + .source = .{ .root = ZERO_HASH, .slot = 0 }, }, }, }, @@ -454,35 +549,80 @@ test "encode decode signed block with attestation roundtrip" { }; defer signed_block_with_attestation.deinit(); - // Encode var encoded = std.ArrayList(u8).init(std.testing.allocator); defer encoded.deinit(); try ssz.serialize(SignedBlockWithAttestation, signed_block_with_attestation, &encoded); - // Convert to hex and compare with expected value - const expected_value = "08000000ec0000008c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005400000004000000"; - 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: SignedBlockWithAttestation = undefined; try ssz.deserialize(SignedBlockWithAttestation, encoded.items[0..], &decoded, std.testing.allocator); defer decoded.deinit(); - // Verify roundtrip try std.testing.expect(decoded.message.block.slot == signed_block_with_attestation.message.block.slot); try std.testing.expect(decoded.message.block.proposer_index == signed_block_with_attestation.message.block.proposer_index); try std.testing.expect(std.mem.eql(u8, &decoded.message.block.parent_root, &signed_block_with_attestation.message.block.parent_root)); try std.testing.expect(std.mem.eql(u8, &decoded.message.block.state_root, &signed_block_with_attestation.message.block.state_root)); try std.testing.expect(decoded.message.proposer_attestation.validator_id == signed_block_with_attestation.message.proposer_attestation.validator_id); try std.testing.expect(decoded.message.proposer_attestation.data.slot == signed_block_with_attestation.message.proposer_attestation.data.slot); - try std.testing.expect(decoded.message.proposer_attestation.data.head.slot == signed_block_with_attestation.message.proposer_attestation.data.head.slot); try std.testing.expect(std.mem.eql(u8, &decoded.message.proposer_attestation.data.head.root, &signed_block_with_attestation.message.proposer_attestation.data.head.root)); - try std.testing.expect(decoded.message.proposer_attestation.data.target.slot == signed_block_with_attestation.message.proposer_attestation.data.target.slot); - try std.testing.expect(std.mem.eql(u8, &decoded.message.proposer_attestation.data.target.root, &signed_block_with_attestation.message.proposer_attestation.data.target.root)); - try std.testing.expect(decoded.message.proposer_attestation.data.source.slot == signed_block_with_attestation.message.proposer_attestation.data.source.slot); - try std.testing.expect(std.mem.eql(u8, &decoded.message.proposer_attestation.data.source.root, &signed_block_with_attestation.message.proposer_attestation.data.source.root)); - try std.testing.expect(decoded.signature.len() == signed_block_with_attestation.signature.len()); - try std.testing.expect(decoded.message.block.body.attestations.len() == signed_block_with_attestation.message.block.body.attestations.len()); + try std.testing.expect(decoded.signature.attestation_signatures.len() == signed_block_with_attestation.signature.attestation_signatures.len()); +} + +test "encode decode signed block with non-empty attestation signatures" { + var attestations = try AggregatedAttestations.init(std.testing.allocator); + errdefer attestations.deinit(); + + var attestation_signatures = try AttestationSignatures.init(std.testing.allocator); + errdefer attestation_signatures.deinit(); + + var validator_signatures = try attestation.NaiveAggregatedSignature.init(std.testing.allocator); + errdefer validator_signatures.deinit(); + + var test_sig1: SIGBYTES = undefined; + var test_sig2: SIGBYTES = undefined; + @memset(&test_sig1, 0x12); + @memset(&test_sig2, 0x34); + + try validator_signatures.append(test_sig1); + try validator_signatures.append(test_sig2); + + try attestation_signatures.append(validator_signatures); + + var signed_block_with_attestation = SignedBlockWithAttestation{ + .message = .{ + .block = .{ + .slot = 1, + .proposer_index = 0, + .parent_root = ZERO_HASH, + .state_root = ZERO_HASH, + .body = .{ .attestations = attestations }, + }, + .proposer_attestation = .{ + .validator_id = 0, + .data = .{ + .slot = 1, + .head = .{ .root = ZERO_HASH, .slot = 1 }, + .target = .{ .root = ZERO_HASH, .slot = 1 }, + .source = .{ .root = ZERO_HASH, .slot = 0 }, + }, + }, + }, + .signature = .{ + .attestation_signatures = attestation_signatures, + .proposer_signature = ZERO_SIGBYTES, + }, + }; + defer signed_block_with_attestation.deinit(); + + var encoded = std.ArrayList(u8).init(std.testing.allocator); + defer encoded.deinit(); + try ssz.serialize(SignedBlockWithAttestation, signed_block_with_attestation, &encoded); + + var decoded: SignedBlockWithAttestation = undefined; + try ssz.deserialize(SignedBlockWithAttestation, encoded.items[0..], &decoded, std.testing.allocator); + defer decoded.deinit(); + + try std.testing.expect(decoded.message.block.slot == signed_block_with_attestation.message.block.slot); + try std.testing.expect(decoded.signature.attestation_signatures.len() == 1); + const decoded_group = try decoded.signature.attestation_signatures.get(0); + try std.testing.expect(decoded_group.len() == 2); } diff --git a/pkgs/types/src/lib.zig b/pkgs/types/src/lib.zig index 8245a070..c57b5fbf 100644 --- a/pkgs/types/src/lib.zig +++ b/pkgs/types/src/lib.zig @@ -7,18 +7,24 @@ pub const BeamBlockHeader = block.BeamBlockHeader; pub const BeamBlockBody = block.BeamBlockBody; pub const BlockWithAttestation = block.BlockWithAttestation; pub const SignedBlockWithAttestation = block.SignedBlockWithAttestation; -pub const Attestations = block.Attestations; +pub const AggregatedAttestations = block.AggregatedAttestations; +pub const AggregatedAttestationsResult = block.AggregatedAttestationsResult; +pub const AttestationSignatures = block.AttestationSignatures; pub const BlockSignatures = block.BlockSignatures; +pub const aggregateSignedAttestations = block.aggregateSignedAttestations; pub const createBlockSignatures = block.createBlockSignatures; const attestation = @import("./attestation.zig"); pub const AggregationBits = attestation.AggregationBits; -pub const AggregatedSignatures = attestation.AggregatedSignatures; +pub const NaiveAggregatedSignature = attestation.NaiveAggregatedSignature; pub const AttestationData = attestation.AttestationData; pub const Attestation = attestation.Attestation; pub const SignedAttestation = attestation.SignedAttestation; pub const AggregatedAttestation = attestation.AggregatedAttestation; pub const SignedAggregatedAttestation = attestation.SignedAggregatedAttestation; +pub const aggregationBitsEnsureLength = attestation.aggregationBitsEnsureLength; +pub const aggregationBitsSet = attestation.aggregationBitsSet; +pub const aggregationBitsToValidatorIndices = attestation.aggregationBitsToValidatorIndices; const state = @import("./state.zig"); pub const BeamStateConfig = state.BeamStateConfig; diff --git a/pkgs/types/src/state.zig b/pkgs/types/src/state.zig index 9f399d4f..36511306 100644 --- a/pkgs/types/src/state.zig +++ b/pkgs/types/src/state.zig @@ -6,12 +6,13 @@ const zeam_utils = @import("@zeam/utils"); const zeam_metrics = @import("@zeam/metrics"); const block = @import("./block.zig"); +const attestation = @import("./attestation.zig"); const utils = @import("./utils.zig"); const mini_3sf = @import("./mini_3sf.zig"); const validator = @import("./validator.zig"); const Allocator = std.mem.Allocator; -const Attestations = block.Attestations; +const AggregatedAttestations = block.AggregatedAttestations; const BeamBlock = block.BeamBlock; const BeamBlockHeader = block.BeamBlockHeader; const Root = utils.Root; @@ -274,6 +275,7 @@ pub const BeamState = struct { // start block processing try self.process_block_header(allocator, staged_block, logger); + // PQ devner-0 has no execution // try process_execution_payload_header(state, block); try self.process_operations(allocator, staged_block, logger); @@ -284,7 +286,7 @@ pub const BeamState = struct { try self.process_attestations(allocator, staged_block.body.attestations, logger); } - fn process_attestations(self: *Self, allocator: Allocator, attestations: Attestations, logger: zeam_utils.ModuleLogger) !void { + fn process_attestations(self: *Self, allocator: Allocator, attestations: AggregatedAttestations, logger: zeam_utils.ModuleLogger) !void { const attestations_timer = zeam_metrics.lean_state_transition_attestations_processing_time_seconds.start(); defer _ = attestations_timer.observe(); @@ -317,16 +319,22 @@ pub const BeamState = struct { // need to cast to usize for slicing ops but does this makes the STF target arch dependent? const num_validators: usize = @intCast(self.validatorCount()); - for (attestations.constSlice()) |attestation| { - const validator_id: usize = @intCast(attestation.validator_id); - const attestation_data = attestation.data; + for (attestations.constSlice()) |aggregated_attestation| { + var validator_indices = try attestation.aggregationBitsToValidatorIndices(&aggregated_attestation.aggregation_bits, allocator); + defer validator_indices.deinit(); + + if (validator_indices.items.len == 0) { + continue; + } + + const attestation_data = aggregated_attestation.data; // check if attestation is sane const source_slot: usize = @intCast(attestation_data.source.slot); const target_slot: usize = @intCast(attestation_data.target.slot); const attestation_str = try attestation_data.toJsonString(allocator); defer allocator.free(attestation_str); - logger.debug("processing attestation={s} validator_id={d}\n....\n", .{ attestation_str, validator_id }); + logger.debug("processing attestation={s} validators={any}\n....\n", .{ attestation_str, validator_indices.items }); if (source_slot >= self.justified_slots.len()) { return StateTransitionError.InvalidSlotIndex; @@ -343,8 +351,11 @@ pub const BeamState = struct { const is_source_justified = try self.justified_slots.get(source_slot); const is_target_already_justified = try self.justified_slots.get(target_slot); - const has_correct_source_root = std.mem.eql(u8, &attestation_data.source.root, &(try self.historical_block_hashes.get(source_slot))); - const has_correct_target_root = std.mem.eql(u8, &attestation_data.target.root, &(try self.historical_block_hashes.get(target_slot))); + const stored_source_root = try self.historical_block_hashes.get(source_slot); + const stored_target_root = try self.historical_block_hashes.get(target_slot); + const has_correct_source_root = std.mem.eql(u8, &attestation_data.source.root, &stored_source_root); + const has_correct_target_root = std.mem.eql(u8, &attestation_data.target.root, &stored_target_root); + const has_known_root = has_correct_source_root and has_correct_target_root; const target_not_ahead = target_slot <= source_slot; const is_target_justifiable = try utils.IsJustifiableSlot(self.latest_finalized.slot, target_slot); @@ -352,26 +363,20 @@ pub const BeamState = struct { // not present in 3sf mini but once a target is justified no need to run loop // as we remove the target from justifications map as soon as its justified is_target_already_justified or - !has_correct_source_root or - !has_correct_target_root or + !has_known_root or target_not_ahead or !is_target_justifiable) { - logger.debug("skipping the attestation as not viable: !(source_justified={}) or target_already_justified={} !(correct_source_root={}) or !(correct_target_root={}) or target_not_ahead={} or !(target_justifiable={})", .{ + logger.debug("skipping the attestation as not viable: !(source_justified={}) or target_already_justified={} !(known_root={}) or target_not_ahead={} or !(target_justifiable={})", .{ is_source_justified, is_target_already_justified, - has_correct_source_root, - has_correct_target_root, + has_known_root, target_not_ahead, is_target_justifiable, }); continue; } - if (validator_id >= num_validators) { - return StateTransitionError.InvalidValidatorId; - } - var target_justifications = justifications.get(attestation_data.target.root) orelse targetjustifications: { var targetjustifications = try allocator.alloc(u8, num_validators); for (0..targetjustifications.len) |i| { @@ -381,7 +386,12 @@ pub const BeamState = struct { break :targetjustifications targetjustifications; }; - target_justifications[validator_id] = 1; + for (validator_indices.items) |validator_index| { + if (validator_index >= num_validators) { + return StateTransitionError.InvalidValidatorId; + } + target_justifications[validator_index] = 1; + } try justifications.put(allocator, attestation_data.target.root, target_justifications); var target_justifications_count: usize = 0; for (target_justifications) |justified| { diff --git a/pkgs/types/src/utils.zig b/pkgs/types/src/utils.zig index eba66b6e..eb29ee5e 100644 --- a/pkgs/types/src/utils.zig +++ b/pkgs/types/src/utils.zig @@ -27,7 +27,7 @@ pub const RootHex = [64]u8; pub const ZERO_HASH = [_]u8{0x00} ** 32; pub const ZERO_SIGBYTES = [_]u8{0} ** SIGSIZE; -pub const StateTransitionError = error{ InvalidParentRoot, InvalidPreState, InvalidPostState, InvalidExecutionPayloadHeaderTimestamp, InvalidJustifiableSlot, InvalidValidatorId, InvalidBlockSignatures, InvalidLatestBlockHeader, InvalidProposer, InvalidJustificationIndex, InvalidSlotIndex }; +pub const StateTransitionError = error{ InvalidParentRoot, InvalidPreState, InvalidPostState, InvalidExecutionPayloadHeaderTimestamp, InvalidJustifiableSlot, InvalidValidatorId, InvalidBlockSignatures, InvalidLatestBlockHeader, InvalidProposer, InvalidJustificationIndex, InvalidSlotIndex, DuplicateAttestationData }; const json = std.json; diff --git a/pkgs/types/src/zk.zig b/pkgs/types/src/zk.zig index 453c1518..1fe37dd9 100644 --- a/pkgs/types/src/zk.zig +++ b/pkgs/types/src/zk.zig @@ -136,7 +136,7 @@ test "ssz seralize/deserialize signed stf prover input" { }; defer test_state.deinit(); - var attestations = try block.Attestations.init(std.testing.allocator); + const attestations = try block.AggregatedAttestations.init(std.testing.allocator); var test_block = block.SignedBlockWithAttestation{ .message = .{