commit
52582662f1
|
@ -76,8 +76,8 @@ BLS_WITHDRAWAL_PREFIX: 0x00
|
|||
|
||||
# Time parameters
|
||||
# ---------------------------------------------------------------
|
||||
# 86400 seconds (1 day)
|
||||
MIN_GENESIS_DELAY: 86400
|
||||
# 172800 seconds (2 days)
|
||||
GENESIS_DELAY: 172800
|
||||
# 12 seconds
|
||||
SECONDS_PER_SLOT: 12
|
||||
# 2**0 (= 1) slots 12 seconds
|
||||
|
|
|
@ -77,7 +77,7 @@ BLS_WITHDRAWAL_PREFIX: 0x00
|
|||
# Time parameters
|
||||
# ---------------------------------------------------------------
|
||||
# [customized] Faster to spin up testnets, but does not give validator reasonable warning time for genesis
|
||||
MIN_GENESIS_DELAY: 300
|
||||
GENESIS_DELAY: 300
|
||||
# [customized] Faster for testing purposes
|
||||
SECONDS_PER_SLOT: 6
|
||||
# 2**0 (= 1) slots 6 seconds
|
||||
|
|
|
@ -218,7 +218,7 @@ The following values are (non-configurable) constants used throughout the specif
|
|||
|
||||
| Name | Value | Unit | Duration |
|
||||
| - | - | :-: | :-: |
|
||||
| `MIN_GENESIS_DELAY` | `86400` | seconds | 1 day |
|
||||
| `GENESIS_DELAY` | `172800` | seconds | 2 days |
|
||||
| `SECONDS_PER_SLOT` | `12` | seconds | 12 seconds |
|
||||
| `SECONDS_PER_ETH1_BLOCK` | `14` | seconds | 14 seconds |
|
||||
| `MIN_ATTESTATION_INCLUSION_DELAY` | `2**0` (= 1) | slots | 12 seconds |
|
||||
|
@ -1137,7 +1137,7 @@ Before the Ethereum 2.0 genesis has been triggered, and for every Ethereum 1.0 b
|
|||
- `eth1_timestamp` is the Unix timestamp corresponding to `eth1_block_hash`
|
||||
- `deposits` is the sequence of all deposits, ordered chronologically, up to (and including) the block with hash `eth1_block_hash`
|
||||
|
||||
Eth1 blocks must only be considered once they are at least `SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE` seconds old (i.e. `eth1_timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= current_unix_time`). Due to this constraint, if `MIN_GENESIS_DELAY < SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE`, then the `genesis_time` can happen before the time/state is first known. Values should be configured to avoid this case.
|
||||
Eth1 blocks must only be considered once they are at least `SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE` seconds old (i.e. `eth1_timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= current_unix_time`). Due to this constraint, if `GENESIS_DELAY < SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE`, then the `genesis_time` can happen before the time/state is first known. Values should be configured to avoid this case.
|
||||
|
||||
```python
|
||||
def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32,
|
||||
|
@ -1149,7 +1149,7 @@ def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32,
|
|||
epoch=GENESIS_EPOCH,
|
||||
)
|
||||
state = BeaconState(
|
||||
genesis_time=eth1_timestamp - eth1_timestamp % MIN_GENESIS_DELAY + 2 * MIN_GENESIS_DELAY,
|
||||
genesis_time=eth1_timestamp + GENESIS_DELAY,
|
||||
fork=fork,
|
||||
eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=len(deposits)),
|
||||
latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())),
|
||||
|
|
|
@ -150,7 +150,7 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> Root:
|
|||
elif block.slot == slot:
|
||||
return root
|
||||
else:
|
||||
# root is older than queried slot, thus a skip slot. Return earliest root prior to slot
|
||||
# root is older than queried slot, thus a skip slot. Return most recent root prior to slot
|
||||
return root
|
||||
```
|
||||
|
||||
|
@ -285,7 +285,7 @@ def validate_on_attestation(store: Store, attestation: Attestation) -> None:
|
|||
# Attestations must not be for blocks in the future. If not, the attestation should not be considered
|
||||
assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot
|
||||
|
||||
# FFG and LMD vote must be consistent with each other
|
||||
# LMD vote must be consistent with FFG vote target
|
||||
target_slot = compute_start_slot_at_epoch(target.epoch)
|
||||
assert target.root == get_ancestor(store, attestation.data.beacon_block_root, target_slot)
|
||||
|
||||
|
|
|
@ -150,6 +150,7 @@ This section outlines constants that are used in this spec.
|
|||
| Name | Value | Description |
|
||||
|---|---|---|
|
||||
| `GOSSIP_MAX_SIZE` | `2**20` (= 1048576, 1 MiB) | The maximum allowed size of uncompressed gossip messages. |
|
||||
| `MAX_REQUEST_BLOCKS` | `2**10` (= 1024) | Maximum number of blocks in a single request |
|
||||
| `MAX_CHUNK_SIZE` | `2**20` (1048576, 1 MiB) | The maximum allowed size of uncompressed req/resp chunked responses. |
|
||||
| `TTFB_TIMEOUT` | `5s` | The maximum time to wait for first byte of request response (time-to-first-byte). |
|
||||
| `RESP_TIMEOUT` | `10s` | The maximum time for complete response transfer. |
|
||||
|
@ -203,7 +204,7 @@ Topics are plain UTF-8 strings and are encoded on the wire as determined by prot
|
|||
- `current_fork_version` is the fork version of the epoch of the message to be sent on the topic
|
||||
- `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root`
|
||||
- `Name` - see table below
|
||||
- `Encoding` - the encoding strategy describes a specific representation of bytes that will be transmitted over the wire. See the [Encodings](#Encoding-strategies) section for further details.
|
||||
- `Encoding` - the encoding strategy describes a specific representation of bytes that will be transmitted over the wire. See the [Encodings](#Encodings) section for further details.
|
||||
|
||||
*Note*: `ForkDigestValue` is composed of values that are not known until the genesis block/state are available. Due to this, clients SHOULD NOT subscribe to gossipsub topics until these genesis values are known.
|
||||
|
||||
|
@ -391,11 +392,11 @@ The `ErrorMessage` schema is:
|
|||
|
||||
```
|
||||
(
|
||||
error_message: String
|
||||
error_message: List[byte, 256]
|
||||
)
|
||||
```
|
||||
|
||||
*Note*: The String type is encoded as UTF-8 bytes without NULL terminator when SSZ-encoded. As the `ErrorMessage` is not an SSZ-container, only the UTF-8 bytes will be sent when SSZ-encoded.
|
||||
*Note*: By convention, the `error_message` is a sequence of bytes that MAY be interpreted as a UTF-8 string (for debugging purposes). Clients MUST treat as valid any byte sequences.
|
||||
|
||||
### Encoding strategies
|
||||
|
||||
|
@ -443,9 +444,9 @@ In case of an invalid input (header or payload), a reader MUST:
|
|||
|
||||
All messages that contain only a single field MUST be encoded directly as the type of that field and MUST NOT be encoded as an SSZ container.
|
||||
|
||||
Responses that are SSZ-lists (for example `[]SignedBeaconBlock`) send their
|
||||
Responses that are SSZ-lists (for example `List[SignedBeaconBlock, ...]`) send their
|
||||
constituents individually as `response_chunk`s. For example, the
|
||||
`[]SignedBeaconBlock` response type sends zero or more `response_chunk`s. Each _successful_ `response_chunk` contains a single `SignedBeaconBlock` payload.
|
||||
`List[SignedBeaconBlock, ...]` response type sends zero or more `response_chunk`s. Each _successful_ `response_chunk` contains a single `SignedBeaconBlock` payload.
|
||||
|
||||
### Messages
|
||||
|
||||
|
@ -468,9 +469,9 @@ The fields are, as seen by the client at the time of sending the message:
|
|||
- `fork_digest`: The node's `ForkDigest` (`compute_fork_digest(current_fork_version, genesis_validators_root)`) where
|
||||
- `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync)
|
||||
- `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root`
|
||||
- `finalized_root`: `state.finalized_checkpoint.root` for the state corresponding to the head block.
|
||||
- `finalized_root`: `state.finalized_checkpoint.root` for the state corresponding to the head block (Note this defaults to `Root(b'\x00' * 32)` for the genesis finalized checkpoint).
|
||||
- `finalized_epoch`: `state.finalized_checkpoint.epoch` for the state corresponding to the head block.
|
||||
- `head_root`: The hash_tree_root root of the current head block.
|
||||
- `head_root`: The `hash_tree_root` root of the current head block (`BeaconBlock`).
|
||||
- `head_slot`: The slot of the block corresponding to the `head_root`.
|
||||
|
||||
The dialing client MUST send a `Status` request upon connection.
|
||||
|
@ -528,7 +529,7 @@ Request Content:
|
|||
Response Content:
|
||||
```
|
||||
(
|
||||
[]SignedBeaconBlock
|
||||
List[SignedBeaconBlock, MAX_REQUEST_BLOCKS]
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -545,7 +546,7 @@ The response MUST consist of zero or more `response_chunk`. Each _successful_ `r
|
|||
|
||||
Clients MUST keep a record of signed blocks seen since the since the start of the weak subjectivity period and MUST support serving requests of blocks up to their own `head_block_root`.
|
||||
|
||||
Clients MUST respond with at least the first block that exists in the range, if they have it.
|
||||
Clients MUST respond with at least the first block that exists in the range, if they have it, and no more than `MAX_REQUEST_BLOCKS` blocks.
|
||||
|
||||
The following blocks, where they exist, MUST be send in consecutive order.
|
||||
|
||||
|
@ -568,7 +569,7 @@ Request Content:
|
|||
|
||||
```
|
||||
(
|
||||
[]Root
|
||||
List[Root, MAX_REQUEST_BLOCKS]
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -576,12 +577,14 @@ Response Content:
|
|||
|
||||
```
|
||||
(
|
||||
[]SignedBeaconBlock
|
||||
List[SignedBeaconBlock, MAX_REQUEST_BLOCKS]
|
||||
)
|
||||
```
|
||||
|
||||
Requests blocks by block root (= `hash_tree_root(SignedBeaconBlock.message)`). The response is a list of `SignedBeaconBlock` whose length is less than or equal to the number of requested blocks. It may be less in the case that the responding peer is missing blocks.
|
||||
|
||||
No more than `MAX_REQUEST_BLOCKS` may be requested at a time.
|
||||
|
||||
`BeaconBlocksByRoot` is primarily used to recover recent blocks (e.g. when receiving a block or attestation whose parent is unknown).
|
||||
|
||||
The request MUST be encoded as an SSZ-field.
|
||||
|
@ -1052,7 +1055,7 @@ discv5 uses ENRs and we will presumably need to:
|
|||
|
||||
Although client software might very well be running locally prior to the solidification of the eth2 genesis state and block, clients cannot form valid ENRs prior to this point. ENRs contain `fork_digest` which utilizes the `genesis_validators_root` for a cleaner separation between chains so prior to knowing genesis, we cannot use `fork_digest` to cleanly find peers on our intended chain. Once genesis data is known, we can then form ENRs and safely find peers.
|
||||
|
||||
When using an eth1 deposit contract for deposits, `fork_digest` will be known at least `MIN_GENESIS_DELAY` (24 hours in mainnet configuration) before `genesis_time`, providing ample time to find peers and form initial connections and gossip subnets prior to genesis.
|
||||
When using an eth1 deposit contract for deposits, `fork_digest` will be known `GENESIS_DELAY` (48hours in mainnet configuration) before `genesis_time`, providing ample time to find peers and form initial connections and gossip subnets prior to genesis.
|
||||
|
||||
## Compression/Encoding
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
- [`compute_shard_from_committee_index`](#compute_shard_from_committee_index)
|
||||
- [`compute_offset_slots`](#compute_offset_slots)
|
||||
- [`compute_updated_gasprice`](#compute_updated_gasprice)
|
||||
- [`compute_committee_source_epoch`](#compute_committee_source_epoch)
|
||||
- [Beacon state accessors](#beacon-state-accessors)
|
||||
- [`get_active_shard_count`](#get_active_shard_count)
|
||||
- [`get_online_validator_indices`](#get_online_validator_indices)
|
||||
|
@ -52,8 +53,9 @@
|
|||
- [`get_latest_slot_for_shard`](#get_latest_slot_for_shard)
|
||||
- [`get_offset_slots`](#get_offset_slots)
|
||||
- [Predicates](#predicates)
|
||||
- [`verify_attestation_custody`](#verify_attestation_custody)
|
||||
- [Updated `is_valid_indexed_attestation`](#updated-is_valid_indexed_attestation)
|
||||
- [`is_shard_attestation`](#is_shard_attestation)
|
||||
- [`is_on_time_attestation`](#is_on_time_attestation)
|
||||
- [`is_winning_attestation`](#is_winning_attestation)
|
||||
- [`optional_aggregate_verify`](#optional_aggregate_verify)
|
||||
- [`optional_fast_aggregate_verify`](#optional_fast_aggregate_verify)
|
||||
|
@ -61,12 +63,14 @@
|
|||
- [Operations](#operations)
|
||||
- [New Attestation processing](#new-attestation-processing)
|
||||
- [`validate_attestation`](#validate_attestation)
|
||||
- [Updated `process_attestation`](#updated-process_attestation)
|
||||
- [Shard transition processing](#shard-transition-processing)
|
||||
- [`apply_shard_transition`](#apply_shard_transition)
|
||||
- [`process_crosslink_for_shard`](#process_crosslink_for_shard)
|
||||
- [`process_crosslinks`](#process_crosslinks)
|
||||
- [`process_attestation`](#process_attestation)
|
||||
- [`verify_empty_shard_transition`](#verify_empty_shard_transition)
|
||||
- [`process_shard_transitions`](#process_shard_transitions)
|
||||
- [New Attester slashing processing](#new-attester-slashing-processing)
|
||||
- [Shard transition false positives](#shard-transition-false-positives)
|
||||
- [Light client processing](#light-client-processing)
|
||||
- [Epoch transition](#epoch-transition)
|
||||
- [Custody game updates](#custody-game-updates)
|
||||
|
@ -130,7 +134,7 @@ class AttestationData(Container):
|
|||
source: Checkpoint
|
||||
target: Checkpoint
|
||||
# Current-slot shard block root
|
||||
head_shard_root: Root
|
||||
shard_head_root: Root
|
||||
# Shard transition root
|
||||
shard_transition_root: Root
|
||||
```
|
||||
|
@ -153,6 +157,7 @@ class PendingAttestation(Container):
|
|||
data: AttestationData
|
||||
inclusion_delay: Slot
|
||||
proposer_index: ValidatorIndex
|
||||
# Phase 1
|
||||
crosslink_success: boolean
|
||||
```
|
||||
|
||||
|
@ -302,6 +307,7 @@ class ShardBlock(Container):
|
|||
shard_parent_root: Root
|
||||
beacon_parent_root: Root
|
||||
slot: Slot
|
||||
shard: Shard
|
||||
proposer_index: ValidatorIndex
|
||||
body: ByteList[MAX_SHARD_BLOCK_SIZE]
|
||||
```
|
||||
|
@ -321,6 +327,7 @@ class ShardBlockHeader(Container):
|
|||
shard_parent_root: Root
|
||||
beacon_parent_root: Root
|
||||
slot: Slot
|
||||
shard: Shard
|
||||
proposer_index: ValidatorIndex
|
||||
body_root: Root
|
||||
```
|
||||
|
@ -413,7 +420,7 @@ def unpack_compact_validator(compact_validator: uint64) -> Tuple[ValidatorIndex,
|
|||
```python
|
||||
def committee_to_compact_committee(state: BeaconState, committee: Sequence[ValidatorIndex]) -> CompactCommittee:
|
||||
"""
|
||||
Given a state and a list of validator indices, outputs the CompactCommittee representing them.
|
||||
Given a state and a list of validator indices, outputs the ``CompactCommittee`` representing them.
|
||||
"""
|
||||
validators = [state.validators[i] for i in committee]
|
||||
compact_validators = [
|
||||
|
@ -445,17 +452,30 @@ def compute_offset_slots(start_slot: Slot, end_slot: Slot) -> Sequence[Slot]:
|
|||
#### `compute_updated_gasprice`
|
||||
|
||||
```python
|
||||
def compute_updated_gasprice(prev_gasprice: Gwei, length: uint8) -> Gwei:
|
||||
if length > TARGET_SHARD_BLOCK_SIZE:
|
||||
delta = (prev_gasprice * (length - TARGET_SHARD_BLOCK_SIZE)
|
||||
def compute_updated_gasprice(prev_gasprice: Gwei, shard_block_length: uint8) -> Gwei:
|
||||
if shard_block_length > TARGET_SHARD_BLOCK_SIZE:
|
||||
delta = (prev_gasprice * (shard_block_length - TARGET_SHARD_BLOCK_SIZE)
|
||||
// TARGET_SHARD_BLOCK_SIZE // GASPRICE_ADJUSTMENT_COEFFICIENT)
|
||||
return min(prev_gasprice + delta, MAX_GASPRICE)
|
||||
else:
|
||||
delta = (prev_gasprice * (TARGET_SHARD_BLOCK_SIZE - length)
|
||||
delta = (prev_gasprice * (TARGET_SHARD_BLOCK_SIZE - shard_block_length)
|
||||
// TARGET_SHARD_BLOCK_SIZE // GASPRICE_ADJUSTMENT_COEFFICIENT)
|
||||
return max(prev_gasprice, MIN_GASPRICE + delta) - delta
|
||||
```
|
||||
|
||||
#### `compute_committee_source_epoch`
|
||||
|
||||
```python
|
||||
def compute_committee_source_epoch(epoch: Epoch, period: uint64) -> Epoch:
|
||||
"""
|
||||
Return the source epoch for computing the committee.
|
||||
"""
|
||||
source_epoch = epoch - epoch % period
|
||||
if source_epoch >= period:
|
||||
source_epoch -= period # `period` epochs lookahead
|
||||
return source_epoch
|
||||
```
|
||||
|
||||
### Beacon state accessors
|
||||
|
||||
#### `get_active_shard_count`
|
||||
|
@ -477,9 +497,10 @@ def get_online_validator_indices(state: BeaconState) -> Set[ValidatorIndex]:
|
|||
|
||||
```python
|
||||
def get_shard_committee(beacon_state: BeaconState, epoch: Epoch, shard: Shard) -> Sequence[ValidatorIndex]:
|
||||
source_epoch = epoch - epoch % SHARD_COMMITTEE_PERIOD
|
||||
if source_epoch >= SHARD_COMMITTEE_PERIOD:
|
||||
source_epoch -= SHARD_COMMITTEE_PERIOD
|
||||
"""
|
||||
Return the shard committee of the given ``epoch`` of the given ``shard``.
|
||||
"""
|
||||
source_epoch = compute_committee_source_epoch(epoch, SHARD_COMMITTEE_PERIOD)
|
||||
active_validator_indices = get_active_validator_indices(beacon_state, source_epoch)
|
||||
seed = get_seed(beacon_state, source_epoch, DOMAIN_SHARD_COMMITTEE)
|
||||
active_shard_count = get_active_shard_count(beacon_state)
|
||||
|
@ -495,9 +516,10 @@ def get_shard_committee(beacon_state: BeaconState, epoch: Epoch, shard: Shard) -
|
|||
|
||||
```python
|
||||
def get_light_client_committee(beacon_state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]:
|
||||
source_epoch = epoch - epoch % LIGHT_CLIENT_COMMITTEE_PERIOD
|
||||
if source_epoch >= LIGHT_CLIENT_COMMITTEE_PERIOD:
|
||||
source_epoch -= LIGHT_CLIENT_COMMITTEE_PERIOD
|
||||
"""
|
||||
Return the light client committee of no more than ``TARGET_COMMITTEE_SIZE`` validators.
|
||||
"""
|
||||
source_epoch = compute_committee_source_epoch(epoch, LIGHT_CLIENT_COMMITTEE_PERIOD)
|
||||
active_validator_indices = get_active_validator_indices(beacon_state, source_epoch)
|
||||
seed = get_seed(beacon_state, source_epoch, DOMAIN_LIGHT_CLIENT)
|
||||
return compute_committee(
|
||||
|
@ -554,11 +576,45 @@ def get_latest_slot_for_shard(state: BeaconState, shard: Shard) -> Slot:
|
|||
|
||||
```python
|
||||
def get_offset_slots(state: BeaconState, shard: Shard) -> Sequence[Slot]:
|
||||
return compute_offset_slots(state.shard_states[shard].slot, state.slot)
|
||||
"""
|
||||
Return the offset slots of the given ``shard`` between that latest included slot and current slot.
|
||||
"""
|
||||
return compute_offset_slots(get_latest_slot_for_shard(state, shard), state.slot)
|
||||
```
|
||||
|
||||
### Predicates
|
||||
|
||||
#### `verify_attestation_custody`
|
||||
|
||||
```python
|
||||
def verify_attestation_custody(state: BeaconState, indexed_attestation: IndexedAttestation) -> bool:
|
||||
"""
|
||||
Check if ``indexed_attestation`` has valid signature against non-empty custody bits.
|
||||
"""
|
||||
attestation = indexed_attestation.attestation
|
||||
aggregation_bits = attestation.aggregation_bits
|
||||
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation.data.target.epoch)
|
||||
all_pubkeys = []
|
||||
all_signing_roots = []
|
||||
for block_index, custody_bits in enumerate(attestation.custody_bits_blocks):
|
||||
assert len(custody_bits) == len(indexed_attestation.committee)
|
||||
for participant, aggregation_bit, custody_bit in zip(
|
||||
indexed_attestation.committee, aggregation_bits, custody_bits
|
||||
):
|
||||
if aggregation_bit:
|
||||
all_pubkeys.append(state.validators[participant].pubkey)
|
||||
# Note: only 2N distinct message hashes
|
||||
attestation_wrapper = AttestationCustodyBitWrapper(
|
||||
attestation_data_root=hash_tree_root(attestation.data),
|
||||
block_index=block_index,
|
||||
bit=custody_bit,
|
||||
)
|
||||
all_signing_roots.append(compute_signing_root(attestation_wrapper, domain))
|
||||
else:
|
||||
assert not custody_bit
|
||||
return bls.AggregateVerify(all_pubkeys, all_signing_roots, signature=attestation.signature)
|
||||
```
|
||||
|
||||
#### Updated `is_valid_indexed_attestation`
|
||||
|
||||
Note that this replaces the Phase 0 `is_valid_indexed_attestation`.
|
||||
|
@ -569,53 +625,34 @@ def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: Indexe
|
|||
Check if ``indexed_attestation`` has valid indices and signature.
|
||||
"""
|
||||
# Verify aggregate signature
|
||||
all_pubkeys = []
|
||||
all_signing_roots = []
|
||||
attestation = indexed_attestation.attestation
|
||||
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation.data.target.epoch)
|
||||
aggregation_bits = attestation.aggregation_bits
|
||||
if not any(aggregation_bits) or len(aggregation_bits) != len(indexed_attestation.committee):
|
||||
return False
|
||||
|
||||
if len(attestation.custody_bits_blocks) == 0:
|
||||
# fall back on phase0 behavior if there is no shard data.
|
||||
for participant, abit in zip(indexed_attestation.committee, aggregation_bits):
|
||||
if abit:
|
||||
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation.data.target.epoch)
|
||||
all_pubkeys = []
|
||||
for participant, aggregation_bit in zip(indexed_attestation.committee, aggregation_bits):
|
||||
if aggregation_bit:
|
||||
all_pubkeys.append(state.validators[participant].pubkey)
|
||||
signing_root = compute_signing_root(indexed_attestation.attestation.data, domain)
|
||||
return bls.FastAggregateVerify(all_pubkeys, signing_root, signature=attestation.signature)
|
||||
else:
|
||||
for i, custody_bits in enumerate(attestation.custody_bits_blocks):
|
||||
assert len(custody_bits) == len(indexed_attestation.committee)
|
||||
for participant, abit, cbit in zip(indexed_attestation.committee, aggregation_bits, custody_bits):
|
||||
if abit:
|
||||
all_pubkeys.append(state.validators[participant].pubkey)
|
||||
# Note: only 2N distinct message hashes
|
||||
attestation_wrapper = AttestationCustodyBitWrapper(
|
||||
attestation_data_root=hash_tree_root(attestation.data),
|
||||
block_index=i,
|
||||
bit=cbit
|
||||
)
|
||||
all_signing_roots.append(compute_signing_root(attestation_wrapper, domain))
|
||||
else:
|
||||
assert not cbit
|
||||
return bls.AggregateVerify(all_pubkeys, all_signing_roots, signature=attestation.signature)
|
||||
return verify_attestation_custody(state, indexed_attestation)
|
||||
```
|
||||
|
||||
#### `is_shard_attestation`
|
||||
#### `is_on_time_attestation`
|
||||
|
||||
```python
|
||||
def is_shard_attestation(state: BeaconState,
|
||||
attestation: Attestation,
|
||||
committee_index: CommitteeIndex) -> bool:
|
||||
if not (
|
||||
attestation.data.index == committee_index
|
||||
and attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY == state.slot # Must be on-time attestation
|
||||
def is_on_time_attestation(state: BeaconState,
|
||||
attestation: Attestation) -> bool:
|
||||
"""
|
||||
Check if the given attestation is on-time.
|
||||
"""
|
||||
# TODO: MIN_ATTESTATION_INCLUSION_DELAY should always be 1
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
return attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY == state.slot
|
||||
```
|
||||
|
||||
#### `is_winning_attestation`
|
||||
|
@ -675,7 +712,6 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
|
|||
process_eth1_data(state, block.body)
|
||||
process_light_client_signatures(state, block.body)
|
||||
process_operations(state, block.body)
|
||||
verify_shard_transition_false_positives(state, block.body)
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
@ -699,7 +735,7 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
|
|||
# See custody game spec.
|
||||
process_custody_game_operations(state, body)
|
||||
|
||||
process_crosslinks(state, body.shard_transitions, body.attestations)
|
||||
process_shard_transitions(state, body.shard_transitions, body.attestations)
|
||||
|
||||
# TODO process_operations(body.shard_receipt_proofs, process_shard_receipt_proofs)
|
||||
```
|
||||
|
@ -730,7 +766,7 @@ def validate_attestation(state: BeaconState, attestation: Attestation) -> None:
|
|||
# Type 1: on-time attestations, the custody bits should be non-empty.
|
||||
if attestation.custody_bits_blocks != []:
|
||||
# Ensure on-time attestation
|
||||
assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY == state.slot
|
||||
assert is_on_time_attestation(state, attestation)
|
||||
# Correct data root count
|
||||
assert len(attestation.custody_bits_blocks) == len(get_offset_slots(state, shard))
|
||||
# Correct parent block root
|
||||
|
@ -746,6 +782,27 @@ def validate_attestation(state: BeaconState, attestation: Attestation) -> None:
|
|||
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
|
||||
```
|
||||
|
||||
###### Updated `process_attestation`
|
||||
|
||||
```python
|
||||
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
|
||||
validate_attestation(state, attestation)
|
||||
# Store pending attestation for epoch processing
|
||||
pending_attestation = PendingAttestation(
|
||||
aggregation_bits=attestation.aggregation_bits,
|
||||
data=attestation.data,
|
||||
inclusion_delay=state.slot - attestation.data.slot,
|
||||
proposer_index=get_beacon_proposer_index(state),
|
||||
crosslink_success=False, # To be filled in during process_shard_transitions
|
||||
)
|
||||
if attestation.data.target.epoch == get_current_epoch(state):
|
||||
state.current_epoch_attestations.append(pending_attestation)
|
||||
else:
|
||||
state.previous_epoch_attestations.append(pending_attestation)
|
||||
```
|
||||
|
||||
##### Shard transition processing
|
||||
|
||||
###### `apply_shard_transition`
|
||||
|
||||
```python
|
||||
|
@ -767,22 +824,23 @@ def apply_shard_transition(state: BeaconState, shard: Shard, transition: ShardTr
|
|||
proposers = []
|
||||
prev_gasprice = state.shard_states[shard].gasprice
|
||||
shard_parent_root = state.shard_states[shard].latest_block_root
|
||||
for i in range(len(offset_slots)):
|
||||
for i, offset_slot in enumerate(offset_slots):
|
||||
shard_block_length = transition.shard_block_lengths[i]
|
||||
shard_state = transition.shard_states[i]
|
||||
# Verify correct calculation of gas prices and slots
|
||||
assert shard_state.gasprice == compute_updated_gasprice(prev_gasprice, shard_block_length)
|
||||
assert shard_state.slot == offset_slots[i]
|
||||
assert shard_state.slot == offset_slot
|
||||
# Collect the non-empty proposals result
|
||||
is_empty_proposal = shard_block_length == 0
|
||||
if not is_empty_proposal:
|
||||
proposal_index = get_shard_proposer_index(state, offset_slots[i], shard)
|
||||
proposal_index = get_shard_proposer_index(state, offset_slot, shard)
|
||||
# Reconstruct shard headers
|
||||
header = ShardBlockHeader(
|
||||
shard_parent_root=shard_parent_root,
|
||||
beacon_parent_root=get_block_root_at_slot(state, offset_slots[i]),
|
||||
beacon_parent_root=get_block_root_at_slot(state, offset_slot),
|
||||
slot=offset_slot,
|
||||
shard=shard,
|
||||
proposer_index=proposal_index,
|
||||
slot=offset_slots[i],
|
||||
body_root=transition.shard_data_roots[i]
|
||||
)
|
||||
shard_parent_root = hash_tree_root(header)
|
||||
|
@ -801,7 +859,7 @@ def apply_shard_transition(state: BeaconState, shard: Shard, transition: ShardTr
|
|||
|
||||
# Save updated state
|
||||
state.shard_states[shard] = transition.shard_states[len(transition.shard_states) - 1]
|
||||
state.shard_states[shard].slot = state.slot - 1
|
||||
state.shard_states[shard].slot = compute_previous_slot(state.slot)
|
||||
```
|
||||
|
||||
###### `process_crosslink_for_shard`
|
||||
|
@ -823,7 +881,7 @@ def process_crosslink_for_shard(state: BeaconState,
|
|||
for attestation in transition_attestations:
|
||||
participants = get_attesting_indices(state, attestation.data, attestation.aggregation_bits)
|
||||
transition_participants = transition_participants.union(participants)
|
||||
assert attestation.data.head_shard_root == shard_transition.shard_data_roots[
|
||||
assert attestation.data.shard_head_root == shard_transition.shard_data_roots[
|
||||
len(shard_transition.shard_data_roots) - 1
|
||||
]
|
||||
|
||||
|
@ -872,13 +930,12 @@ def process_crosslinks(state: BeaconState,
|
|||
for committee_index in map(CommitteeIndex, range(committee_count)):
|
||||
shard = compute_shard_from_committee_index(state, committee_index, state.slot)
|
||||
# All attestations in the block for this committee/shard and current slot
|
||||
shard_transition = shard_transitions[shard]
|
||||
shard_attestations = [
|
||||
attestation for attestation in attestations
|
||||
if is_shard_attestation(state, attestation, committee_index)
|
||||
if is_on_time_attestation(state, attestation) and attestation.data.index == committee_index
|
||||
]
|
||||
|
||||
winning_root = process_crosslink_for_shard(state, committee_index, shard_transition, shard_attestations)
|
||||
winning_root = process_crosslink_for_shard(state, committee_index, shard_transitions[shard], shard_attestations)
|
||||
if winning_root != Root():
|
||||
# Mark relevant pending attestations as creating a successful crosslink
|
||||
for pending_attestation in state.current_epoch_attestations:
|
||||
|
@ -886,23 +943,30 @@ def process_crosslinks(state: BeaconState,
|
|||
pending_attestation.crosslink_success = True
|
||||
```
|
||||
|
||||
###### `process_attestation`
|
||||
###### `verify_empty_shard_transition`
|
||||
|
||||
```python
|
||||
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
|
||||
validate_attestation(state, attestation)
|
||||
# Store pending attestation for epoch processing
|
||||
pending_attestation = PendingAttestation(
|
||||
aggregation_bits=attestation.aggregation_bits,
|
||||
data=attestation.data,
|
||||
inclusion_delay=state.slot - attestation.data.slot,
|
||||
proposer_index=get_beacon_proposer_index(state),
|
||||
crosslink_success=False, # To be filled in during process_crosslinks
|
||||
)
|
||||
if attestation.data.target.epoch == get_current_epoch(state):
|
||||
state.current_epoch_attestations.append(pending_attestation)
|
||||
else:
|
||||
state.previous_epoch_attestations.append(pending_attestation)
|
||||
def verify_empty_shard_transition(state: BeaconState, shard_transitions: Sequence[ShardTransition]) -> bool:
|
||||
"""
|
||||
Verify that a `shard_transition` in a block is empty if an attestation was not processed for it.
|
||||
"""
|
||||
for shard in range(get_active_shard_count(state)):
|
||||
if state.shard_states[shard].slot != compute_previous_slot(state.slot):
|
||||
if shard_transitions[shard] != ShardTransition():
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
###### `process_shard_transitions`
|
||||
|
||||
```python
|
||||
def process_shard_transitions(state: BeaconState,
|
||||
shard_transitions: Sequence[ShardTransition],
|
||||
attestations: Sequence[Attestation]) -> None:
|
||||
# Process crosslinks
|
||||
process_crosslinks(state, shard_transitions, attestations)
|
||||
# Verify the empty proposal shard states
|
||||
assert verify_empty_shard_transition(state, shard_transitions)
|
||||
```
|
||||
|
||||
##### New Attester slashing processing
|
||||
|
@ -910,11 +974,9 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None:
|
|||
```python
|
||||
def get_indices_from_committee(
|
||||
committee: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE],
|
||||
bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]) -> List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE]:
|
||||
bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]) -> Sequence[ValidatorIndex]:
|
||||
assert len(bits) == len(committee)
|
||||
return List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE](
|
||||
[validator_index for i, validator_index in enumerate(committee) if bits[i]]
|
||||
)
|
||||
return [validator_index for i, validator_index in enumerate(committee) if bits[i]]
|
||||
```
|
||||
|
||||
```python
|
||||
|
@ -947,16 +1009,6 @@ def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSla
|
|||
assert slashed_any
|
||||
```
|
||||
|
||||
#### Shard transition false positives
|
||||
|
||||
```python
|
||||
def verify_shard_transition_false_positives(state: BeaconState, block_body: BeaconBlockBody) -> None:
|
||||
# Verify that a `shard_transition` in a block is empty if an attestation was not processed for it
|
||||
for shard in range(get_active_shard_count(state)):
|
||||
if state.shard_states[shard].slot != state.slot - 1:
|
||||
assert block_body.shard_transitions[shard] == ShardTransition()
|
||||
```
|
||||
|
||||
#### Light client processing
|
||||
|
||||
```python
|
||||
|
@ -997,7 +1049,7 @@ def process_epoch(state: BeaconState) -> None:
|
|||
|
||||
#### Custody game updates
|
||||
|
||||
`process_reveal_deadlines` and `process_custody_final_updates` are defined in [the Custody Game spec](./1_custody-game.md),
|
||||
`process_reveal_deadlines` and `process_custody_final_updates` are defined in [the Custody Game spec](./custody-game.md),
|
||||
|
||||
#### Online-tracking
|
||||
|
||||
|
@ -1018,7 +1070,9 @@ def process_online_tracking(state: BeaconState) -> None:
|
|||
|
||||
```python
|
||||
def process_light_client_committee_updates(state: BeaconState) -> None:
|
||||
# Update light client committees
|
||||
"""
|
||||
Update light client committees.
|
||||
"""
|
||||
if get_current_epoch(state) % LIGHT_CLIENT_COMMITTEE_PERIOD == 0:
|
||||
state.current_light_committee = state.next_light_committee
|
||||
new_committee = get_light_client_committee(state, get_current_epoch(state) + LIGHT_CLIENT_COMMITTEE_PERIOD)
|
||||
|
|
|
@ -50,6 +50,7 @@ def verify_shard_block_message(beacon_state: BeaconState,
|
|||
shard: Shard) -> bool:
|
||||
assert block.shard_parent_root == shard_state.latest_block_root
|
||||
assert block.slot == slot
|
||||
assert block.shard == shard
|
||||
assert block.proposer_index == get_shard_proposer_index(beacon_state, slot, shard)
|
||||
assert 0 < len(block.body) <= MAX_SHARD_BLOCK_SIZE
|
||||
return True
|
||||
|
@ -70,22 +71,23 @@ def verify_shard_block_signature(beacon_state: BeaconState,
|
|||
def shard_state_transition(beacon_state: BeaconState,
|
||||
shard_state: ShardState,
|
||||
block: ShardBlock) -> None:
|
||||
# Update shard state
|
||||
"""
|
||||
Update ``shard_state`` with shard ``block`` and ``beacon_state`.
|
||||
"""
|
||||
shard_state.slot = block.slot
|
||||
prev_gasprice = shard_state.gasprice
|
||||
shard_state.gasprice = compute_updated_gasprice(prev_gasprice, len(block.body))
|
||||
if len(block.body) == 0:
|
||||
latest_block_root = shard_state.latest_block_root
|
||||
else:
|
||||
latest_block_root = hash_tree_root(block)
|
||||
|
||||
shard_state.latest_block_root = latest_block_root
|
||||
shard_state.transition_digest = compute_shard_transition_digest(
|
||||
beacon_state,
|
||||
shard_state,
|
||||
block.beacon_parent_root,
|
||||
block.body,
|
||||
hash_tree_root(block.body),
|
||||
)
|
||||
shard_state.gasprice = compute_updated_gasprice(prev_gasprice, len(block.body))
|
||||
shard_state.slot = block.slot
|
||||
shard_state.latest_block_root = latest_block_root
|
||||
```
|
||||
|
||||
We have a pure function `get_post_shard_state` for describing the fraud proof verification and honest validator behavior.
|
||||
|
@ -268,12 +270,8 @@ def get_shard_transition(beacon_state: BeaconState,
|
|||
shard: Shard,
|
||||
shard_blocks: Sequence[SignedShardBlock]) -> ShardTransition:
|
||||
offset_slots = get_offset_slots(beacon_state, shard)
|
||||
start_slot = offset_slots[0]
|
||||
proposals, shard_states, shard_data_roots = get_shard_state_transition_result(beacon_state, shard, shard_blocks)
|
||||
|
||||
assert len(proposals) > 0
|
||||
assert len(shard_data_roots) > 0
|
||||
|
||||
shard_block_lengths = []
|
||||
proposer_signatures = []
|
||||
for proposal in proposals:
|
||||
|
@ -287,7 +285,7 @@ def get_shard_transition(beacon_state: BeaconState,
|
|||
proposer_signature_aggregate = NO_SIGNATURE
|
||||
|
||||
return ShardTransition(
|
||||
start_slot=start_slot,
|
||||
start_slot=offset_slots[0],
|
||||
shard_block_lengths=shard_block_lengths,
|
||||
shard_data_roots=shard_data_roots,
|
||||
shard_states=shard_states,
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.12.0
|
||||
0.12.1
|
|
@ -21,7 +21,7 @@ def test_initialize_beacon_state_from_eth1(spec):
|
|||
# initialize beacon_state
|
||||
state = spec.initialize_beacon_state_from_eth1(eth1_block_hash, eth1_timestamp, deposits)
|
||||
|
||||
assert state.genesis_time == eth1_timestamp - eth1_timestamp % spec.MIN_GENESIS_DELAY + 2 * spec.MIN_GENESIS_DELAY
|
||||
assert state.genesis_time == eth1_timestamp + spec.GENESIS_DELAY
|
||||
assert len(state.validators) == deposit_count
|
||||
assert state.eth1_data.deposit_root == deposit_root
|
||||
assert state.eth1_data.deposit_count == deposit_count
|
||||
|
@ -57,7 +57,7 @@ def test_initialize_beacon_state_some_small_balances(spec):
|
|||
# initialize beacon_state
|
||||
state = spec.initialize_beacon_state_from_eth1(eth1_block_hash, eth1_timestamp, deposits)
|
||||
|
||||
assert state.genesis_time == eth1_timestamp - eth1_timestamp % spec.MIN_GENESIS_DELAY + 2 * spec.MIN_GENESIS_DELAY
|
||||
assert state.genesis_time == eth1_timestamp + spec.GENESIS_DELAY
|
||||
assert len(state.validators) == small_deposit_count
|
||||
assert state.eth1_data.deposit_root == deposit_root
|
||||
assert state.eth1_data.deposit_count == len(deposits)
|
||||
|
|
|
@ -78,7 +78,7 @@ def build_attestation_data(spec, state, slot, index, shard_transition=None, on_t
|
|||
if spec.fork == PHASE1:
|
||||
if shard_transition is not None:
|
||||
lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1
|
||||
attestation_data.head_shard_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
|
||||
attestation_data.shard_head_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
|
||||
attestation_data.shard_transition_root = shard_transition.hash_tree_root()
|
||||
else:
|
||||
# No shard transition
|
||||
|
@ -88,10 +88,10 @@ def build_attestation_data(spec, state, slot, index, shard_transition=None, on_t
|
|||
next_slot(spec, temp_state)
|
||||
shard_transition = spec.get_shard_transition(temp_state, shard, [])
|
||||
lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1
|
||||
attestation_data.head_shard_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
|
||||
attestation_data.shard_head_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
|
||||
attestation_data.shard_transition_root = shard_transition.hash_tree_root()
|
||||
else:
|
||||
attestation_data.head_shard_root = state.shard_states[shard].transition_digest
|
||||
attestation_data.shard_head_root = state.shard_states[shard].transition_digest
|
||||
attestation_data.shard_transition_root = spec.Root()
|
||||
return attestation_data
|
||||
|
||||
|
@ -211,6 +211,9 @@ def sign_indexed_attestation(spec, state, indexed_attestation):
|
|||
indexed_attestation.attestation.aggregation_bits,
|
||||
)
|
||||
data = indexed_attestation.attestation.data
|
||||
if any(indexed_attestation.attestation.custody_bits_blocks):
|
||||
sign_on_time_attestation(spec, state, indexed_attestation.attestation)
|
||||
else:
|
||||
indexed_attestation.attestation.signature = sign_aggregate_attestation(spec, state, data, participants)
|
||||
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ def build_shard_block(spec,
|
|||
shard_parent_root=shard_state.latest_block_root,
|
||||
beacon_parent_root=beacon_parent_root,
|
||||
slot=slot,
|
||||
shard=shard,
|
||||
proposer_index=proposer_index,
|
||||
body=body,
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from eth2spec.test.context import expect_assertion_error
|
||||
|
||||
|
||||
def run_crosslinks_processing(spec, state, shard_transitions, attestations, valid=True):
|
||||
def run_shard_transitions_processing(spec, state, shard_transitions, attestations, valid=True):
|
||||
"""
|
||||
Run ``process_attestation``, yielding:
|
||||
Run ``process_shard_transitions``, yielding:
|
||||
- pre-state ('pre')
|
||||
- shard_transitions ('shard_transitions')
|
||||
- attestations ('attestations')
|
||||
|
@ -17,12 +17,12 @@ def run_crosslinks_processing(spec, state, shard_transitions, attestations, vali
|
|||
|
||||
# If the attestation is invalid, processing is aborted, and there is no post-state.
|
||||
if not valid:
|
||||
expect_assertion_error(lambda: spec.process_crosslinks(state, shard_transitions, attestations))
|
||||
expect_assertion_error(lambda: spec.process_shard_transitions(state, shard_transitions, attestations))
|
||||
yield 'post', None
|
||||
return
|
||||
|
||||
# process crosslinks
|
||||
spec.process_crosslinks(state, shard_transitions, attestations)
|
||||
spec.process_shard_transitions(state, shard_transitions, attestations)
|
||||
|
||||
# yield post-state
|
||||
yield 'post', state
|
|
@ -1,6 +1,6 @@
|
|||
from eth2spec.test.context import (
|
||||
PHASE0, PHASE1,
|
||||
spec_state_test, expect_assertion_error, always_bls, never_bls, with_all_phases, with_phases
|
||||
spec_state_test, expect_assertion_error, always_bls, with_all_phases, with_phases
|
||||
)
|
||||
from eth2spec.test.helpers.attestations import sign_indexed_attestation
|
||||
from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing, \
|
||||
|
@ -89,7 +89,6 @@ def test_success_double(spec, state):
|
|||
|
||||
@with_all_phases
|
||||
@spec_state_test
|
||||
@never_bls
|
||||
def test_success_surround(spec, state):
|
||||
next_epoch_via_block(spec, state)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from eth2spec.test.context import (
|
|||
spec_state_test,
|
||||
always_bls,
|
||||
)
|
||||
from eth2spec.test.helpers.crosslinks import run_crosslinks_processing
|
||||
from eth2spec.test.helpers.shard_transitions import run_shard_transitions_processing
|
||||
from eth2spec.test.helpers.shard_block import (
|
||||
build_attestation_with_shard_transition,
|
||||
build_shard_block,
|
||||
|
@ -46,7 +46,7 @@ def run_basic_crosslink_tests(spec, state, target_len_offset_slot, valid=True):
|
|||
transition_to(spec, state, state.slot + target_len_offset_slot)
|
||||
pre_shard_state = state.shard_states[shard]
|
||||
|
||||
yield from run_crosslinks_processing(spec, state, shard_transitions, [attestation], valid=valid)
|
||||
yield from run_shard_transitions_processing(spec, state, shard_transitions, [attestation], valid=valid)
|
||||
|
||||
if valid:
|
||||
# After state transition,
|
|
@ -184,7 +184,7 @@ def create_provider(handler_name: str, tests_src, config_name: str) -> gen_typin
|
|||
|
||||
if __name__ == "__main__":
|
||||
gen_runner.run_generator("epoch_processing", [
|
||||
create_provider('crosslinks', test_process_crosslinks, 'minimal'),
|
||||
create_provider('final_updates', test_process_final_updates, 'minimal'),
|
||||
...
|
||||
])
|
||||
|
||||
|
|
Loading…
Reference in New Issue