mirror of https://github.com/hyperledger/besu
EIP-7002: Validator Exit contract helper and adding exits to created blocks (#6883)
Signed-off-by: Lucas Saldanha <lucascrsaldanha@gmail.com>pull/6964/head
parent
f68db3801b
commit
61432831d5
File diff suppressed because one or more lines are too long
@ -0,0 +1,42 @@ |
||||
{ |
||||
"request": { |
||||
"jsonrpc": "2.0", |
||||
"method": "engine_newPayloadV3", |
||||
"params": [ |
||||
{ |
||||
"parentHash": "0x2b3ae3a4c482f3dab43f0606af50dc8fd3ab981ba0659d477fa96955927736ae", |
||||
"feeRecipient": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", |
||||
"stateRoot": "0xd5d6e8c8d57e328871c5b81f078ab69e02466ab0e487c2c597effb4ffc185384", |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", |
||||
"gasLimit": "0x1c9c380", |
||||
"gasUsed": "0x0", |
||||
"timestamp": "0x30", |
||||
"extraData": "0x", |
||||
"baseFeePerGas": "0x7", |
||||
"transactions": [], |
||||
"withdrawals": [], |
||||
"depositReceipts": [], |
||||
"exits": [], |
||||
"blockNumber": "0x3", |
||||
"blockHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", |
||||
"excessBlobGas": "0x0", |
||||
"blobGasUsed": "0x0" |
||||
}, |
||||
[], |
||||
"0x0000000000000000000000000000000000000000000000000000000000000000" |
||||
], |
||||
"id": 67 |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"result": { |
||||
"status": "VALID", |
||||
"latestValidHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"validationError": null |
||||
} |
||||
}, |
||||
"statusCode": 200 |
||||
} |
@ -0,0 +1,14 @@ |
||||
{ |
||||
"request": { |
||||
"jsonrpc": "2.0", |
||||
"method": "eth_sendRawTransaction", |
||||
"params": ["0xf8978085e8d4a51000832dc6c0940f1ee3e66777f27a7703400644c6fce41527e01702b08706d19a62f28a6a6549f96c5adaebac9124a61d44868ec94f6d2d707c6a2f82c9162071231dfeb40e24bfde4ffdf243822fdfa01527e82d4155c70f3dc6c1df4ba26f9fb9d7cea03a402a17d630dd5465a82a9aa0378b1a45916be48d98b8ef547df0daf34f2e85037360887d954ccacdc069b222"], |
||||
"id": 67 |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"result": "0xf4aaedb9020f067d720daf555a4ccb6756741365defb4cd9c94c5ba39d64a5e5" |
||||
}, |
||||
"statusCode": 200 |
||||
} |
@ -0,0 +1,34 @@ |
||||
{ |
||||
"request": { |
||||
"jsonrpc": "2.0", |
||||
"method": "engine_forkchoiceUpdatedV3", |
||||
"params": [ |
||||
{ |
||||
"headBlockHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"safeBlockHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"finalizedBlockHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771" |
||||
}, |
||||
{ |
||||
"timestamp": "0x40", |
||||
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", |
||||
"suggestedFeeRecipient": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", |
||||
"withdrawals": [], |
||||
"parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" |
||||
} |
||||
], |
||||
"id": 67 |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"result": { |
||||
"payloadStatus": { |
||||
"status": "VALID", |
||||
"latestValidHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"validationError": null |
||||
}, |
||||
"payloadId": "0x282643bbede61941" |
||||
} |
||||
}, |
||||
"statusCode" : 200 |
||||
} |
@ -0,0 +1,54 @@ |
||||
{ |
||||
"request": { |
||||
"jsonrpc": "2.0", |
||||
"method": "engine_getPayloadV4", |
||||
"params": [ |
||||
"0x282643bbede61941" |
||||
], |
||||
"id": 67 |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 67, |
||||
"result": { |
||||
"executionPayload": { |
||||
"parentHash": "0x0bd5e56ac3552719a1af061ec3f48248e817fc8ac7306d611d195ae023e9f771", |
||||
"feeRecipient": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", |
||||
"stateRoot": "0x99b256355fb804ab33458099469f9a2904b4b4e9171d023334b84d3f0e3a8d43", |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000", |
||||
"gasLimit": "0x1c9c380", |
||||
"gasUsed": "0x145b3", |
||||
"timestamp": "0x40", |
||||
"extraData": "0x", |
||||
"baseFeePerGas": "0x7", |
||||
"excessBlobGas": "0x0", |
||||
"parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", |
||||
"transactions": [ |
||||
"0xf8978085e8d4a51000832dc6c0940f1ee3e66777f27a7703400644c6fce41527e01702b08706d19a62f28a6a6549f96c5adaebac9124a61d44868ec94f6d2d707c6a2f82c9162071231dfeb40e24bfde4ffdf243822fdfa01527e82d4155c70f3dc6c1df4ba26f9fb9d7cea03a402a17d630dd5465a82a9aa0378b1a45916be48d98b8ef547df0daf34f2e85037360887d954ccacdc069b222" |
||||
], |
||||
"withdrawals": [], |
||||
"depositReceipts": [], |
||||
"exits": [ |
||||
{ |
||||
"sourceAddress": "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", |
||||
"validatorPubKey": "0x8706d19a62f28a6a6549f96c5adaebac9124a61d44868ec94f6d2d707c6a2f82c9162071231dfeb40e24bfde4ffdf243" |
||||
} |
||||
], |
||||
"receiptsRoot": "0xf2e2f11f0c553ed811be4460880996149ab3947bd0d2c1330457925a11254514", |
||||
"blobGasUsed": "0x0", |
||||
"blockHash": "0xb26d2fa98315d4d4cdcae8e5590964787b3343c11ff64eb548179687a612d467", |
||||
"blockNumber": "0x4" |
||||
}, |
||||
"blockValue": "0x12838c23cb1481b", |
||||
"blobsBundle": { |
||||
"commitments": [], |
||||
"proofs": [], |
||||
"blobs": [] |
||||
}, |
||||
"shouldOverrideBuilder": false |
||||
} |
||||
}, |
||||
"statusCode": 200, |
||||
"waitTime": 1500 |
||||
} |
@ -0,0 +1,85 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
|
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import org.hyperledger.besu.datatypes.Hash; |
||||
import org.hyperledger.besu.ethereum.core.Block; |
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
public class PragueValidatorExitsValidator implements ValidatorExitsValidator { |
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PragueValidatorExitsValidator.class); |
||||
|
||||
@Override |
||||
public boolean allowValidatorExits() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public boolean validateValidatorExitParameter( |
||||
final Optional<List<ValidatorExit>> validatorExits) { |
||||
return validatorExits.isPresent(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean validateExitsInBlock(final Block block, final List<ValidatorExit> expectedExits) { |
||||
final Hash blockHash = block.getHash(); |
||||
|
||||
if (block.getHeader().getExitsRoot().isEmpty()) { |
||||
LOG.warn("Block {} must contain exits_root", blockHash); |
||||
return false; |
||||
} |
||||
|
||||
if (block.getBody().getExits().isEmpty()) { |
||||
LOG.warn("Block {} must contain exits (even if empty list)", blockHash); |
||||
return false; |
||||
} |
||||
|
||||
final List<ValidatorExit> exitsInBlock = block.getBody().getExits().get(); |
||||
// TODO Do we need to allow for customization? (e.g. if the value changes in the next fork)
|
||||
if (exitsInBlock.size() > ValidatorExitContractHelper.MAX_EXITS_PER_BLOCK) { |
||||
LOG.warn("Block {} has more than the allowed maximum number of exits", blockHash); |
||||
return false; |
||||
} |
||||
|
||||
// Validate exits_root
|
||||
final Hash expectedExitsRoot = BodyValidation.exitsRoot(exitsInBlock); |
||||
if (!expectedExitsRoot.equals(block.getHeader().getExitsRoot().get())) { |
||||
LOG.warn( |
||||
"Block {} exits_root does not match expected hash root for exits in block", blockHash); |
||||
return false; |
||||
} |
||||
|
||||
// Validate exits
|
||||
final boolean expectedExitsMatch = expectedExits.equals(exitsInBlock); |
||||
if (!expectedExitsMatch) { |
||||
LOG.warn( |
||||
"Block {} has a mismatch between its exits and expected exits (in_block = {}, expected = {})", |
||||
blockHash, |
||||
exitsInBlock, |
||||
expectedExits); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,146 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import org.hyperledger.besu.datatypes.Address; |
||||
import org.hyperledger.besu.datatypes.BLSPublicKey; |
||||
import org.hyperledger.besu.datatypes.Hash; |
||||
import org.hyperledger.besu.ethereum.core.MutableWorldState; |
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
import org.hyperledger.besu.evm.account.Account; |
||||
import org.hyperledger.besu.evm.account.MutableAccount; |
||||
import org.hyperledger.besu.evm.worldstate.WorldUpdater; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import com.google.common.annotations.VisibleForTesting; |
||||
import org.apache.tuweni.bytes.Bytes; |
||||
import org.apache.tuweni.units.bigints.UInt256; |
||||
|
||||
/** |
||||
* Helper for interacting with the Validator Exit Contract (https://eips.ethereum.org/EIPS/eip-7002)
|
||||
* |
||||
* <p>TODO: Please note that this is not the spec-way of interacting with the Validator Exit |
||||
* contract. See https://github.com/hyperledger/besu/issues/6918 for more information.
|
||||
*/ |
||||
public class ValidatorExitContractHelper { |
||||
|
||||
public static final Address VALIDATOR_EXIT_ADDRESS = |
||||
Address.fromHexString("0x0f1ee3e66777F27a7703400644C6fCE41527E017"); |
||||
|
||||
@VisibleForTesting |
||||
// Storage slot to store the difference between number of exits since last block and target exits
|
||||
// per block
|
||||
static final UInt256 EXCESS_EXITS_STORAGE_SLOT = UInt256.valueOf(0L); |
||||
|
||||
@VisibleForTesting |
||||
// Storage slot to store the number of exits added since last block
|
||||
static final UInt256 EXIT_COUNT_STORAGE_SLOT = UInt256.valueOf(1L); |
||||
|
||||
@VisibleForTesting |
||||
static final UInt256 EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT = UInt256.valueOf(2L); |
||||
|
||||
@VisibleForTesting |
||||
static final UInt256 EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT = UInt256.valueOf(3L); |
||||
|
||||
private static final UInt256 EXIT_MESSAGE_QUEUE_STORAGE_OFFSET = UInt256.valueOf(4L); |
||||
// How many slots each exit occupies in the account state
|
||||
private static final int EXIT_MESSAGE_STORAGE_SLOT_SIZE = 3; |
||||
@VisibleForTesting static final int MAX_EXITS_PER_BLOCK = 16; |
||||
private static final int TARGET_EXITS_PER_BLOCK = 2; |
||||
|
||||
/* |
||||
Pop the expected list of exits from the validator exit smart contract, updating the queue pointers and other |
||||
control variables in the contract state. |
||||
*/ |
||||
public static List<ValidatorExit> popExitsFromQueue(final MutableWorldState mutableWorldState) { |
||||
final WorldUpdater worldUpdater = mutableWorldState.updater(); |
||||
final MutableAccount account = worldUpdater.getAccount(VALIDATOR_EXIT_ADDRESS); |
||||
if (Hash.EMPTY.equals(account.getCodeHash())) { |
||||
return List.of(); |
||||
} |
||||
|
||||
final List<ValidatorExit> exits = dequeueExits(account); |
||||
updateExcessExits(account); |
||||
resetExitCount(account); |
||||
|
||||
worldUpdater.commit(); |
||||
|
||||
return exits; |
||||
} |
||||
|
||||
private static List<ValidatorExit> dequeueExits(final MutableAccount account) { |
||||
final UInt256 queueHeadIndex = account.getStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT); |
||||
final UInt256 queueTailIndex = account.getStorageValue(EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT); |
||||
|
||||
final List<ValidatorExit> exits = peekExpectedExits(account, queueHeadIndex, queueTailIndex); |
||||
|
||||
final UInt256 newQueueHeadIndex = queueHeadIndex.plus(exits.size()); |
||||
if (newQueueHeadIndex.equals(queueTailIndex)) { |
||||
// Queue is empty, reset queue pointers
|
||||
account.setStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, UInt256.valueOf(0L)); |
||||
account.setStorageValue(EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, UInt256.valueOf(0L)); |
||||
} else { |
||||
account.setStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, newQueueHeadIndex); |
||||
} |
||||
|
||||
return exits; |
||||
} |
||||
|
||||
private static List<ValidatorExit> peekExpectedExits( |
||||
final Account account, final UInt256 queueHeadIndex, final UInt256 queueTailIndex) { |
||||
final long numExitsInQueue = queueTailIndex.subtract(queueHeadIndex).toLong(); |
||||
final long numExitsDequeued = Long.min(numExitsInQueue, MAX_EXITS_PER_BLOCK); |
||||
|
||||
final List<ValidatorExit> exits = new ArrayList<>(); |
||||
|
||||
for (int i = 0; i < numExitsDequeued; i++) { |
||||
final UInt256 queueStorageSlot = |
||||
EXIT_MESSAGE_QUEUE_STORAGE_OFFSET.plus( |
||||
queueHeadIndex.plus(i).multiply(EXIT_MESSAGE_STORAGE_SLOT_SIZE)); |
||||
final Address sourceAddress = |
||||
Address.wrap(account.getStorageValue(queueStorageSlot).toBytes().slice(12, 20)); |
||||
final BLSPublicKey validatorPubKey = |
||||
BLSPublicKey.wrap( |
||||
Bytes.concatenate( |
||||
account |
||||
.getStorageValue(queueStorageSlot.plus(1)) |
||||
.toBytes() |
||||
.slice(0, 32), // no need to slice
|
||||
account.getStorageValue(queueStorageSlot.plus(2)).toBytes().slice(0, 16))); |
||||
|
||||
exits.add(new ValidatorExit(sourceAddress, validatorPubKey)); |
||||
} |
||||
|
||||
return exits; |
||||
} |
||||
|
||||
private static void updateExcessExits(final MutableAccount account) { |
||||
final UInt256 previousExcessExits = account.getStorageValue(EXCESS_EXITS_STORAGE_SLOT); |
||||
final UInt256 exitCount = account.getStorageValue(EXIT_COUNT_STORAGE_SLOT); |
||||
|
||||
UInt256 newExcessExits = UInt256.valueOf(0L); |
||||
if (previousExcessExits.plus(exitCount).toLong() > TARGET_EXITS_PER_BLOCK) { |
||||
newExcessExits = previousExcessExits.plus(exitCount).subtract(TARGET_EXITS_PER_BLOCK); |
||||
} |
||||
|
||||
account.setStorageValue(EXCESS_EXITS_STORAGE_SLOT, newExcessExits); |
||||
} |
||||
|
||||
private static void resetExitCount(final MutableAccount account) { |
||||
account.setStorageValue(EXIT_COUNT_STORAGE_SLOT, UInt256.valueOf(0L)); |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsAndExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsMismatch; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsRootMismatch; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsWithoutExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithMoreThanMaximumExits; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithoutExitsAndExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithoutExitsWithExitsRoot; |
||||
|
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
import org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.ValidateExitTestParameter; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.Arguments; |
||||
import org.junit.jupiter.params.provider.MethodSource; |
||||
|
||||
class PragueValidatorExitsValidatorTest { |
||||
|
||||
@ParameterizedTest(name = "{index}: {0}") |
||||
@MethodSource("paramsForValidateValidatorExitParameter") |
||||
public void validateValidatorExitParameter( |
||||
final String description, |
||||
final Optional<List<ValidatorExit>> maybeExits, |
||||
final boolean expectedValidity) { |
||||
assertThat(new PragueValidatorExitsValidator().validateValidatorExitParameter(maybeExits)) |
||||
.isEqualTo(expectedValidity); |
||||
} |
||||
|
||||
private static Stream<Arguments> paramsForValidateValidatorExitParameter() { |
||||
return Stream.of( |
||||
Arguments.of("Allowed exits - validating empty exits", Optional.empty(), false), |
||||
Arguments.of("Allowed exits - validating present exits", Optional.of(List.of()), true)); |
||||
} |
||||
|
||||
@ParameterizedTest(name = "{index}: {0}") |
||||
@MethodSource("validateExitsInBlockParamsForPrague") |
||||
public void validateExitsInBlock_WhenPrague( |
||||
final ValidateExitTestParameter param, final boolean expectedValidity) { |
||||
assertThat( |
||||
new PragueValidatorExitsValidator() |
||||
.validateExitsInBlock(param.block, param.expectedExits)) |
||||
.isEqualTo(expectedValidity); |
||||
} |
||||
|
||||
private static Stream<Arguments> validateExitsInBlockParamsForPrague() { |
||||
return Stream.of( |
||||
Arguments.of(blockWithExitsAndExitsRoot(), true), |
||||
Arguments.of(blockWithExitsWithoutExitsRoot(), false), |
||||
Arguments.of(blockWithoutExitsWithExitsRoot(), false), |
||||
Arguments.of(blockWithoutExitsAndExitsRoot(), false), |
||||
Arguments.of(blockWithExitsRootMismatch(), false), |
||||
Arguments.of(blockWithExitsMismatch(), false), |
||||
Arguments.of(blockWithMoreThanMaximumExits(), false)); |
||||
} |
||||
|
||||
@Test |
||||
public void allowExitsShouldReturnTrue() { |
||||
assertThat(new PragueValidatorExitsValidator().allowValidatorExits()).isTrue(); |
||||
} |
||||
} |
@ -0,0 +1,197 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.EXCESS_EXITS_STORAGE_SLOT; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.EXIT_COUNT_STORAGE_SLOT; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.VALIDATOR_EXIT_ADDRESS; |
||||
|
||||
import org.hyperledger.besu.datatypes.Address; |
||||
import org.hyperledger.besu.datatypes.BLSPublicKey; |
||||
import org.hyperledger.besu.ethereum.core.MutableWorldState; |
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
import org.hyperledger.besu.evm.account.MutableAccount; |
||||
import org.hyperledger.besu.evm.worldstate.WorldUpdater; |
||||
|
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.IntStream; |
||||
|
||||
import org.apache.tuweni.bytes.Bytes; |
||||
import org.apache.tuweni.bytes.Bytes32; |
||||
import org.apache.tuweni.bytes.Bytes48; |
||||
import org.apache.tuweni.units.bigints.UInt256; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
class ValidatorExitContractHelperTest { |
||||
|
||||
private MutableWorldState worldState; |
||||
private MutableAccount contract; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
worldState = createInMemoryWorldStateArchive().getMutable(); |
||||
} |
||||
|
||||
@Test |
||||
public void popExitsFromQueue_ReadExitsCorrectly() { |
||||
final List<ValidatorExit> validatorExits = List.of(createExit(), createExit(), createExit()); |
||||
loadContractStorage(worldState, validatorExits); |
||||
|
||||
final List<ValidatorExit> poppedExits = |
||||
ValidatorExitContractHelper.popExitsFromQueue(worldState); |
||||
|
||||
assertThat(poppedExits).isEqualTo(validatorExits); |
||||
} |
||||
|
||||
@Test |
||||
public void popExitsFromQueue_whenContractCodeIsEmpty_ReturnsEmptyListOfExits() { |
||||
// Create account with empty code
|
||||
final WorldUpdater updater = worldState.updater(); |
||||
updater.createAccount(VALIDATOR_EXIT_ADDRESS); |
||||
updater.commit(); |
||||
|
||||
assertThat(ValidatorExitContractHelper.popExitsFromQueue(worldState)).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void popExitsFromQueue_WhenMoreExits_UpdatesQueuePointers() { |
||||
// Loading contract with more than 16 exits
|
||||
final List<ValidatorExit> validatorExits = |
||||
IntStream.range(0, 30).mapToObj(__ -> createExit()).collect(Collectors.toList()); |
||||
loadContractStorage(worldState, validatorExits); |
||||
// After loading the contract, the exit count since last block should match the size of the list
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, validatorExits.size()); |
||||
|
||||
final List<ValidatorExit> poppedExits = |
||||
ValidatorExitContractHelper.popExitsFromQueue(worldState); |
||||
assertThat(poppedExits).hasSize(16); |
||||
|
||||
// Check that queue pointers were updated successfully (head advanced to index 16)
|
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, 16); |
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, 30); |
||||
|
||||
// We had 30 exits in the queue, and target per block is 2, so we have 28 excess
|
||||
assertContractStorageValue(EXCESS_EXITS_STORAGE_SLOT, 28); |
||||
|
||||
// We always reset the exit count after processing the queue
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, 0); |
||||
} |
||||
|
||||
@Test |
||||
public void popExitsFromQueue_WhenNoMoreExits_ZeroQueuePointers() { |
||||
final List<ValidatorExit> validatorExits = List.of(createExit(), createExit(), createExit()); |
||||
loadContractStorage(worldState, validatorExits); |
||||
// After loading the contract, the exit count since last block should match the size of the list
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, validatorExits.size()); |
||||
|
||||
final List<ValidatorExit> poppedExits = |
||||
ValidatorExitContractHelper.popExitsFromQueue(worldState); |
||||
assertThat(poppedExits).hasSize(3); |
||||
|
||||
// Check that queue pointers were updated successfully (head and tail zero because queue is
|
||||
// empty)
|
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, 0); |
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, 0); |
||||
|
||||
// We had 3 exits in the queue, target per block is 2, so we have 1 excess
|
||||
assertContractStorageValue(EXCESS_EXITS_STORAGE_SLOT, 1); |
||||
|
||||
// We always reset the exit count after processing the queue
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, 0); |
||||
} |
||||
|
||||
@Test |
||||
public void popExitsFromQueue_WhenNoExits_DoesNothing() { |
||||
// Loading contract with 0 exits
|
||||
loadContractStorage(worldState, List.of()); |
||||
// After loading storage, we have the exit count as zero because no exits were aded
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, 0); |
||||
|
||||
final List<ValidatorExit> poppedExits = |
||||
ValidatorExitContractHelper.popExitsFromQueue(worldState); |
||||
assertThat(poppedExits).hasSize(0); |
||||
|
||||
// Check that queue pointers are correct (head and tail are zero)
|
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_HEAD_STORAGE_SLOT, 0); |
||||
assertContractStorageValue(EXIT_MESSAGE_QUEUE_TAIL_STORAGE_SLOT, 0); |
||||
|
||||
// We had 0 exits in the queue, and target per block is 2, so we have 0 excess
|
||||
assertContractStorageValue(EXCESS_EXITS_STORAGE_SLOT, 0); |
||||
|
||||
// We always reset the exit count after processing the queue
|
||||
assertContractStorageValue(EXIT_COUNT_STORAGE_SLOT, 0); |
||||
} |
||||
|
||||
private void assertContractStorageValue(final UInt256 slot, final int expectedValue) { |
||||
assertContractStorageValue(slot, UInt256.valueOf(expectedValue)); |
||||
} |
||||
|
||||
private void assertContractStorageValue(final UInt256 slot, final UInt256 expectedValue) { |
||||
assertThat(worldState.get(VALIDATOR_EXIT_ADDRESS).getStorageValue(slot)) |
||||
.isEqualTo(expectedValue); |
||||
} |
||||
|
||||
private void loadContractStorage( |
||||
final MutableWorldState worldState, final List<ValidatorExit> exits) { |
||||
final WorldUpdater updater = worldState.updater(); |
||||
contract = updater.getOrCreate(VALIDATOR_EXIT_ADDRESS); |
||||
|
||||
contract.setCode( |
||||
Bytes.fromHexString( |
||||
"0x61013680600a5f395ff33373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b36603014156101325760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061013257600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460ed5780604402838201600302600401805490600101805490600101549160601b8160a01c17835260601b8160a01c17826020015260601b906040015260010160a6565b910180921460fe5790600255610109565b90505f6002555f6003555b5f546001546002828201116101205750505f610126565b01600290035b5f555f6001556044025ff35b5f5ffd")); |
||||
// excess exits
|
||||
contract.setStorageValue(UInt256.valueOf(0), UInt256.valueOf(0)); |
||||
// exits count
|
||||
contract.setStorageValue(UInt256.valueOf(1), UInt256.valueOf(exits.size())); |
||||
// exits queue head pointer
|
||||
contract.setStorageValue(UInt256.valueOf(2), UInt256.valueOf(0)); |
||||
// exits queue tail pointer
|
||||
contract.setStorageValue(UInt256.valueOf(3), UInt256.valueOf(exits.size())); |
||||
|
||||
int offset = 4; |
||||
for (int i = 0; i < exits.size(); i++) { |
||||
final ValidatorExit exit = exits.get(i); |
||||
// source_account
|
||||
contract.setStorageValue( |
||||
// set account to slot, with 12 bytes padding on the left
|
||||
UInt256.valueOf(offset++), |
||||
UInt256.fromBytes( |
||||
Bytes.concatenate( |
||||
Bytes.fromHexString("0x000000000000000000000000"), exit.getSourceAddress()))); |
||||
// validator_pubkey
|
||||
contract.setStorageValue( |
||||
UInt256.valueOf(offset++), UInt256.fromBytes(exit.getValidatorPubKey().slice(0, 32))); |
||||
contract.setStorageValue( |
||||
// set public key to slot, with 16 bytes padding on the right
|
||||
UInt256.valueOf(offset++), |
||||
UInt256.fromBytes( |
||||
Bytes.concatenate( |
||||
exit.getValidatorPubKey().slice(32, 16), |
||||
Bytes.fromHexString("0x00000000000000000000000000000000")))); |
||||
} |
||||
updater.commit(); |
||||
} |
||||
|
||||
private ValidatorExit createExit() { |
||||
return new ValidatorExit( |
||||
Address.extract(Bytes32.random()), BLSPublicKey.wrap(Bytes48.random())); |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
/* |
||||
* Copyright Hyperledger Besu Contributors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsAndExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithExitsWithoutExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithoutExitsAndExitsRoot; |
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.blockWithoutExitsWithExitsRoot; |
||||
|
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
import org.hyperledger.besu.ethereum.mainnet.ValidatorExitsValidatorTestFixtures.ValidateExitTestParameter; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.Arguments; |
||||
import org.junit.jupiter.params.provider.MethodSource; |
||||
|
||||
class ValidatorExitsValidatorTest { |
||||
|
||||
@ParameterizedTest(name = "{index}: {0}") |
||||
@MethodSource("paramsForValidateValidatorExitParameter") |
||||
public void validateValidatorExitParameter( |
||||
final String description, |
||||
final Optional<List<ValidatorExit>> maybeExits, |
||||
final boolean expectedValidity) { |
||||
assertThat( |
||||
new ValidatorExitsValidator.ProhibitedExits() |
||||
.validateValidatorExitParameter(maybeExits)) |
||||
.isEqualTo(expectedValidity); |
||||
} |
||||
|
||||
private static Stream<Arguments> paramsForValidateValidatorExitParameter() { |
||||
return Stream.of( |
||||
Arguments.of("Prohibited exits - validating empty exits", Optional.empty(), true), |
||||
Arguments.of("Prohibited exits - validating present exits", Optional.of(List.of()), false)); |
||||
} |
||||
|
||||
@ParameterizedTest(name = "{index}: {0}") |
||||
@MethodSource("validateExitsInBlockParamsForProhibited") |
||||
public void validateExitsInBlock_WhenProhibited( |
||||
final ValidateExitTestParameter param, final boolean expectedValidity) { |
||||
assertThat( |
||||
new ValidatorExitsValidator.ProhibitedExits() |
||||
.validateExitsInBlock(param.block, param.expectedExits)) |
||||
.isEqualTo(expectedValidity); |
||||
} |
||||
|
||||
private static Stream<Arguments> validateExitsInBlockParamsForProhibited() { |
||||
return Stream.of( |
||||
Arguments.of(blockWithExitsAndExitsRoot(), false), |
||||
Arguments.of(blockWithExitsWithoutExitsRoot(), false), |
||||
Arguments.of(blockWithoutExitsWithExitsRoot(), false), |
||||
Arguments.of(blockWithoutExitsAndExitsRoot(), true)); |
||||
} |
||||
|
||||
@Test |
||||
public void allowExitsShouldReturnFalse() { |
||||
assertThat(new ValidatorExitsValidator.ProhibitedExits().allowValidatorExits()).isFalse(); |
||||
} |
||||
} |
@ -0,0 +1,155 @@ |
||||
/* |
||||
* Copyright contributors to Hyperledger Besu. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
* |
||||
* SPDX-License-Identifier: Apache-2.0 |
||||
*/ |
||||
|
||||
package org.hyperledger.besu.ethereum.mainnet; |
||||
|
||||
import static org.hyperledger.besu.ethereum.mainnet.ValidatorExitContractHelper.MAX_EXITS_PER_BLOCK; |
||||
|
||||
import org.hyperledger.besu.datatypes.Address; |
||||
import org.hyperledger.besu.datatypes.BLSPublicKey; |
||||
import org.hyperledger.besu.datatypes.Hash; |
||||
import org.hyperledger.besu.ethereum.core.Block; |
||||
import org.hyperledger.besu.ethereum.core.BlockDataGenerator; |
||||
import org.hyperledger.besu.ethereum.core.ValidatorExit; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
import java.util.stream.IntStream; |
||||
|
||||
import org.apache.tuweni.bytes.Bytes32; |
||||
import org.apache.tuweni.bytes.Bytes48; |
||||
|
||||
public class ValidatorExitsValidatorTestFixtures { |
||||
|
||||
private static final BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); |
||||
|
||||
static ValidateExitTestParameter blockWithExitsAndExitsRoot() { |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.of(List.of(createExit())); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create() |
||||
.setExitsRoot(BodyValidation.exitsRoot(maybeExits.get())) |
||||
.setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter("Block with exits and exits_root", block, maybeExits); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithoutExitsWithExitsRoot() { |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.empty(); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create().setExitsRoot(Hash.EMPTY).setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter( |
||||
"Block with exits_root but without exits", block, maybeExits); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithExitsWithoutExitsRoot() { |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.of(List.of(createExit())); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create().setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter( |
||||
"Block with exits but without exits_root", block, maybeExits); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithoutExitsAndExitsRoot() { |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.empty(); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create().setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter("Block without exits and exits_root", block, maybeExits); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithExitsRootMismatch() { |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.of(List.of(createExit())); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create().setExitsRoot(Hash.EMPTY).setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter("Block with exits_root mismatch", block, maybeExits); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithExitsMismatch() { |
||||
final Optional<List<ValidatorExit>> maybeExits = |
||||
Optional.of(List.of(createExit(), createExit())); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create() |
||||
.setExitsRoot(BodyValidation.exitsRoot(maybeExits.get())) |
||||
.setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter( |
||||
"Block with exits mismatch", block, maybeExits, List.of(createExit())); |
||||
} |
||||
|
||||
static ValidateExitTestParameter blockWithMoreThanMaximumExits() { |
||||
final List<ValidatorExit> validatorExits = |
||||
IntStream.range(0, MAX_EXITS_PER_BLOCK + 1).mapToObj(__ -> createExit()).toList(); |
||||
final Optional<List<ValidatorExit>> maybeExits = Optional.of(validatorExits); |
||||
|
||||
final BlockDataGenerator.BlockOptions blockOptions = |
||||
BlockDataGenerator.BlockOptions.create() |
||||
.setExitsRoot(BodyValidation.exitsRoot(maybeExits.get())) |
||||
.setExits(maybeExits); |
||||
final Block block = blockDataGenerator.block(blockOptions); |
||||
|
||||
return new ValidateExitTestParameter("Block with more than maximum exits", block, maybeExits); |
||||
} |
||||
|
||||
static ValidatorExit createExit() { |
||||
return new ValidatorExit( |
||||
Address.extract(Bytes32.random()), BLSPublicKey.wrap(Bytes48.random())); |
||||
} |
||||
|
||||
static class ValidateExitTestParameter { |
||||
|
||||
String description; |
||||
Block block; |
||||
Optional<List<ValidatorExit>> maybeExits; |
||||
List<ValidatorExit> expectedExits; |
||||
|
||||
public ValidateExitTestParameter( |
||||
final String description, |
||||
final Block block, |
||||
final Optional<List<ValidatorExit>> maybeExits) { |
||||
this(description, block, maybeExits, maybeExits.orElseGet(List::of)); |
||||
} |
||||
|
||||
public ValidateExitTestParameter( |
||||
final String description, |
||||
final Block block, |
||||
final Optional<List<ValidatorExit>> maybeExits, |
||||
final List<ValidatorExit> expectedExits) { |
||||
this.description = description; |
||||
this.block = block; |
||||
this.maybeExits = maybeExits; |
||||
this.expectedExits = expectedExits; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return description; |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue