Skip to content

Commit a9f7e2b

Browse files
committed
fix: estimate gas before sending transactions to avoid intrinsic gas too low errors
1 parent f07819c commit a9f7e2b

File tree

9 files changed

+144
-87
lines changed

9 files changed

+144
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ To run properly, the iExec Blockchain Adapter API requires:
3333
| `IEXEC_HUB_ADDRESS` | Proxy contract address to interact with the iExec on-chain protocol. | Ethereum Address | `0x3eca1B216A7DF1C7689aEb259fFB83ADFB894E7f` |
3434
| `IEXEC_GAS_PRICE_MULTIPLIER` | Transactions will be sent with `networkGasPrice * gasPriceMultiplier`. | Float | `1.0` |
3535
| `IEXEC_GAS_PRICE_CAP` | In Wei, will be used for transactions if `networkGasPrice * gasPriceMultiplier > gasPriceCap`. | Positive integer | `22000000000` |
36-
| `IEXEC_BLOCKCHAIN_ADAPTER_API_MAX_ALLOWED_TX_PER_BLOCK` | Max number of transactions per block, between `1` and `4`. | Positive integer | `1` |
36+
| `IEXEC_BLOCKCHAIN_ADAPTER_API_MAX_ALLOWED_TX_PER_BLOCK` | Max number of transactions per block, between `1` and `8`. | Positive integer | `1` |
3737
| `IEXEC_BLOCKCHAIN_ADAPTER_API_WALLET_PATH` | Path to the wallet of the server. | String | `src/main/resources/wallet.json` |
3838
| `IEXEC_BLOCKCHAIN_ADAPTER_API_WALLET_PASSWORD` | Password to unlock the wallet of the server. | String | `whatever` |
3939

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# x-release-please-start-version
22
version=9.0.1
33
# x-release-please-end
4-
iexecCommonVersion=9.0.0
5-
iexecCommonsPocoVersion=5.0.0
4+
iexecCommonsPocoVersion=5.1.0
5+
iexecCommonVersion=9.1.0
66

77
nexusUser
88
nexusPassword

src/main/java/com/iexec/blockchain/chain/ChainConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class ChainConfig {
6161
long gasPriceCap;
6262

6363
@Positive(message = "Max allowed tx per block must be greater than 0")
64-
@Max(value = 4, message = "Max allowed tx per block must be less or equal to 4")
64+
@Max(value = 8, message = "Max allowed tx per block must be less or equal to 8")
6565
int maxAllowedTxPerBlock;
6666

6767
}

src/main/java/com/iexec/blockchain/chain/IexecHubService.java

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,24 +16,36 @@
1616

1717
package com.iexec.blockchain.chain;
1818

19+
import com.iexec.blockchain.command.generic.SubmittedTx;
1920
import com.iexec.commons.poco.chain.*;
21+
import com.iexec.commons.poco.encoding.PoCoDataEncoder;
2022
import com.iexec.commons.poco.utils.BytesUtils;
2123
import com.iexec.commons.poco.utils.HashUtils;
24+
import io.micrometer.core.instrument.Counter;
25+
import io.micrometer.core.instrument.Metrics;
26+
import lombok.extern.slf4j.Slf4j;
2227
import org.apache.commons.lang3.StringUtils;
2328
import org.springframework.stereotype.Service;
2429
import org.web3j.protocol.core.methods.response.TransactionReceipt;
30+
import org.web3j.protocol.exceptions.TransactionException;
2531

32+
import java.io.IOException;
2633
import java.math.BigInteger;
2734
import java.nio.charset.StandardCharsets;
28-
import java.time.Instant;
29-
import java.time.temporal.ChronoUnit;
3035
import java.util.Date;
3136

3237
import static com.iexec.commons.poco.utils.BytesUtils.stringToBytes;
3338

39+
@Slf4j
3440
@Service
3541
public class IexecHubService extends IexecHubAbstractService {
3642

43+
private final ChainConfig chainConfig;
44+
private final SignerService signerService;
45+
private final Web3jService web3jService;
46+
private final Counter failureCounter = Metrics.counter("iexec.poco.transaction", "status", "failure");
47+
private final Counter successCounter = Metrics.counter("iexec.poco.transaction", "status", "success");
48+
3749
public IexecHubService(final SignerService signerService,
3850
final Web3jService web3jService,
3951
final ChainConfig chainConfig) {
@@ -42,6 +54,9 @@ public IexecHubService(final SignerService signerService,
4254
web3jService,
4355
chainConfig.getHubAddress()
4456
);
57+
this.chainConfig = chainConfig;
58+
this.signerService = signerService;
59+
this.web3jService = web3jService;
4560
}
4661

4762
public static boolean isByte32(final String hexString) {
@@ -51,12 +66,9 @@ public static boolean isByte32(final String hexString) {
5166

5267
public TransactionReceipt initializeTask(final String chainDealId,
5368
final int taskIndex) throws Exception {
54-
addLatency();
55-
return iexecHubContract
56-
.initialize(
57-
stringToBytes(chainDealId),
58-
BigInteger.valueOf(taskIndex))
59-
.send();
69+
final String txData = PoCoDataEncoder.encodeInitialize(chainDealId, taskIndex);
70+
final SubmittedTx submittedTx = submit("initialize", txData);
71+
return waitForTxMined(submittedTx);
6072
}
6173

6274
public TransactionReceipt contribute(final String chainTaskId,
@@ -67,59 +79,64 @@ public TransactionReceipt contribute(final String chainTaskId,
6779
final String resultHash = HashUtils.concatenateAndHash(chainTaskId, resultDigest);
6880
final String resultSeal = HashUtils.concatenateAndHash(credentials.getAddress(), chainTaskId, resultDigest);
6981

70-
return iexecHubContract
71-
.contribute(
72-
stringToBytes(chainTaskId),
73-
stringToBytes(resultHash),
74-
stringToBytes(resultSeal),
75-
enclaveChallenge,
76-
stringToBytes(enclaveSignature),
77-
stringToBytes(workerpoolSignature))
78-
.send();
82+
final String txData = PoCoDataEncoder.encodeContribute(
83+
chainTaskId, resultHash, resultSeal, enclaveChallenge, enclaveSignature, workerpoolSignature);
84+
final SubmittedTx submittedTx = submit("contribute", txData);
85+
return waitForTxMined(submittedTx);
7986
}
8087

8188

8289
public TransactionReceipt reveal(final String chainTaskId,
8390
final String resultDigest) throws Exception {
84-
return iexecHubContract
85-
.reveal(
86-
stringToBytes(chainTaskId),
87-
stringToBytes(resultDigest))
88-
.send();
91+
final String txData = PoCoDataEncoder.encodeReveal(chainTaskId, resultDigest);
92+
final SubmittedTx submittedTx = submit("reveal", txData);
93+
return waitForTxMined(submittedTx);
8994
}
9095

9196
public TransactionReceipt finalizeTask(final String chainTaskId,
9297
final String resultLink,
9398
final String callbackData) throws Exception {
94-
addLatency();
95-
byte[] results = StringUtils.isNotEmpty(resultLink) ?
99+
final byte[] results = StringUtils.isNotEmpty(resultLink) ?
96100
resultLink.getBytes(StandardCharsets.UTF_8) : new byte[0];
97-
byte[] resultsCallback = StringUtils.isNotEmpty(callbackData) ?
101+
final byte[] resultsCallback = StringUtils.isNotEmpty(callbackData) ?
98102
stringToBytes(callbackData) : new byte[0];
99103

100-
return iexecHubContract
101-
.finalize(
102-
stringToBytes(chainTaskId),
103-
results,
104-
resultsCallback)
105-
.send();
104+
final String txData = PoCoDataEncoder.encodeFinalize(chainTaskId, results, resultsCallback);
105+
final SubmittedTx submittedTx = submit("finalize", txData);
106+
return waitForTxMined(submittedTx);
106107
}
107108

108109
/**
109-
* Synchronized sleep to ensure several transactions will never be sent in the same time interval.
110+
* Submits the transaction to the blockchain network mem-pool.
110111
* <p>
111-
* This synchronized sleep is required for nonce computation on pending block.
112+
* This method can be {@code synchronized} as there is only a single {@code IexecHubService} instance.
112113
* When a first transaction will be emitted, it will be emitted and registered on the pending block.
113114
* After a latency, a second transaction can be sent on the pending block and the nonce will be computed successfully.
114115
* With a correct nonce, it becomes possible to perform several transactions from the same wallet in the same block.
115-
*
116-
* @throws InterruptedException if the calling thread is interrupted
117116
*/
118-
private synchronized void addLatency() throws InterruptedException {
119-
final long deadline = Instant.now().plus(1L, ChronoUnit.SECONDS).toEpochMilli();
120-
while (Instant.now().toEpochMilli() < deadline) {
121-
wait(deadline - Instant.now().toEpochMilli());
117+
private synchronized SubmittedTx submit(final String function, final String txData) throws IOException {
118+
final BigInteger nonce = signerService.getNonce();
119+
final BigInteger gasLimit = switch (function) {
120+
case "initialize" -> signerService.estimateGas(chainConfig.getHubAddress(), txData);
121+
case "finalize" -> signerService.estimateGas(chainConfig.getHubAddress(), txData).add(getCallbackGas());
122+
default -> PoCoDataEncoder.getGasLimitForFunction(function);
123+
};
124+
final String txHash = signerService.signAndSendTransaction(
125+
nonce, web3jService.getUserGasPrice(), gasLimit, chainConfig.getHubAddress(), txData);
126+
log.info("Transaction submitted [nonce:{}, hash:{}]", nonce, txHash);
127+
return new SubmittedTx(nonce, gasLimit, txData, txHash);
128+
}
129+
130+
private TransactionReceipt waitForTxMined(final SubmittedTx submittedTx) throws IOException, TransactionException {
131+
final TransactionReceipt receipt = txReceiptProcessor.waitForTransactionReceipt(submittedTx.hash());
132+
log.info("Transaction receipt [nonce:{}, hash:{}, status:{}, revert-reason:{}]",
133+
submittedTx.nonce(), submittedTx.hash(), receipt.getStatus(), receipt.getRevertReason());
134+
if (!receipt.isStatusOK()) {
135+
failureCounter.increment();
136+
} else {
137+
successCounter.increment();
122138
}
139+
return receipt;
123140
}
124141

125142
public boolean hasEnoughGas() {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2025 IEXEC BLOCKCHAIN TECH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.iexec.blockchain.command.generic;
18+
19+
import java.math.BigInteger;
20+
21+
public record SubmittedTx(BigInteger nonce, BigInteger gasLimit, String txData, String hash) {
22+
}

src/main/java/com/iexec/blockchain/command/task/finalize/TaskFinalizeBlockchainService.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,12 +32,12 @@ public class TaskFinalizeBlockchainService implements CommandBlockchain<TaskFina
3232

3333
private final IexecHubService iexecHubService;
3434

35-
public TaskFinalizeBlockchainService(IexecHubService iexecHubService) {
35+
public TaskFinalizeBlockchainService(final IexecHubService iexecHubService) {
3636
this.iexecHubService = iexecHubService;
3737
}
3838

3939
@Override
40-
public boolean canSendBlockchainCommand(TaskFinalizeArgs args) {
40+
public boolean canSendBlockchainCommand(final TaskFinalizeArgs args) {
4141
final String chainTaskId = args.getChainTaskId();
4242
final ChainTask chainTask = iexecHubService.getChainTask(chainTaskId).orElse(null);
4343
if (chainTask == null) {
@@ -62,13 +62,13 @@ public boolean canSendBlockchainCommand(TaskFinalizeArgs args) {
6262
return true;
6363
}
6464

65-
private void logError(String chainTaskId, TaskFinalizeArgs args, String error) {
66-
log.error("Finalize task blockchain call is likely to revert ({}) " +
67-
"[chainTaskId:{}, args:{}]", error, chainTaskId, args);
65+
private void logError(final String chainTaskId, final TaskFinalizeArgs args, final String error) {
66+
log.error("Finalize task blockchain call is likely to revert ({}) [chainTaskId:{}, args:{}]",
67+
error, chainTaskId, args);
6868
}
6969

7070
@Override
71-
public TransactionReceipt sendBlockchainCommand(TaskFinalizeArgs args) throws Exception {
71+
public TransactionReceipt sendBlockchainCommand(final TaskFinalizeArgs args) throws Exception {
7272
return iexecHubService.finalizeTask(args.getChainTaskId(),
7373
args.getResultLink(),
7474
args.getCallbackData());

src/main/java/com/iexec/blockchain/command/task/initialize/TaskInitializeBlockchainService.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,12 +28,12 @@ public class TaskInitializeBlockchainService implements CommandBlockchain<TaskIn
2828

2929
private final IexecHubService iexecHubService;
3030

31-
public TaskInitializeBlockchainService(IexecHubService iexecHubService) {
31+
public TaskInitializeBlockchainService(final IexecHubService iexecHubService) {
3232
this.iexecHubService = iexecHubService;
3333
}
3434

3535
@Override
36-
public boolean canSendBlockchainCommand(TaskInitializeArgs args) {
36+
public boolean canSendBlockchainCommand(final TaskInitializeArgs args) {
3737
String chainTaskId = args.getChainTaskId();
3838
if (!iexecHubService.hasEnoughGas()) {
3939
logError(chainTaskId, args, "task is not revealing");
@@ -50,13 +50,13 @@ public boolean canSendBlockchainCommand(TaskInitializeArgs args) {
5050
return true;
5151
}
5252

53-
private void logError(String chainTaskId, TaskInitializeArgs args, String error) {
54-
log.error("Initialize task blockchain call is likely to revert ({}) " +
55-
"[chainTaskId:{}, args:{}]", error, chainTaskId, args);
53+
private void logError(final String chainTaskId, final TaskInitializeArgs args, final String error) {
54+
log.error("Initialize task blockchain call is likely to revert ({}) [chainTaskId:{}, args:{}]",
55+
error, chainTaskId, args);
5656
}
5757

5858
@Override
59-
public TransactionReceipt sendBlockchainCommand(TaskInitializeArgs args) throws Exception {
59+
public TransactionReceipt sendBlockchainCommand(final TaskInitializeArgs args) throws Exception {
6060
return iexecHubService.initializeTask(args.getChainDealId(), args.getTaskIndex());
6161
}
6262

src/test/java/com/iexec/blockchain/chain/BlockchainListenerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ class BlockchainListenerTests {
5757
@DynamicPropertySource
5858
static void registerProperties(DynamicPropertyRegistry registry) {
5959
registry.add("chain.id", () -> "65535");
60-
registry.add("chain.hubAddress", () -> "0xc4b11f41746D3Ad8504da5B383E1aB9aa969AbC7");
61-
registry.add("chain.nodeAddress", () -> getServiceUrl(
60+
registry.add("chain.hub-address", () -> "0xc4b11f41746D3Ad8504da5B383E1aB9aa969AbC7");
61+
registry.add("chain.node-address", () -> getServiceUrl(
6262
environment.getServiceHost(CHAIN_SVC_NAME, CHAIN_SVC_PORT),
6363
environment.getServicePort(CHAIN_SVC_NAME, CHAIN_SVC_PORT)));
6464
registry.add("sprint.data.mongodb.host", () -> environment.getServiceHost(MONGO_SVC_NAME, MONGO_SVC_PORT));

0 commit comments

Comments
 (0)