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

Validator Triggered Exits (EIP-7002) ATs #8547

Merged
merged 3 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.Disabled;
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 ExecutionLayerTriggeredExitAcceptanceTest extends AcceptanceTestBase {

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

@Test
@Disabled("Waiting for Besu 24.9.0 release (https://github.com/Consensys/teku/issues/8535)")
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

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

final ValidatorKeystores validatorKeys =
createTekuDepositSender(NETWORK_NAME)
.generateValidatorKeys(4, 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 validator is active long enough to exit
tekuNode.waitForNewFinalization();

final ValidatorKeys validator = validatorKeys.getValidatorKeys().get(0);
final BLSPublicKey validatorPublicKey = validator.getValidatorKey().getPublicKey();

besuNode.createWithdrawalRequest(eth1PrivateKey, validatorPublicKey, UInt64.ZERO);

// Wait for validator exit confirmation
tekuNode.waitForLogMessageContaining(
"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(
BesuDockerVersion.STABLE,
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)
.withTrustedSetupFromClasspath("mainnet-trusted-setup.txt")
.withReadOnlyKeystorePath(validatorKeys)
.withValidatorProposerDefaultFeeRecipient("0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73")
.withStartupTargetPeerCount(0)
.withRealNetwork()
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand All @@ -35,10 +36,17 @@
import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.utility.MountableFile;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.execution.types.Eth1Address;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.async.Waiter;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader;
import tech.pegasys.teku.spec.datastructures.interop.MergedGenesisTestBuilder;
import tech.pegasys.teku.test.acceptance.dsl.executionrequests.ExecutionRequestsService;

public class BesuNode extends Node {

Expand Down Expand Up @@ -118,6 +126,13 @@ public Eth1Address getDepositContractAddress() {
return Eth1Address.fromHexString("0xdddddddddddddddddddddddddddddddddddddddd");
}

/*
Defined on https://eips.ethereum.org/EIPS/eip-7002 (WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS)
*/
public Eth1Address getWithdrawalRequestContractAddress() {
return Eth1Address.fromHexString("0x00A3ca265EBcb825B45F985A16CEFB49958cE017");
}

public String getInternalJsonRpcUrl() {
return "http://" + nodeAlias + ":" + JSON_RPC_PORT;
}
Expand Down Expand Up @@ -158,6 +173,10 @@ public Boolean addPeer(final BesuNode node) throws Exception {
return OBJECT_MAPPER.readTree(response).get("result").asBoolean();
}

public String getRichBenefactorAddress() {
return "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73";
}

public String getRichBenefactorKey() {
return "0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63";
}
Expand All @@ -173,6 +192,30 @@ public ExecutionPayloadHeader createGenesisExecutionPayload(final Spec spec) {
}
}

/**
* Sends a transaction to the withdrawal request contract in the execution layer to create a
* withdrawal request.
*
* @param eth1PrivateKey the private key of the eth1 account that will sign the transaction to the
* withdrawal contract (has to match the validator withdrawal credentials)
* @param publicKey validator public key
* @param amountInGwei the amount for the withdrawal request (zero for full withdrawal, greater
* than zero for partial withdrawal)
*/
public void createWithdrawalRequest(
final String eth1PrivateKey, final BLSPublicKey publicKey, final UInt64 amountInGwei)
throws Exception {
final Credentials eth1Credentials = Credentials.create(eth1PrivateKey);
try (final ExecutionRequestsService executionRequestsService =
new ExecutionRequestsService(
getExternalJsonRpcUrl(), eth1Credentials, getWithdrawalRequestContractAddress())) {

final SafeFuture<TransactionReceipt> future =
executionRequestsService.createWithdrawalRequest(publicKey, amountInGwei);
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,118 @@
/*
* 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.dsl.executionrequests;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.FastRawTransactionManager;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.response.PollingTransactionReceiptProcessor;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.execution.types.Eth1Address;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.async.Waiter;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;

public class ExecutionRequestsService implements AutoCloseable {

// Increase the poll rate for tx receipts but keep the default 10 min timeout.
private static final int POLL_INTERVAL_MILLIS = 2000;
private static final int MAX_POLL_ATTEMPTS = 300;

// private final WithdrawalRequestTransactionSender sender;
private final OkHttpClient httpClient;
private final ScheduledExecutorService executorService;
private final Web3j web3j;
private final WithdrawalRequestContract withdrawalRequestContract;

public ExecutionRequestsService(
final String eth1NodeUrl,
final Credentials eth1Credentials,
final Eth1Address withdrawalRequestAddress) {
this.httpClient = new OkHttpClient.Builder().connectionPool(new ConnectionPool()).build();
this.executorService =
Executors.newScheduledThreadPool(
1,
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("web3j-executionRequests-%d")
.build());
this.web3j = Web3j.build(new HttpService(eth1NodeUrl, httpClient), 1000, executorService);

final TransactionManager transactionManager =
new FastRawTransactionManager(
web3j,
eth1Credentials,
new PollingTransactionReceiptProcessor(web3j, POLL_INTERVAL_MILLIS, MAX_POLL_ATTEMPTS));

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

@Override
public void close() {
web3j.shutdown();
httpClient.dispatcher().executorService().shutdownNow();
httpClient.connectionPool().evictAll();
executorService.shutdownNow();
}

public SafeFuture<TransactionReceipt> createWithdrawalRequest(
final BLSPublicKey publicKey, final UInt64 amount) {
// Sanity check that we can interact with the contract
Waiter.waitFor(
() ->
assertThat(withdrawalRequestContract.getExcessWithdrawalRequests().get()).isEqualTo(0));

return withdrawalRequestContract
.createWithdrawalRequest(publicKey, amount)
.thenCompose(
response -> {
final String txHash = response.getResult();
waitForSuccessfulTransaction(txHash);
return getTransactionReceipt(txHash);
});
}

private SafeFuture<TransactionReceipt> getTransactionReceipt(final String txHash) {
return SafeFuture.of(
web3j
.ethGetTransactionReceipt(txHash)
.sendAsync()
.thenApply(EthGetTransactionReceipt::getTransactionReceipt)
.thenApply(Optional::orElseThrow));
}

private void waitForSuccessfulTransaction(final String txHash) {
Waiter.waitFor(
() -> {
final TransactionReceipt transactionReceipt =
web3j.ethGetTransactionReceipt(txHash).send().getTransactionReceipt().orElseThrow();
if (!"0x1".equals(transactionReceipt.getStatus())) {
throw new RuntimeException("Transaction failed");
}
});
}
}
Loading