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

WhaleBasedAccountManager with Retry Mechanism #278

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
Expand Up @@ -13,6 +13,7 @@ import org.web3j.crypto.Credentials
import org.web3j.protocol.Web3j
import org.web3j.protocol.core.DefaultBlockParameterName
import org.web3j.protocol.core.Response
import org.web3j.protocol.core.methods.response.TransactionReceipt
import org.web3j.tx.response.PollingTransactionReceiptProcessor
import tech.pegasys.teku.infrastructure.async.SafeFuture
import java.io.File
Expand Down Expand Up @@ -107,12 +108,6 @@ private open class WhaleBasedAccountManager(
val accountIndex = testWorkerId % whaleAccounts.size
val whaleAccount = whaleAccounts[accountIndex.toInt()]
val whaleTxManager = txManagers[accountIndex.toInt()]
// for faster feedback loop troubleshooting account selection
// throw RuntimeException(
// "pid=${ProcessHandle.current().pid()}, " +
// "threadName=${Thread.currentThread().name} threadId=${Thread.currentThread().id} workerId=$testWorkerId " +
// "accIndex=$accountIndex/${whaleAccounts.size} whaleAccount=${whaleAccount.privateKey.takeLast(4)}"
// )
return Pair(whaleAccount, whaleTxManager)
}

Expand All @@ -133,30 +128,16 @@ private open class WhaleBasedAccountManager(
)

val result = synchronized(whaleTxManager) {
(0..numberOfAccounts).map {
(1..numberOfAccounts).map { _ ->
val randomPrivKey = Bytes.random(32).toHexString().replace("0x", "")
val newAccount = Account(randomPrivKey, Credentials.create(randomPrivKey).address)
val transferResult = whaleTxManager.sendTransaction(
/*gasPrice*/ 300000000.toBigInteger(),
/*gasLimit*/ 21000.toBigInteger(),
newAccount.address,
"",
initialBalanceWei
)
if (transferResult.hasError()) {
val accountBalance =
web3jClient.ethGetBalance(whaleAccount.address, DefaultBlockParameterName.LATEST).send().result
throw RuntimeException(
"Failed to send funds from accAddress=${whaleAccount.address}, " +
"accBalance=$accountBalance, " +
"accPrivKey=0x...${whaleAccount.privateKey.takeLast(8)}, " +
"error: ${transferResult.error.asString()}"
)
}
newAccount to transferResult
val transferResult = sendWithRetry(whaleTxManager, newAccount.address, initialBalanceWei)
Pair(newAccount, transferResult)
}
}
result.forEach { (account, transferTx) ->

result.forEach { pair ->
val (account, transferTx) = pair
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be simplified as:

result.forEach { (account, transferTx) ->

log.debug(
"Waiting for account funding: newAccount={} txHash={} whaleAccount={}",
account.address,
Expand All @@ -171,7 +152,7 @@ private open class WhaleBasedAccountManager(
)
if (log.isDebugEnabled) {
log.debug(
"Account funded: newAccount={} balance={}wei",
"Account funded: newAccount={} balance={} wei",
account.address,
web3jClient.ethGetBalance(account.address, DefaultBlockParameterName.LATEST).send().balance
)
Expand All @@ -180,6 +161,67 @@ private open class WhaleBasedAccountManager(
return result.map { it.first }
}

private fun sendWithRetry(
txManager: AsyncFriendlyTransactionManager,
toAddress: String,
amountWei: BigInteger,
maxRetries: Int = 5,
initialGasPrice: BigInteger = BigInteger("300000000")
): TransactionReceipt {
var attempt = 0
var gasPrice = initialGasPrice
var lastError: Response.Error? = null

while (attempt < maxRetries) {
try {
val transferResult = txManager.sendTransaction(
gasPrice,
BigInteger.valueOf(21000),
toAddress,
"",
amountWei
)
if (transferResult.hasError()) {
val error = transferResult.error
if (isRecoverableError(error)) {
log.warn(
"Transfer attempt {} failed with error: {}. Retrying with higher gas price...",
attempt + 1,
error.asString()
)
lastError = error
attempt++
gasPrice = gasPrice.add(BigInteger("100000000"))
continue
} else {
throw RuntimeException("Failed to send funds due to unrecoverable error: ${error.asString()}")
}
}
return transferResult
} catch (e: Exception) {
log.warn(
"Exception during transfer attempt {}: {}. Retrying...",
attempt + 1,
e.message
)
attempt++
gasPrice = gasPrice.add(BigInteger("100000000"))
}
}
throw RuntimeException("Failed to send funds after $maxRetries attempts. Last error: ${lastError?.asString()}")
}

private fun isRecoverableError(error: Response.Error): Boolean {
val recoverableErrors = listOf(
"Nonce too low",
"Replacement transaction underpriced",
"Transaction nonce is too low",
"Transaction with the same hash was already imported",
"Known transaction"
)
return recoverableErrors.any { error.message.contains(it, ignoreCase = true) }
}

fun Response.Error.asString(): String {
return "Response.Error(code=$code, message=$message)"
}
Expand Down Expand Up @@ -215,7 +257,6 @@ private open class WhaleBasedAccountManager(
)
}
}

return futureResult
}
}
Expand Down
Loading