Skip to content

Commit

Permalink
Validator consolidation ATs (#8565)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucassaldanha authored Sep 5, 2024
1 parent fd99450 commit 99e72b2
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class ExecutionLayerTriggeredExitAcceptanceTest extends AcceptanceTestBas
void triggerValidatorExitWithFullWithdrawal() throws Exception {
final UInt64 currentTime = new SystemTimeProvider().getTimeInSeconds();
final int genesisTime =
currentTime.intValue() + 10; // genesis in 10 seconds to give node time to start
currentTime.intValue() + 30; // genesis in 30 seconds to give node time to start

final BesuNode besuNode = createBesuNode(genesisTime);
besuNode.start();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Consensys Software Inc., 2022
*
* 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.
*/

package tech.pegasys.teku.test.acceptance;

import com.google.common.io.Resources;
import java.net.URL;
import java.util.Map;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.execution.types.Eth1Address;
import tech.pegasys.teku.infrastructure.time.SystemTimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.test.acceptance.dsl.AcceptanceTestBase;
import tech.pegasys.teku.test.acceptance.dsl.BesuDockerVersion;
import tech.pegasys.teku.test.acceptance.dsl.BesuNode;
import tech.pegasys.teku.test.acceptance.dsl.GenesisGenerator.InitialStateData;
import tech.pegasys.teku.test.acceptance.dsl.TekuBeaconNode;
import tech.pegasys.teku.test.acceptance.dsl.TekuNodeConfig;
import tech.pegasys.teku.test.acceptance.dsl.TekuNodeConfigBuilder;
import tech.pegasys.teku.test.acceptance.dsl.tools.deposits.ValidatorKeys;
import tech.pegasys.teku.test.acceptance.dsl.tools.deposits.ValidatorKeystores;

public class ValidatorConsolidationAcceptanceTest extends AcceptanceTestBase {

private static final String NETWORK_NAME = "swift";
private static final URL JWT_FILE = Resources.getResource("auth/ee-jwt-secret.hex");

@Test
void consolidateValidator() throws Exception {
final UInt64 currentTime = new SystemTimeProvider().getTimeInSeconds();
final int genesisTime =
currentTime.intValue() + 30; // genesis in 30 seconds to give node time to start

final BesuNode besuNode = createBesuNode(genesisTime);
besuNode.start();

final String eth1Address =
besuNode.getRichBenefactorAddress(); // used as withdrawal_credentials
final String eth1PrivateKey =
besuNode.getRichBenefactorKey(); // key for withdrawal_credentials account

// 192 validators (32 eth each) is the minimum number required to enable us to have enough limit
// to consolidate
final ValidatorKeystores validatorKeys =
createTekuDepositSender(NETWORK_NAME)
.generateValidatorKeys(192, Eth1Address.fromHexString(eth1Address));

final InitialStateData initialStateData =
createGenesisGenerator()
.network(NETWORK_NAME)
.withGenesisTime(genesisTime)
.genesisDelaySeconds(0)
.withAltairEpoch(UInt64.ZERO)
.withBellatrixEpoch(UInt64.ZERO)
.withCapellaEpoch(UInt64.ZERO)
.withDenebEpoch(UInt64.ZERO)
.withElectraEpoch(UInt64.ZERO)
.withTotalTerminalDifficulty(0)
.genesisExecutionPayloadHeaderSource(besuNode::createGenesisExecutionPayload)
.validatorKeys(validatorKeys)
.generate();

final TekuBeaconNode tekuNode =
createTekuBeaconNode(beaconNode(genesisTime, besuNode, initialStateData, validatorKeys));
tekuNode.start();
// Ensures sourceValidator is active long enough to exit
tekuNode.waitForNewFinalization();

final ValidatorKeys sourceValidator = validatorKeys.getValidatorKeys().get(0);
final BLSPublicKey sourceValidatorPublicKey = sourceValidator.getValidatorKey().getPublicKey();

final ValidatorKeys targetValidator = validatorKeys.getValidatorKeys().get(1);
final BLSPublicKey targetValidatorPublicKey = targetValidator.getValidatorKey().getPublicKey();

besuNode.createConsolidationRequest(
eth1PrivateKey, sourceValidatorPublicKey, targetValidatorPublicKey);
waitForValidatorExit(tekuNode, sourceValidatorPublicKey);
}

private void waitForValidatorExit(
final TekuBeaconNode tekuNode, final BLSPublicKey validatorPublicKey) {
final String pubKeySubstring = validatorPublicKey.toHexString().substring(2, 9);
tekuNode.waitForLogMessageContaining(
"Validator "
+ pubKeySubstring
+ " has changed status from active_ongoing to active_exiting");
}

private BesuNode createBesuNode(final int genesisTime) {
final Map<String, String> genesisOverrides = Map.of("pragueTime", String.valueOf(genesisTime));

return createBesuNode(
// "Waiting for Besu 24.9.0 release (https://github.com/Consensys/teku/issues/8535)"
BesuDockerVersion.DEVELOP,
config ->
config
.withMergeSupport()
.withGenesisFile("besu/pragueGenesis.json")
.withP2pEnabled(true)
.withJwtTokenAuthorization(JWT_FILE),
genesisOverrides);
}

private static TekuNodeConfig beaconNode(
final int genesisTime,
final BesuNode besuNode,
final InitialStateData initialStateData,
final ValidatorKeystores validatorKeys)
throws Exception {
return TekuNodeConfigBuilder.createBeaconNode()
.withInitialState(initialStateData)
.withNetwork(NETWORK_NAME)
.withAltairEpoch(UInt64.ZERO)
.withBellatrixEpoch(UInt64.ZERO)
.withCapellaEpoch(UInt64.ZERO)
.withDenebEpoch(UInt64.ZERO)
.withElectraEpoch(UInt64.ZERO)
.withTotalTerminalDifficulty(0)
.withGenesisTime(genesisTime)
.withExecutionEngine(besuNode)
.withJwtSecretFile(JWT_FILE)
.withReadOnlyKeystorePath(validatorKeys)
.withValidatorProposerDefaultFeeRecipient("0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73")
.withStartupTargetPeerCount(0)
.withRealNetwork()
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ public Eth1Address getWithdrawalRequestContractAddress() {
return Eth1Address.fromHexString("0x00A3ca265EBcb825B45F985A16CEFB49958cE017");
}

/*
Defined on https://eips.ethereum.org/EIPS/eip-7251 (CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS)
*/
public Eth1Address getConsolidationRequestContractAddress() {
return Eth1Address.fromHexString("0x00b42dbF2194e931E80326D950320f7d9Dbeac02");
}

public String getInternalJsonRpcUrl() {
return "http://" + nodeAlias + ":" + JSON_RPC_PORT;
}
Expand Down Expand Up @@ -208,14 +215,46 @@ public void createWithdrawalRequest(
final Credentials eth1Credentials = Credentials.create(eth1PrivateKey);
try (final ExecutionRequestsService executionRequestsService =
new ExecutionRequestsService(
getExternalJsonRpcUrl(), eth1Credentials, getWithdrawalRequestContractAddress())) {
getExternalJsonRpcUrl(),
eth1Credentials,
getWithdrawalRequestContractAddress(),
getConsolidationRequestContractAddress())) {

final SafeFuture<TransactionReceipt> future =
executionRequestsService.createWithdrawalRequest(publicKey, amountInGwei);
Waiter.waitFor(future, Duration.ofMinutes(1));
}
}

/**
* Sends a transaction to the consolidation request contract in the execution layer to create a
* consolidation request.
*
* @param eth1PrivateKey the private key of the eth1 account that will sign the transaction to the
* consolidation contract (has to match the source validator withdrawal credentials)
* @param sourceValidatorPublicKey source validator public key
* @param targetValidatorPublicKey target validator public key
*/
public void createConsolidationRequest(
final String eth1PrivateKey,
final BLSPublicKey sourceValidatorPublicKey,
final BLSPublicKey targetValidatorPublicKey)
throws Exception {
final Credentials eth1Credentials = Credentials.create(eth1PrivateKey);
try (final ExecutionRequestsService executionRequestsService =
new ExecutionRequestsService(
getExternalJsonRpcUrl(),
eth1Credentials,
getWithdrawalRequestContractAddress(),
getConsolidationRequestContractAddress())) {

final SafeFuture<TransactionReceipt> future =
executionRequestsService.createConsolidationRequest(
sourceValidatorPublicKey, targetValidatorPublicKey);
Waiter.waitFor(future, Duration.ofMinutes(1));
}
}

@SuppressWarnings("unused")
private static class Request {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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.
*/

package tech.pegasys.teku.test.acceptance.dsl.executionrequests;

import java.math.BigInteger;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.tx.TransactionManager;
import org.web3j.utils.Async;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.execution.types.Eth1Address;
import tech.pegasys.teku.infrastructure.async.SafeFuture;

/*
Wrapper classed based on https://github.com/lightclient/sys-asm/blob/main/src/consolidations/main.eas
*/
public class ConsolidationRequestContract {

private final Eth1Address contractAddress;
private final TransactionManager transactionManager;
private final Web3j web3j;

public ConsolidationRequestContract(
final Eth1Address contractAddress,
final Web3j web3j,
final TransactionManager transactionManager) {
this.contractAddress = contractAddress;
this.web3j = web3j;
this.transactionManager = transactionManager;
}

public SafeFuture<Integer> getExcessConsolidationRequests() {
return SafeFuture.of(
web3j
.ethCall(
Transaction.createEthCallTransaction(
Eth1Address.ZERO.toHexString(), contractAddress.toHexString(), ""),
DefaultBlockParameter.valueOf("latest"))
.sendAsync()
.thenApply(response -> UInt256.fromHexString(response.getResult()).toInt()));
}

public SafeFuture<EthSendTransaction> createConsolidationRequest(
final BLSPublicKey sourcePublicKey, final BLSPublicKey targetPublicKey) {
final BigInteger gasPrice = BigInteger.valueOf(1_000);
final BigInteger gasLimit = BigInteger.valueOf(1_000_000);
final Bytes data =
Bytes.concatenate(sourcePublicKey.toBytesCompressed(), targetPublicKey.toBytesCompressed());
final BigInteger value = BigInteger.valueOf(2); // has to be more than current fee (0)

return SafeFuture.of(
Async.run(
() ->
transactionManager.sendTransaction(
gasPrice, gasLimit, contractAddress.toHexString(), data.toHexString(), value)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ public class ExecutionRequestsService implements AutoCloseable {
private final ScheduledExecutorService executorService;
private final Web3j web3j;
private final WithdrawalRequestContract withdrawalRequestContract;
private final ConsolidationRequestContract consolidationRequestContract;

public ExecutionRequestsService(
final String eth1NodeUrl,
final Credentials eth1Credentials,
final Eth1Address withdrawalRequestAddress) {
final Eth1Address withdrawalRequestAddress,
final Eth1Address consolidationRequestAddress) {
this.httpClient = new OkHttpClient.Builder().connectionPool(new ConnectionPool()).build();
this.executorService =
Executors.newScheduledThreadPool(
Expand All @@ -69,6 +71,8 @@ public ExecutionRequestsService(

this.withdrawalRequestContract =
new WithdrawalRequestContract(withdrawalRequestAddress, web3j, transactionManager);
this.consolidationRequestContract =
new ConsolidationRequestContract(consolidationRequestAddress, web3j, transactionManager);
}

@Override
Expand Down Expand Up @@ -96,6 +100,24 @@ public SafeFuture<TransactionReceipt> createWithdrawalRequest(
});
}

public SafeFuture<TransactionReceipt> createConsolidationRequest(
final BLSPublicKey sourceValidatorPublicKey, final BLSPublicKey targetValidatorPublicKey) {
// Sanity check that we can interact with the contract
Waiter.waitFor(
() ->
assertThat(consolidationRequestContract.getExcessConsolidationRequests().get())
.isEqualTo(0));

return consolidationRequestContract
.createConsolidationRequest(sourceValidatorPublicKey, targetValidatorPublicKey)
.thenCompose(
response -> {
final String txHash = response.getResult();
waitForSuccessfulTransaction(txHash);
return getTransactionReceipt(txHash);
});
}

private SafeFuture<TransactionReceipt> getTransactionReceipt(final String txHash) {
return SafeFuture.of(
web3j
Expand Down

0 comments on commit 99e72b2

Please sign in to comment.