Skip to content

Commit 2cf6ef5

Browse files
authored
feat: sync committee contribution (#1284)
1 parent 8af3687 commit 2cf6ef5

File tree

15 files changed

+795
-155
lines changed

15 files changed

+795
-155
lines changed

Makefile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ KURTOSIS_GRAFANA_DASHBOARDS_DIR ?= $(KURTOSIS_DIR)/static_files/grafana-config/d
5252
KURTOSIS_COOKIE ?= secret
5353
# Name of the kurtosis service pointing to the lambdaconsesus node
5454
KURTOSIS_SERVICE ?= cl-3-lambda-geth
55+
# Name of the enclave to be used with kurtosis
56+
KURTOSIS_ENCLAVE ?= lambdanet
5557

5658
##### TARGETS #####
5759

@@ -75,18 +77,18 @@ kurtosis.setup.lambdaconsensus:
7577

7678
#💻 kurtosis.start: @ Starts the kurtosis environment
7779
kurtosis.start:
78-
kurtosis run --enclave lambdanet $(KURTOSIS_DIR) --args-file network_params.yaml
80+
kurtosis run --enclave $(KURTOSIS_ENCLAVE) $(KURTOSIS_DIR) --args-file network_params.yaml
7981

8082
#💻 kurtosis.build-and-start: @ Builds the lambdaconsensus Docker image and starts the kurtosis environment.
8183
kurtosis.clean-start: kurtosis.clean kurtosis.setup.lambdaconsensus kurtosis.start
8284

8385
#💻 kurtosis.stop: @ Stops the kurtosis environment
8486
kurtosis.stop:
85-
kurtosis enclave stop lambdanet
87+
kurtosis enclave stop $(KURTOSIS_ENCLAVE)
8688

8789
#💻 kurtosis.remove: @ Removes the kurtosis environment
8890
kurtosis.remove:
89-
kurtosis enclave rm lambdanet
91+
kurtosis enclave rm $(KURTOSIS_ENCLAVE)
9092

9193
#💻 kurtosis.clean: @ Clean the kurtosis environment
9294
kurtosis.clean:
@@ -97,7 +99,7 @@ kurtosis.purge: kurtosis.stop kurtosis.remove kurtosis.clean
9799

98100
#💻 kurtosis.connect: @ Connects to the client running in kurtosis, KURTOSIS_SERVICE could be given
99101
kurtosis.connect:
100-
kurtosis service shell lambdanet $(KURTOSIS_SERVICE)
102+
kurtosis service shell $(KURTOSIS_ENCLAVE) $(KURTOSIS_SERVICE)
101103

102104
#💻 kurtosis.connect.iex: @ Connects to iex ONCE INSIDE THE KURTOSIS SERVICE
103105
kurtosis.connect.iex:

lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do
99
alias LambdaEthereumConsensus.P2P
1010
alias LambdaEthereumConsensus.P2P.Gossip.Handler
1111
alias LambdaEthereumConsensus.StateTransition.Misc
12-
alias Types.SubnetInfo
12+
alias Types.AttSubnetInfo
1313

1414
@behaviour Handler
1515

@@ -36,7 +36,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do
3636
# TODO: validate before accepting
3737
Libp2pPort.validate_message(msg_id, :accept)
3838

39-
SubnetInfo.add_attestation!(subnet_id, attestation)
39+
AttSubnetInfo.add_attestation!(subnet_id, attestation)
4040
else
4141
{:error, _} -> Libp2pPort.validate_message(msg_id, :reject)
4242
end
@@ -70,18 +70,18 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do
7070
@spec collect(non_neg_integer(), Types.Attestation.t()) :: :ok
7171
def collect(subnet_id, attestation) do
7272
join(subnet_id)
73-
SubnetInfo.new_subnet_with_attestation(subnet_id, attestation)
73+
AttSubnetInfo.new_subnet_with_attestation(subnet_id, attestation)
7474
Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__)
7575
end
7676

7777
@spec stop_collecting(non_neg_integer()) ::
7878
{:ok, list(Types.Attestation.t())} | {:error, String.t()}
7979
def stop_collecting(subnet_id) do
80-
# TODO: implement some way to unsubscribe without leaving the topic
80+
# TODO: (#1289) implement some way to unsubscribe without leaving the topic
8181
topic = topic(subnet_id)
8282
Libp2pPort.leave_topic(topic)
8383
Libp2pPort.join_topic(topic)
84-
SubnetInfo.stop_collecting(subnet_id)
84+
AttSubnetInfo.stop_collecting(subnet_id)
8585
end
8686

8787
defp topic(subnet_id) do

lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,125 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do
22
@moduledoc """
33
This module handles sync committee from specific gossip subnets.
44
Used by validators to fulfill aggregation duties.
5+
6+
TODO: This module borrows almost all of its logic from Attestation,
7+
this could be refactored to a common module if needed in the future.
58
"""
69
alias LambdaEthereumConsensus.ForkChoice
710
alias LambdaEthereumConsensus.Libp2pPort
11+
alias LambdaEthereumConsensus.P2P
12+
alias LambdaEthereumConsensus.P2P.Gossip.Handler
13+
alias LambdaEthereumConsensus.StateTransition.Misc
14+
alias Types.SyncSubnetInfo
15+
16+
@behaviour Handler
817

918
require Logger
1019

20+
@spec join([non_neg_integer()]) :: :ok
21+
def join(subnet_ids) when is_list(subnet_ids) do
22+
for subnet_id <- subnet_ids do
23+
topic = topic(subnet_id)
24+
Libp2pPort.join_topic(topic)
25+
26+
P2P.Metadata.set_syncnet(subnet_id)
27+
end
28+
29+
P2P.Metadata.get_metadata()
30+
|> update_enr()
31+
end
32+
33+
@impl true
34+
def handle_gossip_message(store, topic, msg_id, message) do
35+
handle_gossip_message(topic, msg_id, message)
36+
store
37+
end
38+
39+
def handle_gossip_message(topic, msg_id, message) do
40+
subnet_id = extract_subnet_id(topic)
41+
42+
with {:ok, uncompressed} <- :snappyer.decompress(message),
43+
{:ok, sync_committee_msg} <- Ssz.from_ssz(uncompressed, Types.SyncCommitteeMessage) do
44+
# TODO: validate before accepting
45+
Libp2pPort.validate_message(msg_id, :accept)
46+
47+
SyncSubnetInfo.add_message!(subnet_id, sync_committee_msg)
48+
else
49+
{:error, _} -> Libp2pPort.validate_message(msg_id, :reject)
50+
end
51+
end
52+
1153
@spec publish(Types.SyncCommitteeMessage.t(), [non_neg_integer()]) :: :ok
1254
def publish(%Types.SyncCommitteeMessage{} = sync_committee_msg, subnet_ids) do
13-
Enum.each(subnet_ids, fn subnet_id ->
55+
for subnet_id <- subnet_ids do
1456
topic = topic(subnet_id)
1557

1658
{:ok, encoded} = SszEx.encode(sync_committee_msg, Types.SyncCommitteeMessage)
1759
{:ok, message} = :snappyer.compress(encoded)
1860
Libp2pPort.publish(topic, message)
19-
end)
61+
end
62+
63+
:ok
64+
end
65+
66+
@spec publish_contribution(Types.SignedContributionAndProof.t()) :: :ok
67+
def publish_contribution(%Types.SignedContributionAndProof{} = signed_contribution) do
68+
fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower)
69+
topic = "/eth2/#{fork_context}/sync_committee_contribution_and_proof/ssz_snappy"
70+
{:ok, encoded} = SszEx.encode(signed_contribution, Types.SignedContributionAndProof)
71+
{:ok, message} = :snappyer.compress(encoded)
72+
Libp2pPort.publish(topic, message)
73+
end
74+
75+
@spec collect([non_neg_integer()], Types.SyncCommitteeMessage.t()) :: :ok
76+
def collect(subnet_ids, message) do
77+
join(subnet_ids)
78+
79+
for subnet_id <- subnet_ids do
80+
SyncSubnetInfo.new_subnet_with_message(subnet_id, message)
81+
Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__)
82+
end
83+
84+
:ok
85+
end
86+
87+
@spec stop_collecting(non_neg_integer()) ::
88+
{:ok, list(Types.SyncCommitteeMessage.t())} | {:error, String.t()}
89+
def stop_collecting(subnet_id) do
90+
# TODO: (#1289) implement some way to unsubscribe without leaving the topic
91+
topic = topic(subnet_id)
92+
Libp2pPort.leave_topic(topic)
93+
Libp2pPort.join_topic(topic)
94+
SyncSubnetInfo.stop_collecting(subnet_id)
2095
end
2196

2297
defp topic(subnet_id) do
2398
# TODO: this doesn't take into account fork digest changes
2499
fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower)
25100
"/eth2/#{fork_context}/sync_committee_#{subnet_id}/ssz_snappy"
26101
end
102+
103+
defp update_enr(%{attnets: attnets, syncnets: syncnets}) do
104+
enr_fork_id = compute_enr_fork_id()
105+
Libp2pPort.update_enr(enr_fork_id, attnets, syncnets)
106+
end
107+
108+
defp compute_enr_fork_id() do
109+
current_version = ForkChoice.get_fork_version()
110+
111+
fork_digest =
112+
Misc.compute_fork_digest(current_version, ChainSpec.get_genesis_validators_root())
113+
114+
%Types.EnrForkId{
115+
fork_digest: fork_digest,
116+
next_fork_version: current_version,
117+
next_fork_epoch: Constants.far_future_epoch()
118+
}
119+
end
120+
121+
@subnet_id_start byte_size("/eth2/00000000/sync_committee_")
122+
123+
defp extract_subnet_id(<<_::binary-size(@subnet_id_start)>> <> id_with_trailer) do
124+
id_with_trailer |> String.trim_trailing("/ssz_snappy") |> String.to_integer()
125+
end
27126
end

lib/lambda_ethereum_consensus/state_transition/accessors.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do
1919

2020
@max_random_byte 2 ** 8 - 1
2121

22+
@doc """
23+
Compute the correct sync committee for a given `epoch`.
24+
"""
25+
def get_sync_committee_for_epoch!(%BeaconState{} = state, epoch) do
26+
sync_committee_period = Misc.compute_sync_committee_period(epoch)
27+
current_epoch = get_current_epoch(state)
28+
current_sync_committee_period = Misc.compute_sync_committee_period(current_epoch)
29+
next_sync_committee_period = current_sync_committee_period + 1
30+
31+
case sync_committee_period do
32+
^current_sync_committee_period ->
33+
state.current_sync_committee
34+
35+
^next_sync_committee_period ->
36+
state.next_sync_committee
37+
38+
_ ->
39+
raise ArgumentError,
40+
"Invalid epoch #{epoch}, should be in the current or next sync committee period"
41+
end
42+
end
43+
2244
@doc """
2345
Return the next sync committee, with possible pubkey duplicates.
2446
"""

lib/lambda_ethereum_consensus/state_transition/misc.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ defmodule LambdaEthereumConsensus.StateTransition.Misc do
290290
div(epoch, ChainSpec.get("EPOCHS_PER_SYNC_COMMITTEE_PERIOD"))
291291
end
292292

293+
@spec sync_subcommittee_size() :: Types.uint64()
294+
def sync_subcommittee_size() do
295+
div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count())
296+
end
297+
293298
@doc """
294299
Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``.
295300
This is used primarily in signature domains to avoid collisions across forks/chains.

0 commit comments

Comments
 (0)