Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v0.11.1 to master #1686

Merged
merged 30 commits into from
Mar 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5315f3d
clarify proposer_index must be from expected shuffling when validatin…
djrtwo Mar 13, 2020
8c532c0
Merge pull request #1661 from ethereum/clarify-proposer-gossip
djrtwo Mar 16, 2020
e2ef436
min total balance is now EFFECTIVE_BALANCE_INCREMENT. update testing
djrtwo Mar 16, 2020
e6998a6
remove IK handshake from noise spec (removed in core libp2p specs)
djrtwo Mar 17, 2020
e429030
Merge pull request #1665 from ethereum/remove-ik-handshake
djrtwo Mar 18, 2020
0a9b306
PR feedback from @hwwhww
djrtwo Mar 18, 2020
2b8c32a
Merge pull request #1664 from ethereum/div-zero-total-balance
djrtwo Mar 19, 2020
759af67
Normalize attestation signature getter name
arnetheduck Mar 19, 2020
2978f21
minor edit to validator get_attestation_signature
djrtwo Mar 19, 2020
6fde75b
Merge pull request #1671 from ethereum/val-attestation-sig
djrtwo Mar 20, 2020
33f8f49
Fix base-reward memoization bug, improve memoization with LRU, and im…
protolambda Mar 20, 2020
93ff016
fix base-reward lru size, fix indent, change total-active-balance key
protolambda Mar 23, 2020
c00d8e3
Merge branch 'v011x' into fix-memoization
protolambda Mar 23, 2020
cf20a7a
Shift some ENR fields to MAY
AgeManning Mar 18, 2020
f227e02
Draft PING and ENR rpc protocol
AgeManning Mar 23, 2020
9e137a6
Merge pull request #1672 from ethereum/fix-memoization
djrtwo Mar 24, 2020
e301e1c
Merge pull request #1668 from AgeManning/enr-update
djrtwo Mar 24, 2020
1c49f99
Fix bug in attestation reward calculation
michaelsproul Mar 24, 2020
88fddf2
Remove unneccessary clock disparity check
paulhauner Mar 18, 2020
b0df824
Merge pull request #1683 from ethereum/unnecessary-clock-disp
protolambda Mar 24, 2020
b8d25dc
Merge pull request #1682 from ethereum/reward-acc-bug
djrtwo Mar 25, 2020
d5a9af6
update ping protocol to use MetaData
djrtwo Mar 25, 2020
7af6a3a
do not require non-aggregating validators to subscribe to attestation…
djrtwo Mar 25, 2020
ee45cf7
proto feedback on MetaData
djrtwo Mar 26, 2020
050b428
Merge pull request #1684 from ethereum/enr-ping
djrtwo Mar 26, 2020
a83e7a5
proto PR feedback
djrtwo Mar 26, 2020
c46fe86
Merge branch 'v011x' into publish-vs-subscribe
djrtwo Mar 26, 2020
28f091c
Merge pull request #1685 from ethereum/publish-vs-subscribe
protolambda Mar 26, 2020
d42e08a
verify proposer sig before proposer shuffling in p2p block gossip con…
djrtwo Mar 26, 2020
e6e7aab
Merge pull request #1687 from ethereum/reorder-p2p-block-conditions
djrtwo Mar 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ test: pyspec
. venv/bin/activate; cd $(PY_SPEC_DIR); \
python -m pytest -n 4 --cov=eth2spec.phase0.spec --cov=eth2spec.phase1.spec --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec

find_test: pyspec
. venv/bin/activate; cd $(PY_SPEC_DIR); \
python -m pytest -k=$(K) --cov=eth2spec.phase0.spec --cov=eth2spec.phase1.spec --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec

citest: pyspec
mkdir -p tests/core/pyspec/test-reports/eth2spec; . venv/bin/activate; cd $(PY_SPEC_DIR); \
python -m pytest -n 4 --junitxml=eth2spec/test_results.xml eth2spec
Expand Down
40 changes: 30 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def get_spec(file_name: str) -> SpecObject:
field,
)

from lru import LRU

from eth2spec.utils.ssz.ssz_impl import hash_tree_root
from eth2spec.utils.ssz.ssz_typing import (
View, boolean, Container, List, Vector, uint64,
Expand All @@ -114,6 +116,8 @@ def get_spec(file_name: str) -> SpecObject:
field,
)

from lru import LRU

from eth2spec.utils.ssz.ssz_impl import hash_tree_root
from eth2spec.utils.ssz.ssz_typing import (
View, boolean, Container, List, Vector, uint64, uint8, bit,
Expand Down Expand Up @@ -152,8 +156,8 @@ def hash(x: bytes) -> Bytes32: # type: ignore
return hash_cache[x]


def cache_this(key_fn, value_fn): # type: ignore
cache_dict = {} # type: ignore
def cache_this(key_fn, value_fn, lru_size): # type: ignore
cache_dict = LRU(size=lru_size)

def wrapper(*args, **kw): # type: ignore
key = key_fn(*args, **kw)
Expand All @@ -164,35 +168,50 @@ def wrapper(*args, **kw): # type: ignore
return wrapper


_compute_shuffled_index = compute_shuffled_index
compute_shuffled_index = cache_this(
lambda index, index_count, seed: (index, index_count, seed),
_compute_shuffled_index, lru_size=SLOTS_PER_EPOCH * 3)

_get_total_active_balance = get_total_active_balance
get_total_active_balance = cache_this(
lambda state: (state.validators.hash_tree_root(), compute_epoch_at_slot(state.slot)),
_get_total_active_balance, lru_size=10)

_get_base_reward = get_base_reward
get_base_reward = cache_this(
lambda state, index: (state.validators.hash_tree_root(), state.slot),
_get_base_reward)
lambda state, index: (state.validators.hash_tree_root(), state.slot, index),
_get_base_reward, lru_size=2048)

_get_committee_count_at_slot = get_committee_count_at_slot
get_committee_count_at_slot = cache_this(
lambda state, epoch: (state.validators.hash_tree_root(), epoch),
_get_committee_count_at_slot)
_get_committee_count_at_slot, lru_size=SLOTS_PER_EPOCH * 3)

_get_active_validator_indices = get_active_validator_indices
get_active_validator_indices = cache_this(
lambda state, epoch: (state.validators.hash_tree_root(), epoch),
_get_active_validator_indices)
_get_active_validator_indices, lru_size=3)

_get_beacon_committee = get_beacon_committee
get_beacon_committee = cache_this(
lambda state, slot, index: (state.validators.hash_tree_root(), state.randao_mixes.hash_tree_root(), slot, index),
_get_beacon_committee)
_get_beacon_committee, lru_size=SLOTS_PER_EPOCH * MAX_COMMITTEES_PER_SLOT * 3)

_get_matching_target_attestations = get_matching_target_attestations
get_matching_target_attestations = cache_this(
lambda state, epoch: (state.hash_tree_root(), epoch),
_get_matching_target_attestations)
_get_matching_target_attestations, lru_size=10)

_get_matching_head_attestations = get_matching_head_attestations
get_matching_head_attestations = cache_this(
lambda state, epoch: (state.hash_tree_root(), epoch),
_get_matching_head_attestations)'''
_get_matching_head_attestations, lru_size=10)

_get_attesting_indices = get_attesting_indices
get_attesting_indices = cache_this(
lambda state, data, bits: (state.validators.hash_tree_root(), data.hash_tree_root(), bits.hash_tree_root()),
_get_attesting_indices, lru_size=SLOTS_PER_EPOCH * MAX_COMMITTEES_PER_SLOT * 3)'''


def objects_to_spec(spec_object: SpecObject, imports: str, fork: str) -> str:
Expand Down Expand Up @@ -481,6 +500,7 @@ def run(self):
"py_ecc==2.0.0",
"dataclasses==0.6",
"remerkleable==0.1.12",
"ruamel.yaml==0.16.5"
"ruamel.yaml==0.16.5",
"lru-dict==1.1.6"
]
)
12 changes: 9 additions & 3 deletions specs/phase0/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -996,10 +996,11 @@ def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
```python
def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei:
"""
Return the combined effective balance of the ``indices``. (1 Gwei minimum to avoid divisions by zero.)
Return the combined effective balance of the ``indices``.
``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero.
Math safe up to ~10B ETH, afterwhich this overflows uint64.
"""
return Gwei(max(1, sum([state.validators[index].effective_balance for index in indices])))
return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([state.validators[index].effective_balance for index in indices])))
```

#### `get_total_active_balance`
Expand All @@ -1008,6 +1009,7 @@ def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei:
def get_total_active_balance(state: BeaconState) -> Gwei:
"""
Return the combined effective balance of the active validators.
Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero.
"""
return get_total_balance(state, set(get_active_validator_indices(state, get_current_epoch(state))))
```
Expand Down Expand Up @@ -1289,6 +1291,10 @@ def get_unslashed_attesting_indices(state: BeaconState,

```python
def get_attesting_balance(state: BeaconState, attestations: Sequence[PendingAttestation]) -> Gwei:
"""
Return the combined effective balance of the set of unslashed validators participating in ``attestations``.
Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero.
"""
return get_total_balance(state, get_unslashed_attesting_indices(state, attestations))
```

Expand Down Expand Up @@ -1366,7 +1372,7 @@ def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence
if index in unslashed_attesting_indices:
increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment)
rewards[index] = reward_numerator // (total_balance // increment)
rewards[index] += reward_numerator // (total_balance // increment)
else:
penalties[index] += get_base_reward(state, index)

Expand Down
92 changes: 87 additions & 5 deletions specs/phase0/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ It consists of four main sections:
- [Multiplexing](#multiplexing)
- [Eth2 network interaction domains](#eth2-network-interaction-domains)
- [Configuration](#configuration)
- [MetaData](#metadata)
- [The gossip domain: gossipsub](#the-gossip-domain-gossipsub)
- [Topics and messages](#topics-and-messages)
- [Global topics](#global-topics)
Expand All @@ -49,6 +50,8 @@ It consists of four main sections:
- [Goodbye](#goodbye)
- [BeaconBlocksByRange](#beaconblocksbyrange)
- [BeaconBlocksByRoot](#beaconblocksbyroot)
- [Ping](#ping)
- [GetMetaData](#getmetadata)
- [The discovery domain: discv5](#the-discovery-domain-discv5)
- [Integration into libp2p stacks](#integration-into-libp2p-stacks)
- [ENR structure](#enr-structure)
Expand Down Expand Up @@ -157,8 +160,7 @@ The following SecIO parameters MUST be supported by all stacks:
The [Libp2p-noise](https://github.com/libp2p/specs/tree/master/noise) secure
channel handshake with `secp256k1` identities will be used for mainnet.

As specified in the libp2p specification, clients MUST support the `XX` handshake pattern and
can optionally implement the `IK` and `XXfallback` patterns for optimistic 0-RTT.
As specified in the libp2p specification, clients MUST support the `XX` handshake pattern.

## Protocol Negotiation

Expand Down Expand Up @@ -196,6 +198,24 @@ This section outlines constants that are used in this spec.
| `ATTESTATION_PROPAGATION_SLOT_RANGE` | `32` | The maximum number of slots during which an attestation can be propagated. |
| `MAXIMUM_GOSSIP_CLOCK_DISPARITY` | `500ms` | The maximum milliseconds of clock disparity assumed between honest nodes. |

## MetaData

Clients MUST locally store the following `MetaData`:

```
(
seq_number: uint64
attnets: Bitvector[ATTESTATION_SUBNET_COUNT]
)
```

Where

- `seq_number` is a `uint64` starting at `0` used to version the node's metadata. If any other field in the local `MetaData` changes, the node MUST increment `seq_number` by 1.
- `attnets` is a `Bitvector` representing the node's persistent attestation subnet subscriptions.

*Note*: `MetaData.seq_number` is used for versioning of the node's metadata, is entirely independent of the ENR sequence number, and will in most cases be out of sync with the ENR sequence number.

## The gossip domain: gossipsub

Clients MUST support the [gossipsub](https://github.com/libp2p/specs/tree/master/pubsub/gossipsub) libp2p protocol.
Expand Down Expand Up @@ -260,9 +280,10 @@ There are two primary global topics used to propagate beacon blocks and aggregat

- `beacon_block` - This topic is used solely for propagating new signed beacon blocks to all nodes on the networks. Signed blocks are sent in their entirety. The following validations MUST pass before forwarding the `signed_beacon_block` on the network
- The block is not from a future slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. validate that `signed_beacon_block.message.slot <= current_slot` (a client MAY queue future blocks for processing at the appropriate slot).
- The block is from a slot greater than the latest finalized slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. validate that `signed_beacon_block.message.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)` (a client MAY choose to validate and store such blocks for additional purposes -- e.g. slashing detection, archive nodes, etc).
- The block is from a slot greater than the latest finalized slot -- i.e. validate that `signed_beacon_block.message.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)` (a client MAY choose to validate and store such blocks for additional purposes -- e.g. slashing detection, archive nodes, etc).
- The block is the first block with valid signature received for the proposer for the slot, `signed_beacon_block.message.slot`.
- The proposer signature, `signed_beacon_block.signature`, is valid.
- The proposer signature, `signed_beacon_block.signature`, is valid with respect to the `proposer_index` pubkey.
- The block is proposed by the expected `proposer_index` for the block's slot in the context of the current shuffling (defined by `parent_root`/`slot`). If the `proposer_index` cannot immediately be verified against the expected shuffling, the block MAY be queued for later processing while proposers for the block's branch are calculated.
- `beacon_aggregate_and_proof` - This topic is used to propagate aggregated attestations (as `SignedAggregateAndProof`s) to subscribing nodes (typically validators) to be included in future blocks. The following validations MUST pass before forwarding the `signed_aggregate_and_proof` on the network. (We define the following for convenience -- `aggregate_and_proof = signed_aggregate_and_proof.message` and `aggregate = aggregate_and_proof.aggregate`)
- `aggregate.data.slot` is within the last `ATTESTATION_PROPAGATION_SLOT_RANGE` slots (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. `aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot` (a client MAY queue future aggregates for processing at the appropriate slot).
- The aggregate attestation defined by `hash_tree_root(aggregate)` has _not_ already been seen (via aggregate gossip, within a block, or through the creation of an equivalent aggregate locally).
Expand Down Expand Up @@ -601,6 +622,60 @@ Clients MUST support requesting blocks since the latest finalized epoch.

Clients MUST respond with at least one block, if they have it. Clients MAY limit the number of blocks in the response.

#### Ping

**Protocol ID:** `/eth2/beacon_chain/req/ping/1/`

Request Content:

```
(
uint64
)
```

Response Content:

```
(
uint64
)
```

Sent intermittently, the `Ping` protocol checks liveness of connected peers.
Peers request and respond with their local metadata sequence number (`MetaData.seq_number`).

If the peer does not respond to the `Ping` request, the client MAY disconnect from the peer.

A client can then determine if their local record of a peer's MetaData is up to date
and MAY request an updated version via the `MetaData` RPC method if not.

The request MUST be encoded as an SSZ-field.

The response MUST consist of a single `response_chunk`.

#### GetMetaData

**Protocol ID:** `/eth2/beacon_chain/req/metadata/1/`

No Request Content.

Response Content:

```
(
MetaData
)
```

Requests the MetaData of a peer. The request opens and negotiates the stream without
sending any request content. Once established the receiving peer responds with
it's local most up-to-date MetaData.

The response MUST be encoded as an SSZ-container.

The response MUST consist of a single `response_chunk`.

## The discovery domain: discv5

Discovery Version 5 ([discv5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md)) is used for peer discovery, both in the interoperability testnet and mainnet.
Expand All @@ -622,6 +697,9 @@ This integration enables the libp2p stack to subsequently form connections and s
The Ethereum Node Record (ENR) for an Ethereum 2.0 client MUST contain the following entries (exclusive of the sequence number and signature, which MUST be present in an ENR):

- The compressed secp256k1 publickey, 33 bytes (`secp256k1` field).

The ENR MAY contain the following entries:

- An IPv4 address (`ip` field) and/or IPv6 address (`ip6` field).
- A TCP port (`tcp` field) representing the local libp2p listening port.
- A UDP port (`udp` field) representing the local discv5 listening port.
Expand All @@ -630,12 +708,16 @@ Specifications of these parameters can be found in the [ENR Specification](http:

#### Attestation subnet bitfield

The ENR MAY contain an entry (`attnets`) signifying the attestation subnet bitfield with the following form to more easily discover peers participating in particular attestation gossip subnets.
The ENR `attnets` entry signifies the attestation subnet bitfield with the following form to more easily discover peers participating in particular attestation gossip subnets.

| Key | Value |
|:-------------|:-------------------------------------------------|
| `attnets` | SSZ `Bitvector[ATTESTATION_SUBNET_COUNT]` |

If a node's `MetaData.attnets` has any non-zero bit, the ENR MUST include the `attnets` entry with the same value as `MetaData.attnets`.

If a node's `MetaData.attnets` is composed of all zeros, the ENR MAY optionally include the `attnets` entry or leave it out entirely.

#### Interop

In the interoperability testnet, all peers will support all capabilities defined in this document (gossip, full Req/Resp suite, discovery protocol), therefore the ENR record does not need to carry Eth2 capability information, as it would be superfluous.
Expand Down
16 changes: 9 additions & 7 deletions specs/phase0/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,11 @@ The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahe

Specifically a validator should:
* Call `get_committee_assignment(state, next_epoch, validator_index)` when checking for next epoch assignments.
* Join the pubsub topic -- `committee_index{committee_index % ATTESTATION_SUBNET_COUNT}_beacon_attestation`.
* For any current peer subscribed to the topic, the validator simply sends a `subscribe` message for the new topic.
* If an _insufficient_ number of current peers are subscribed to the topic, the validator must discover new peers on this topic. Via the discovery protocol, find peers with an ENR containing the `attnets` entry such that `ENR["attnets"][committee_index % ATTESTATION_SUBNET_COUNT] == True`.
* Find peers of the pubsub topic `committee_index{committee_index % ATTESTATION_SUBNET_COUNT}_beacon_attestation`.
* If an _insufficient_ number of current peers are subscribed to the topic, the validator must discover new peers on this topic. Via the discovery protocol, find peers with an ENR containing the `attnets` entry such that `ENR["attnets"][committee_index % ATTESTATION_SUBNET_COUNT] == True`. Then validate that the peers are still persisted on the desired topic by requesting `GetMetaData` and checking the resulting `attnets` field.
* If the validator is assigned to be an aggregator for the slot (see `is_aggregator()`), then subscribe to the topic.

*Note*: If the validator is _not_ assigned to be an aggregator, the validator only needs sufficient number of peers on the topic to be able to publish messages. The validator does not need to _subscribe_ and listen to all messages on the topic.

## Beacon chain responsibilities

Expand Down Expand Up @@ -404,12 +406,12 @@ Set `attestation.data = attestation_data` where `attestation_data` is the `Attes

##### Aggregate signature

Set `attestation.signature = signed_attestation_data` where `signed_attestation_data` is obtained from:
Set `attestation.signature = attestation_signature` where `attestation_signature` is obtained from:

```python
def get_signed_attestation_data(state: BeaconState, attestation: IndexedAttestation, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation.data.target.epoch)
signing_root = compute_signing_root(attestation.data, domain)
def get_attestation_signature(state: BeaconState, attestation_data: AttestationData, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch)
signing_root = compute_signing_root(attestation_data, domain)
return bls.Sign(privkey, signing_root)
```

Expand Down
23 changes: 21 additions & 2 deletions tests/core/pyspec/eth2spec/test/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .utils import vector_test, with_meta_tags

from random import Random
from typing import Any, Callable, Sequence, TypedDict, Protocol

from importlib import reload
Expand Down Expand Up @@ -71,6 +72,14 @@ def default_activation_threshold(spec):
return spec.MAX_EFFECTIVE_BALANCE


def zero_activation_threshold(spec):
"""
Helper method to use 0 gwei as the activation threshold for state creation for tests.
Usage: `@with_custom_state(threshold_fn=zero_activation_threshold, ...)`
"""
return 0


def default_balances(spec):
"""
Helper method to create a series of default balances.
Expand Down Expand Up @@ -100,8 +109,18 @@ def misc_balances(spec):
Usage: `@with_custom_state(balances_fn=misc_balances, ...)`
"""
num_validators = spec.SLOTS_PER_EPOCH * 8
num_misc_validators = spec.SLOTS_PER_EPOCH
return [spec.MAX_EFFECTIVE_BALANCE] * num_validators + [spec.MIN_DEPOSIT_AMOUNT] * num_misc_validators
balances = [spec.MAX_EFFECTIVE_BALANCE * 2 * i // num_validators for i in range(num_validators)]
rng = Random(1234)
rng.shuffle(balances)
return balances


def low_single_balance(spec):
"""
Helper method to create a single of balance of 1 Gwei.
Usage: `@with_custom_state(balances_fn=low_single_balance, ...)`
"""
return [1]


def single_phase(fn):
Expand Down
5 changes: 3 additions & 2 deletions tests/core/pyspec/eth2spec/test/helpers/attestations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def build_attestation_data(spec, state, slot, index):
)


def get_valid_attestation(spec, state, slot=None, index=None, signed=False):
def get_valid_attestation(spec, state, slot=None, index=None, empty=False, signed=False):
if slot is None:
slot = state.slot
if index is None:
Expand All @@ -59,7 +59,8 @@ def get_valid_attestation(spec, state, slot=None, index=None, signed=False):
aggregation_bits=aggregation_bits,
data=attestation_data,
)
fill_aggregate_attestation(spec, state, attestation)
if not empty:
fill_aggregate_attestation(spec, state, attestation)
if signed:
sign_attestation(spec, state, attestation)
return attestation
Expand Down
Loading