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

First draft of the PoC interactive game #645

Closed
wants to merge 12 commits into from
243 changes: 243 additions & 0 deletions specs/core/1_shard-data-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,246 @@ The `shard_chain_commitment` is only valid if it equals `compute_commitment(head
### Shard block fork choice rule

The fork choice rule for any shard is LMD GHOST using the shard chain attestations of the persistent committee and the beacon chain attestations of the crosslink committee currently assigned to that shard, but instead of being rooted in the genesis it is rooted in the block referenced in the most recent accepted crosslink (ie. `state.crosslinks[shard].shard_block_root`). Only blocks whose `beacon_chain_ref` is the block in the main beacon chain at the specified `slot` should be considered (if the beacon chain skips a slot, then the block at that slot is considered to be the block in the beacon chain at the highest slot lower than a slot).

# Proof of custody interactive game

### Constants

| Constant | Value | Unit | Approximation |
|--------------------------------------------|------------------|---------|---------------|
| `MAX_POC_RESPONSE_DEPTH` | 5 | layers | |
| `DOMAIN_CUSTODY_INTERACTIVE` | 132 | | |
| `VALIDATOR_NULL` | 2**64 - 1 | | |
| `MAX_INTERACTIVE_CHALLENGE_INITIATIONS` | 2 | | |
| `MAX_INTERACTIVE_CHALLENGE_RESPONSES` | 16 | | |
| `MAX_INTERACTIVE_CHALLENGE_CONTINUTATIONS` | 16 | | |

### Data structures and verification

Add the following data structure to the `Validator` record:

```python
interactive_custody_challenge_data: InteractiveCustodyChallengeData,
now_challenging: 'uint64',
```

Where `InteractiveCustodyChallengeData` is defined as follows:

```python
{
# Who initiated the challenge
'challenger': 'uint64',
# Initial data root
'data_root': 'bytes32',
# Initial custody bit
'custody_bit': 'bool',
# Responder subkey
'responder_subkey': 'bytes96',
# The hash in the PoC tree in the position that we are currently at
'current_custody_tree_node': 'bytes32',
# The position in the tree, in terms of depth and position offset
'depth': 'uint64',
'offset': 'uint64',
# Max depth of the branch
'max_depth': 'uint64',
# Deadline to respond (as an epoch)
'deadline': 'uint64',
}
```

The initial value is `EMPTY_CHALLENGE_DATA = InteractiveCustodyChallengeData(challenger=VALIDATOR_NULL, data_root=ZERO_HASH, custody_bit=False, responder_subkey=EMPTY_SIGNATURE, current_custody_tree_node=ZERO_HASH, depth=0, offset=0, max_depth=0, deadline=0)`

We define an `InteractiveCustodyChallengeInitiation` as follows:

```python
{
'attestation': SlashableAttestation,
'responder_index': 'uint64',
'challenger_index': 'uint64',
'responder_subkey': 'bytes96',
'signature': 'bytes96'
}
```

Here's the function for validating and processing an initiation:

```python
def process_initiation(initiation: InteractiveCustodyChallengeInitiation,
state: BeaconState):
challenger = state.validator_registry[challenger_index]
responder = state.validator_registry[responder_index]
# Verify the signature
assert bls_verify(
message_hash=signed_root(initiation, 'signature'),
pubkey=state.validator_registry[challenger_index].pubkey,
signature=initiation.signature,
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_INTERACTIVE)
)
# Check that the responder actually participated in the attestation
assert responder_index in attestation.validator_indices
# Can only be challenged by one challenger at a time
assert responder.interactive_custody_challenge_data.challenger_index == VALIDATOR_NULL
# Can only challenge one responder at a time
assert challenger.now_challenging == VALIDATOR_NULL
# Can't challenge if you've been penalized
assert challenger.penalized_epoch == FAR_FUTURE_EPOCH
# Make sure the revealed subkey is valid
assert verify_custody_subkey_reveal(
pubkey=state.validator_registry[responder_index].pubkey,
subkey=responder_subkey,
mask=ZERO_HASH,
mask_pubkey=b'',
period=slot_to_custody_period(attestation.data.slot)
)
# Set the challenge object
responder.interactive_custody_challenge_data = InteractiveCustodyChallengeData(
challenger=initiation.challenger_index,
data_root=attestation.custody_commitment,
custody_bit=get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)),
responder_subkey=responder_subkey,
current_custody_tree_node=ZERO_HASH,
depth=0,
offset=0,
max_depth=get_merkle_depth(initiation.attestation),
deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE
)
# Responder can't withdraw yet!
state.validator_registry[responder_index].withdrawable_epoch = FAR_FUTURE_EPOCH
# Challenger can't challenge anyone else
challenger.now_challenging = responder_index
```

We define an `InteractiveCustodyChallengeResponse` as follows:

```python
{
'responder_index': 'uint64',
'hashes': ['bytes32'],
'signature': 'bytes96',
}
```

A response provides 32 hashes that are under current known proof of custody tree node. Note that at the beginning the tree node is just one bit of the custody root, so we ask the responder to sign to commit to the top 5 levels of the tree and therefore the root hash; at all other stages in the game responses are self-verifying.

Here's the function for verifying and processing a response:

```python
def process_response(response: InteractiveCustodyChallengeResponse,
state: State):
responder = state.validator_registry[response.responder_index]
challenge_data = responder.interactive_custody_challenge_data
# Check that the right number of hashes was provided
expected_depth = min(challenge_data.max_depth - challenge_data.depth, MAX_POC_RESPONSE_DEPTH)
assert 2**expected_depth == len(response.hashes)
# Must make some progress!
assert expected_depth > 0
# Check the hashes match the previously provided root
root = merkle_root(response.hashes)
# If this is the first response check the bit and the signature and set the root
if challenge_data.depth == 0:
vbuterin marked this conversation as resolved.
Show resolved Hide resolved
assert get_bitfield_bit(root, 0) == challenge_data.custody_bit
assert bls_verify(
message_hash=signed_root(response, 'signature'),
pubkey=responder.pubkey,
signature=response.signature,
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_INTERACTIVE)
)
challenge_data.current_custody_tree_node = root
# Otherwise just check the response against the root
else:
assert root == challenge_data.current_custody_tree_node
# Update challenge data
challenge_data.deadline=FAR_FUTURE_EPOCH
responder.withdrawable_epoch = get_current_epoch(state) + MAX_POC_RESPONSE_DEPTH
```

Once a response provides 32 hashes, the challenger has the right to choose any one of them that they feel is constructed incorrectly to continue the game. Note that eventually, the game will get to the point where the `new_custody_tree_node` is a leaf node. We define an `InteractiveCustodyChallengeContinuation` object as follows:

```python
{
'challenger_index: 'uint64',
'responder_index': 'uint64',
'sub_index': 'uint64',
'new_custody_tree_node': 'bytes32',
'proof': ['bytes32'],
'signature': 'bytes96'
}
```

Here's the function for verifying and processing a continuation challenge:

```python
def process_continuation(continuation: InteractiveCustodyChallengeContinuation,
state: State):
responder = state.validator_registry[continuation.responder_index]
challenge_data = responder.interactive_custody_challenge_data
expected_depth = min(challenge_data.max_depth - challenge_data.depth, MAX_POC_RESPONSE_DEPTH)
# Verify we're not too late
assert get_current_epoch(state) < responder.withdrawable_epoch
# Verify the Merkle branch (the previous custody response provided the next level of hashes so the
# challenger has the info to make any Merkle branch)
assert verify_merkle_branch(
leaf=new_custody_tree_node,
branch=continuation.proof,
depth=expected_depth,
index=sub_index,
root=challenge_data.current_custody_tree_node
)
# Verify signature
assert bls_verify(message_hash=signed_root(continutation, 'signature'),
pubkey=responder.pubkey,
signature=continutation.signature,
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_INTERACTIVE))
# Update the challenge data
challenge_data.current_custody_tree_node = continuation.new_custody_tree_node
challenge_data.depth += expected_depth
challenge_data.deadline = get_current_epoch(state) + MAX_POC_RESPONSE_DEPTH
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
challenge_data.offset = challenger_data.offset * 2**expected_depth + sub_index
```

Once the `new_custody_tree_node` reaches the leaves of the tree, the responder can no longer provide a valid `InteractiveCustodyChallengeResponse`; instead, the responder or the challenger must provide a branch response that provides a branch of the original data tree, at which point the custody leaf equation can be checked and either side of the custody game can "conclusively win".

```python
def process_branch_response(response: BranchResponse,
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
state: State):
responder = state.validator_registry[response.responder_index]
challenge_data = responder.interactive_custody_challenge_data
assert challenge_data.depth == challenge_data.max_depth
# Verify we're not too late
assert get_current_epoch(state) < responder.withdrawable_epoch
# Verify the Merkle branch *of the data tree*
assert verify_merkle_branch(
leaf=response.data,
branch=response.branch,
depth=challenge_data.max_depth,
index=challenge_data.offset,
root=challenge_data.data_root
)
# Responder wins
if hash(challenge_data.responder_subkey + response.data) == challenge_data.current_custody_tree_node:
penalize_validator(state, challenge_data.challenger_index, response.responder_index)
responder.interactive_custody_challenge_data = EMPTY_CHALLENGE_DATA
# Challenger wins
else:
penalize_validator(state, response.responder_index, challenge_data.challenger_index)
state.validator_registry[challenge_data.challenger_index].now_challenging = VALIDATOR_NULL
```

Amend `process_challenge_absences` as follows:

```python
def process_challenge_absences(state: BeaconState) -> None:
"""
Iterate through the validator registry
and penalize validators with balance that did not answer challenges.
"""
for index, validator in enumerate(state.validator_registry):
if len(validator.open_branch_challenges) > 0 and get_current_epoch(state) > validator.open_branch_challenges[0].inclusion_epoch + CHALLENGE_RESPONSE_DEADLINE:
penalize_validator(state, index, validator.open_branch_challenges[0].challenger_index)
if validator.challenge_data.challenger != VALIDATOR_NULL and get_current_epoch(state) > validator.challenge.deadline:
penalize_validator(state, index, validator.challenge_data.challenger_index)
if get_current_epoch(state) >= state.validator_registry[validator.now_challenging].withdrawal_epoch:
penalize_validator(state, index, validator.now_challenging)
penalize_validator(state, index, validator.challenge_data.challenger_index)
```