From adde1b04ff577c8f4b05dc363e9c6347cf2b78d6 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Wed, 19 Jun 2024 13:48:40 +0200 Subject: [PATCH 01/10] Backmerge 4.1.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b020eb..46cfc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. +## [[NEXT]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/vNEXT) 2024 + ## [[4.1.0]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/v4.1.0) 2024-06-17 ### New Features From 81801177a14fd00861554951ef935d1408699214 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Mon, 8 Jul 2024 14:59:29 +0200 Subject: [PATCH 02/10] Add `getUserGasPrice` method to `Web3jAbstractService` (#104) --- CHANGELOG.md | 4 ++ .../poco/chain/Web3jAbstractService.java | 17 ++++++-- .../poco/chain/Web3jAbstractServiceTest.java | 4 +- .../iexec/commons/poco/itest/ChainTests.java | 42 ++++++++++++++++--- .../commons/poco/itest/Web3jTestService.java | 8 +++- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cfc06..e83d2fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [[NEXT]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/vNEXT) 2024 +### New Features + +- Add `getUserGasPrice` method to `Web3jAbstractService`. (#104) + ## [[4.1.0]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/v4.1.0) 2024-06-17 ### New Features diff --git a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java index f135cc6..f341266 100644 --- a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java +++ b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java @@ -265,7 +265,7 @@ public Optional getBalance(String address) { */ public Optional getNetworkGasPrice() { try { - BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice(); + final BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice(); if (gasPrice != null && gasPrice.signum() > 0) { return Optional.of(gasPrice); } @@ -276,17 +276,28 @@ public Optional getNetworkGasPrice() { } public BigInteger getUserGasPrice(float gasPriceMultiplier, long gasPriceCap) { - Optional networkGasPrice = getNetworkGasPrice(); + final Optional networkGasPrice = getNetworkGasPrice(); if (networkGasPrice.isEmpty()) { log.warn("Undefined network gas price (will use default) " + "[userGasPriceCap:{}]", gasPriceCap); return BigInteger.valueOf(gasPriceCap); } - long wishedGasPrice = (long) (networkGasPrice.get().floatValue() * gasPriceMultiplier); + final long wishedGasPrice = (long) (networkGasPrice.get().floatValue() * gasPriceMultiplier); return BigInteger.valueOf(Math.min(wishedGasPrice, gasPriceCap)); } + /** + * Returns gas price following current user parameters defined in the {@code Web3jAbstractService} instance. + *

+ * The gas price will be Min(gasPriceCap, networkGasPrice * gasPriceMultiplier) + * + * @return The gas Price as a {@code BigInteger} + */ + public BigInteger getUserGasPrice() { + return getUserGasPrice(gasPriceMultiplier, gasPriceCap); + } + private ContractGasProvider getWritingContractGasProvider() { return new ContractGasProvider() { diff --git a/src/test/java/com/iexec/commons/poco/chain/Web3jAbstractServiceTest.java b/src/test/java/com/iexec/commons/poco/chain/Web3jAbstractServiceTest.java index 610508a..a81e633 100644 --- a/src/test/java/com/iexec/commons/poco/chain/Web3jAbstractServiceTest.java +++ b/src/test/java/com/iexec/commons/poco/chain/Web3jAbstractServiceTest.java @@ -56,9 +56,9 @@ void shouldNotCreateInstanceWhenNullBlockTime() { @Test void shouldNotCreateInstanceWhenNegativeBlockTime() { - Duration blockTime = Duration.ofSeconds(-1); + final Duration negativeBlockTime = Duration.ofSeconds(-1); assertThrows(IllegalArgumentException.class, - () -> new Web3jAbstractService(chainId, nodeAddress, blockTime, gasPriceMultiplier, gasPriceCap, isSidechain) { + () -> new Web3jAbstractService(chainId, nodeAddress, negativeBlockTime, gasPriceMultiplier, gasPriceCap, isSidechain) { }); } diff --git a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java index 0e5c102..bba425f 100644 --- a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java @@ -36,6 +36,7 @@ import java.io.File; import java.io.IOException; import java.math.BigInteger; +import java.util.Optional; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -47,9 +48,12 @@ class ChainTests { static final String SERVICE_NAME = "poco-chain"; static final int SERVICE_PORT = 8545; + private final String badBlockchainAddress = "http://localhost:5458"; + private Credentials credentials; private IexecHubTestService iexecHubService; private Web3jTestService web3jService; + private String chainNodeAddress; @Container static ComposeContainer environment = new ComposeContainer(new File("docker-compose.yml")) @@ -58,11 +62,11 @@ class ChainTests { @BeforeEach void init() throws CipherException, IOException { - credentials = WalletUtils.loadCredentials("whatever", "src/test/resources/wallet.json"); - final String chainNodeAddress = "http://" + environment.getServiceHost(SERVICE_NAME, SERVICE_PORT) + ":" + + this.credentials = WalletUtils.loadCredentials("whatever", "src/test/resources/wallet.json"); + this.chainNodeAddress = "http://" + environment.getServiceHost(SERVICE_NAME, SERVICE_PORT) + ":" + environment.getServicePort(SERVICE_NAME, SERVICE_PORT); - web3jService = new Web3jTestService(chainNodeAddress); - iexecHubService = new IexecHubTestService(credentials, web3jService); + this.web3jService = new Web3jTestService(chainNodeAddress); + this.iexecHubService = new IexecHubTestService(credentials, web3jService); } @Test @@ -81,7 +85,7 @@ void shouldGetBalance() { @Test void shouldNotGetBalance() { - final Web3jTestService badWeb3jService = new Web3jTestService("http://localhost:8545"); + final Web3jTestService badWeb3jService = new Web3jTestService(badBlockchainAddress); assertThat(badWeb3jService.getBalance(credentials.getAddress())).isEmpty(); } @@ -96,7 +100,7 @@ void shouldGetBlockNumber() throws IOException { @Test void shouldNotGetBlockNumber() { - final Web3jTestService badWeb3jService = new Web3jTestService("http://localhost:8545"); + final Web3jTestService badWeb3jService = new Web3jTestService(badBlockchainAddress); assertThat(badWeb3jService.getLatestBlockNumber()).isZero(); } @@ -150,4 +154,30 @@ void shouldValidateSmartContract() { assertThat(IexecHubSmartContractValidator.validate(iexecHubService.getHubContract())).isTrue(); } + // region gas price + @Test + void shouldGetNetworkGasPrice() { + assertThat(web3jService.getNetworkGasPrice()).isEqualTo(Optional.of(BigInteger.valueOf(8_000_000_000L))); + } + + @Test + void shouldReturnNetworkPriceWhenBelowGasPriceCap() { + assertThat(web3jService.getUserGasPrice()).isEqualTo(8_000_000_000L); + } + + @Test + void shouldReturnUserPriceCapWhenBelowNetworkPrice() { + final float gasPriceMultiplier = 1.2f; + final long gasPriceCap = 5_000_000_000L; + final Web3jTestService newService = new Web3jTestService(chainNodeAddress, gasPriceMultiplier, gasPriceCap); + assertThat(newService.getUserGasPrice()).isEqualTo(gasPriceCap); + } + + @Test + void shouldReturnGasPriceCapOnBlockchainCommunicationError() { + final Web3jTestService badWeb3jService = new Web3jTestService(badBlockchainAddress); + assertThat(badWeb3jService.getUserGasPrice()).isEqualTo(22_000_000_000L); + } + // endregion + } diff --git a/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java b/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java index 82ba456..f5f0b66 100644 --- a/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java +++ b/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java @@ -29,10 +29,14 @@ @Slf4j public class Web3jTestService extends Web3jAbstractService { - static long BLOCK_TIME = 5; + static final long BLOCK_TIME = 5; public Web3jTestService(String chainNodeAddress) { - super(65535, chainNodeAddress, Duration.ofSeconds(BLOCK_TIME), 1.0f, 22_000_000_000L, true); + this(chainNodeAddress, 1.0f, 22_000_000_000L); + } + + public Web3jTestService(String chainNodeAddress, float gasPriceMultiplier, long gasPriceCap) { + super(65535, chainNodeAddress, Duration.ofSeconds(BLOCK_TIME), gasPriceMultiplier, gasPriceCap, true); } public boolean areTxMined(String... txHashes) { From a3a41509f8202c72bf5bbd402b7768d4f8499e4d Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Fri, 18 Oct 2024 09:02:38 +0200 Subject: [PATCH 03/10] Upgrade to Gradle 8.10.2 (#105) --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 2 ++ 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e83d2fd..9a7fd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. - Add `getUserGasPrice` method to `Web3jAbstractService`. (#104) +### Dependency Upgrades + +- Upgrade to Gradle 8.10.2. (#105) + ## [[4.1.0]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/v4.1.0) 2024-06-17 ### New Features diff --git a/build.gradle b/build.gradle index f408fce..41a7983 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java-library' - id 'io.freefair.lombok' version '8.6' + id 'io.freefair.lombok' version '8.10.2' id 'jacoco' - id 'org.sonarqube' version '5.0.0.4638' + id 'org.sonarqube' version '5.1.0.4882' id 'maven-publish' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..df97d72 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From 07349b46f8210b3ea87dd91569636a385a2b05c1 Mon Sep 17 00:00:00 2001 From: nabil-Tounarti <117689544+nabil-Tounarti@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:16:38 +0100 Subject: [PATCH 04/10] Use poco-chain with poco v5.5.0 and voucher v1.0.0 in tests (#106) --- CHANGELOG.md | 1 + docker-compose.yml | 2 +- .../poco/itest/AssetRegistriesTests.java | 3 ++- .../iexec/commons/poco/itest/ChainTests.java | 12 +++++----- .../poco/itest/IexecHubTestService.java | 2 +- .../commons/poco/itest/MatchOrdersTests.java | 23 ++++++++++--------- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7fd33..757fb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file. - Restrict several methods visibility in `IexecHubAbstractService`. (#96) - Avoid exceptions during `IexecHubAbstractService` and `Web3jAbstractService` objects creation. (#99) - Improve `SignatureUtils`: remove dead code and remove cleanly unused parameter in `hashAndSign`. (#100) +- Use `poco-chain` with `poco v5.5.0` and `voucher v1.0.0` in tests. (#106) ### Dependency Upgrades diff --git a/docker-compose.yml b/docker-compose.yml index 7c4ea27..44aead5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: poco-chain: - image: docker-regis.iex.ec/poco-chain:native-v5.4.2-5s + image: docker-regis.iex.ec/poco-chain:1.0.0-poco-v5.5.0-voucher-v1.0.0-nethermind expose: - "8545" diff --git a/src/test/java/com/iexec/commons/poco/itest/AssetRegistriesTests.java b/src/test/java/com/iexec/commons/poco/itest/AssetRegistriesTests.java index 4c2e5b9..b312de0 100644 --- a/src/test/java/com/iexec/commons/poco/itest/AssetRegistriesTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/AssetRegistriesTests.java @@ -52,7 +52,8 @@ class AssetRegistriesTests { @Container static ComposeContainer environment = new ComposeContainer(new File("docker-compose.yml")) - .withExposedService("poco-chain", 8545); + .withPull(true) + .withExposedService(SERVICE_NAME, SERVICE_PORT); @BeforeEach void init() throws CipherException, IOException { diff --git a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java index bba425f..e8ceaec 100644 --- a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java @@ -36,7 +36,6 @@ import java.io.File; import java.io.IOException; import java.math.BigInteger; -import java.util.Optional; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -57,7 +56,8 @@ class ChainTests { @Container static ComposeContainer environment = new ComposeContainer(new File("docker-compose.yml")) - .withExposedService("poco-chain", 8545); + .withPull(true) + .withExposedService(SERVICE_NAME, SERVICE_PORT); @BeforeEach @@ -73,14 +73,14 @@ void init() throws CipherException, IOException { void shouldGetAccount() { final ChainAccount chainAccount = iexecHubService.getChainAccount(credentials.getAddress()).orElse(null); assertThat(chainAccount).isNotNull(); - assertThat(chainAccount.getDeposit()).isEqualTo(10_000_000L); + assertThat(chainAccount.getDeposit()).isEqualTo(40_178L); assertThat(chainAccount.getLocked()).isZero(); } @Test void shouldGetBalance() { final BigInteger balance = web3jService.getBalance(credentials.getAddress()).orElse(null); - assertThat(balance).isEqualTo(new BigInteger("1000000000000000000000000000000000000000000")); + assertThat(balance).isEqualTo(new BigInteger("3188369135434504514964210500676909925639291603846501657344")); } @Test @@ -157,12 +157,12 @@ void shouldValidateSmartContract() { // region gas price @Test void shouldGetNetworkGasPrice() { - assertThat(web3jService.getNetworkGasPrice()).isEqualTo(Optional.of(BigInteger.valueOf(8_000_000_000L))); + assertThat(web3jService.getNetworkGasPrice()).isEmpty(); } @Test void shouldReturnNetworkPriceWhenBelowGasPriceCap() { - assertThat(web3jService.getUserGasPrice()).isEqualTo(8_000_000_000L); + assertThat(web3jService.getUserGasPrice()).isEqualTo(22_000_000_000L); } @Test diff --git a/src/test/java/com/iexec/commons/poco/itest/IexecHubTestService.java b/src/test/java/com/iexec/commons/poco/itest/IexecHubTestService.java index 9200718..a72023d 100644 --- a/src/test/java/com/iexec/commons/poco/itest/IexecHubTestService.java +++ b/src/test/java/com/iexec/commons/poco/itest/IexecHubTestService.java @@ -33,7 +33,7 @@ public class IexecHubTestService extends IexecHubAbstractService { static final BigInteger GAS_PRICE = BigInteger.valueOf(22_000_000_000L); static final BigInteger GAS_LIMIT = BigInteger.valueOf(1_000_000L); - static final String IEXEC_HUB_ADDRESS = "0xC129e7917b7c7DeDfAa5Fff1FB18d5D7050fE8ca"; + static final String IEXEC_HUB_ADDRESS = "0xc4b11f41746D3Ad8504da5B383E1aB9aa969AbC7"; private static final String ASSET_MULTI_ADDRESS = "multiAddress"; private static final String ASSET_CHECKSUM = Numeric.toHexStringNoPrefix(new byte[32]); diff --git a/src/test/java/com/iexec/commons/poco/itest/MatchOrdersTests.java b/src/test/java/com/iexec/commons/poco/itest/MatchOrdersTests.java index 57da3de..73d7cb6 100644 --- a/src/test/java/com/iexec/commons/poco/itest/MatchOrdersTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/MatchOrdersTests.java @@ -61,6 +61,7 @@ class MatchOrdersTests { @Container static ComposeContainer environment = new ComposeContainer(new File("docker-compose.yml")) + .withPull(true) .withExposedService(SERVICE_NAME, SERVICE_PORT); @BeforeEach @@ -88,34 +89,34 @@ void shouldMatchOrdersWithIexecHubService() throws Exception { BytesUtils.EMPTY_HEX_STRING_32, "{}" ); - assertThat(appAddress).isEqualTo("0x564ef252a271ff68a74266e8f574ed827297caf4"); + assertThat(appAddress).isEqualTo("0x909375d8bc4e9c26afa996bad7571b88b6cefa72"); String datasetAddress = iexecHubService.createDataset( "my-dataset-1", "multiAddress", BytesUtils.EMPTY_HEX_STRING_32 ); - assertThat(datasetAddress).isEqualTo("0x8e0c3441b788fe2bafb29a729c59d9e9a9ca5483"); + assertThat(datasetAddress).isEqualTo("0x049d8b40e50282ae8b34780240ff49235b87bf59"); String workerpoolAddress = iexecHubService.createWorkerpool("my-workerpool-1"); - assertThat(workerpoolAddress).isEqualTo("0x1d9340e5c33c6d289250df3b1ce31bc3ac6b16c0"); + assertThat(workerpoolAddress).isEqualTo("0x4f1e8ec6d1fb6ce8309d816ecacde77da4f7de9c"); final AppOrder signedAppOrder = ordersService.buildSignedAppOrder(appAddress); assertThat(signedAppOrder.getSign()) - .isEqualTo("0xbcb64941a9e7162f6f3a2b9e636596eec1bee8d420556469e11641905b45c7785691c310e4cce295311ebf6439acf2a6a943fb8d5db87a1971f979a83e6ea34d1b"); + .isEqualTo("0x95bc07e7a64415d7529ded8c4730c17870d519e569857533e273f350235b4ebf17d46ffff80b37452cca1b6540db127cd991ce91c6cb7ba3d2a1bc9409d70ad51b"); final DatasetOrder signedDatasetOrder = ordersService.buildSignedDatasetOrder(datasetAddress); assertThat(signedDatasetOrder.getSign()) - .isEqualTo("0x014011344b5684761605fd34ecdcebd34b1efc249658a1f975d8c2a26a7adc5b68dcda7bd298cc578aea99877b5ae4e7b0f3cacd307cd4fdfb382d87a0fdc5e11c"); + .isEqualTo("0x08195ca0a92aea60ff6e08c7b8d089d50b650a4ac214d1233d6857cc58349a8f710fdc16121bf028e035b26774c2a341c67e556119bec42532c4e0586256f5481b"); final WorkerpoolOrder signedWorkerpoolOrder = ordersService.buildSignedWorkerpoolOrder(workerpoolAddress); assertThat(signedWorkerpoolOrder.getSign()) - .isEqualTo("0xb3b4cc51859bf106562baecf6261bfa8e168a11e06dcc82b91d0ccfbe16be14c414526c4a2e0908ef61eae6a91ec96a8d977fda4b204ac06d1b7e477abe8ea8d1b"); + .isEqualTo("0x012dd3697776b6ce1a8ea8dc8541155dff59422bc9f21c28e61009817934ec943ff274d3831432b9ee6b2206b8b0a7e669e38cf8fa9c73bd339dd6a6e1514c8b1b"); final RequestOrder signedRequestOrder = ordersService.buildSignedRequestOrder( signedAppOrder, signedDatasetOrder, signedWorkerpoolOrder); assertThat(signedRequestOrder.getSign()) - .isEqualTo("0xa6a6eea2c49c7df388d8a265926f19bf4cac049f73875f1dbbe813ce088a7e833a2552cb33a14927b350c858349d3bcedbe23286edf912ad585dc102a1249e751c"); + .isEqualTo("0x66d7c636055ff2d03e5250065cc24481ed7bfa80cc0b92f2437feb9690c9ad30077351549030ab65eaf647de8b2b9e7ac758d6b6cb53550d4771b213b5f206e11b"); BigInteger nonce = signerService.getNonce(); String matchOrdersTxData = MatchOrdersDataEncoder.encode(signedAppOrder, signedDatasetOrder, signedWorkerpoolOrder, signedRequestOrder); @@ -156,20 +157,20 @@ void shouldMatchOrdersWithSignerService() throws IOException { final AppOrder signedAppOrder = ordersService.buildSignedAppOrder(predictedAppAddress); assertThat(signedAppOrder.getSign()) - .isEqualTo("0xc200bf4ac4170d3949831d0049a184b7ee612d74a54f4d12af62cfa734330b3523c1db3a57a4f91d03932313aed3c95200e7950d52cc2f98ee0e08cb0648cbf31c"); + .isEqualTo("0x8a3d3283f11e22318ed65fc22dcf1101a5905c1fbfb2dee67981668de7130647175ca7b728f066d764006b9e95cc4f8097157de7ee6b6d917479f6598d055ff81b"); final DatasetOrder signedDatasetOrder = ordersService.buildSignedDatasetOrder(predictedDatasetAddress); assertThat(signedDatasetOrder.getSign()) - .isEqualTo("0xfa2bf8bbfde7db118970aae8781dc78210bd9433111f1332f549aa003f7f28cc6c68c038aa35b5ddfc98242a8ede07ea8fa073f2b0e7f1247b8b97fadc4837c91c"); + .isEqualTo("0x936734f59674cb89ba77616b4a00f91b9b5ec938723b4465b15bca74e3e8274a72db4ad60cc5097dbecd5f721f7d241efe5905fe6d49968dffe6a14ca33c82951b"); final WorkerpoolOrder signedWorkerpoolOrder = ordersService.buildSignedWorkerpoolOrder(predictedWorkerpoolAddress); assertThat(signedWorkerpoolOrder.getSign()) - .isEqualTo("0xf6474336a10e3ec12ff65e721fee6ba53763f937a1b54659332f61cf1770b4b26947df22ad89e8623aa985763e917e9f66670e51befc7195b16b0e8db365c13c1c"); + .isEqualTo("0x0d204f203e07e8250bb587fbcd64adf8da95accf2f3cc686e9585794f3d1aad076a996d5d2fe6ff4b4cabd8f127fc7543468d784c74420a8440d9742a72bd0ee1b"); final RequestOrder signedRequestOrder = ordersService.buildSignedRequestOrder( signedAppOrder, signedDatasetOrder, signedWorkerpoolOrder); assertThat(signedRequestOrder.getSign()) - .isEqualTo("0x17d53276b2f125953d147af94381388ee20e6be80a6c76cf5359c8a1f04c0f0b670a3bd890fdca9307a11ef52927634b65399fa3a03fc6e89f17d5e3c383b4c31c"); + .isEqualTo("0xf6117dcc9a2feac58fcf3bb9b4cfebac9cd2f538c6611dce8ff2c3a7ea1d5c464c4e46e8622f5faeb2ec88dc6e5231fb97163494acbccf54bf8650477b11c4781b"); nonce = nonce.add(BigInteger.ONE); final String matchOrdersTxData = MatchOrdersDataEncoder.encode(signedAppOrder, signedDatasetOrder, signedWorkerpoolOrder, signedRequestOrder); From 0e9ec6a88b83298500594cdcd7a449570351238d Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Tue, 5 Nov 2024 12:40:29 +0100 Subject: [PATCH 05/10] Prefer methods with `ECKeyPair` parameter when signing prefixed messages (#107) --- CHANGELOG.md | 9 ++++- .../commons/poco/chain/SignerService.java | 4 +- .../commons/poco/utils/SignatureUtils.java | 40 ++++++++++++------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 757fb98..80ce82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ All notable changes to this project will be documented in this file. - Add `getUserGasPrice` method to `Web3jAbstractService`. (#104) +### Bug Fixes + +- Prefer methods with ECKEyPair parameter when signing prefixed messages. (#107) + +### Quality + +- Use `poco-chain` with `poco v5.5.0` and `voucher v1.0.0` in tests. (#106) + ### Dependency Upgrades - Upgrade to Gradle 8.10.2. (#105) @@ -30,7 +38,6 @@ All notable changes to this project will be documented in this file. - Restrict several methods visibility in `IexecHubAbstractService`. (#96) - Avoid exceptions during `IexecHubAbstractService` and `Web3jAbstractService` objects creation. (#99) - Improve `SignatureUtils`: remove dead code and remove cleanly unused parameter in `hashAndSign`. (#100) -- Use `poco-chain` with `poco v5.5.0` and `voucher v1.0.0` in tests. (#106) ### Dependency Upgrades diff --git a/src/main/java/com/iexec/commons/poco/chain/SignerService.java b/src/main/java/com/iexec/commons/poco/chain/SignerService.java index bfe1fc1..560011b 100644 --- a/src/main/java/com/iexec/commons/poco/chain/SignerService.java +++ b/src/main/java/com/iexec/commons/poco/chain/SignerService.java @@ -34,7 +34,6 @@ import org.web3j.protocol.core.methods.response.Transaction; import org.web3j.protocol.exceptions.JsonRpcError; import org.web3j.tx.RawTransactionManager; -import org.web3j.utils.Numeric; import java.io.IOException; import java.math.BigInteger; @@ -108,8 +107,7 @@ public BigInteger getNonce() { * Signs messages with Ethereum prefix */ public Signature signMessageHash(String messageHash) { - String hexPrivateKey = Numeric.toHexStringWithPrefix(credentials.getEcKeyPair().getPrivateKey()); - return SignatureUtils.signMessageHashAndGetSignature(messageHash, hexPrivateKey); + return SignatureUtils.signMessageHashAndGetSignature(messageHash, credentials.getEcKeyPair()); } public String signEIP712Entity(EIP712Entity eip712Entity) { diff --git a/src/main/java/com/iexec/commons/poco/utils/SignatureUtils.java b/src/main/java/com/iexec/commons/poco/utils/SignatureUtils.java index f9d015e..574aad6 100644 --- a/src/main/java/com/iexec/commons/poco/utils/SignatureUtils.java +++ b/src/main/java/com/iexec/commons/poco/utils/SignatureUtils.java @@ -108,27 +108,39 @@ private static String createStringFromSignature(Sign.SignatureData sign) { return String.join("", r, Numeric.cleanHexPrefix(s), Numeric.cleanHexPrefix(v)); } - /* - * web3j signMessageHash(..) base method [built on EthereumMessageHash] - * */ + /** + * Sign a message prefixed with {@code "\x19Ethereum Signed Message:\n" + len(message)}. + * + * @param messageHash Hashed data to sign + * @param ecKeyPair Key pair to sign the message + * @return The signature data + * @see ERC-191 specification. + */ private static Sign.SignatureData signMessageHash(String messageHash, ECKeyPair ecKeyPair) { return Sign.signPrefixedMessage(stringToBytes(messageHash), ecKeyPair); } - /* - * web3j signMessageHash(..) returns Sign.SignatureData [built on EthereumMessageHash] - * */ - public static Sign.SignatureData signMessageHashAndGetSignatureData(String messageHash, String privateKey) { - ECKeyPair ecKeyPair = ECKeyPair.create(BytesUtils.hexStringToBytes32(privateKey)); - return signMessageHash(messageHash, ecKeyPair); + /** + * Sign a message prefixed with {@code "\x19Ethereum Signed Message:\n" + len(message)}. + * + * @param messageHash Hashed data to sign + * @param ecKeyPair Key pair to sign the message + * @return The signature data wrapped in a {@link Signature} instance + */ + public static Signature signMessageHashAndGetSignature(String messageHash, ECKeyPair ecKeyPair) { + return new Signature(signMessageHash(messageHash, ecKeyPair)); } - /* - * iExec signMessageHash(..) returns Signature [built on EthereumMessageHash] - * */ + /** + * Sign a message prefixed with {@code "\x19Ethereum Signed Message:\n" + len(message)}. + * + * @param messageHash Hashed data to sign + * @param privateKey Private key to use, an {@code ECKeyPair} will be created from this key + * @return The signature data wrapper in a {@link Signature} instance + */ public static Signature signMessageHashAndGetSignature(String messageHash, String privateKey) { - Sign.SignatureData signatureData = signMessageHashAndGetSignatureData(messageHash, privateKey); - return new Signature(signatureData); + final ECKeyPair ecKeyPair = ECKeyPair.create(Numeric.hexStringToByteArray(privateKey)); + return signMessageHashAndGetSignature(messageHash, ecKeyPair); } /* From 8592ce3ca0997f30a25c4e34a559f1c157cbc844 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Tue, 12 Nov 2024 14:14:36 +0100 Subject: [PATCH 06/10] Manage deal parameters in a single field and add assets owner and assets price fields in `TaskDescription` (#108) --- CHANGELOG.md | 1 + .../commons/poco/task/TaskDescription.java | 61 ++- .../poco/task/TaskDescriptionTests.java | 414 +++++++++--------- 3 files changed, 271 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ce82b..21abdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. ### Quality - Use `poco-chain` with `poco v5.5.0` and `voucher v1.0.0` in tests. (#106) +- Manage deal parameters in a single field and add assets owner and assets price fields in `TaskDescription`. (#108) ### Dependency Upgrades diff --git a/src/main/java/com/iexec/commons/poco/task/TaskDescription.java b/src/main/java/com/iexec/commons/poco/task/TaskDescription.java index 5c1b296..c0e1606 100644 --- a/src/main/java/com/iexec/commons/poco/task/TaskDescription.java +++ b/src/main/java/com/iexec/commons/poco/task/TaskDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.iexec.commons.poco.chain.ChainDeal; import com.iexec.commons.poco.chain.ChainTask; +import com.iexec.commons.poco.chain.DealParams; import com.iexec.commons.poco.dapp.DappType; import com.iexec.commons.poco.tee.TeeEnclaveConfiguration; import com.iexec.commons.poco.tee.TeeFramework; @@ -38,7 +39,17 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class TaskDescription { + // computed data, not available on-chain String chainTaskId; + + // assets + String appOwner; + BigInteger appPrice; + String datasetOwner; + BigInteger datasetPrice; + String workerpoolOwner; + BigInteger workerpoolPrice; + String requester; String beneficiary; String callback; @@ -46,6 +57,10 @@ public class TaskDescription { String appUri; String appAddress; TeeEnclaveConfiguration appEnclaveConfiguration; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) String cmd; boolean isTeeTask; TeeFramework teeFramework; @@ -55,12 +70,34 @@ public class TaskDescription { String datasetUri; String datasetName; String datasetChecksum; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) List inputFiles; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) boolean isResultEncryption; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) String resultStorageProvider; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) String resultStorageProxy; String smsUrl; + /** + * @deprecated Use dealParams instead + */ + @Deprecated(forRemoval = true) Map secrets; + @Builder.Default + DealParams dealParams = DealParams.builder().build(); BigInteger trust; // from task int botIndex; @@ -101,16 +138,15 @@ public boolean containsCallback() { * @return true if at least one input file is present, false otherwise */ public boolean containsInputFiles() { - return inputFiles != null && !inputFiles.isEmpty(); + return (dealParams != null && dealParams.getIexecInputFiles() != null && !dealParams.getIexecInputFiles().isEmpty()) + || (inputFiles != null && !inputFiles.isEmpty()); } public String getAppCommand() { - String appArgs = appEnclaveConfiguration.getEntrypoint(); - //TODO: Add unit test - if (!StringUtils.isEmpty(cmd)) { - appArgs = appArgs + " " + cmd; - } - return appArgs; + final String args = (dealParams != null && !StringUtils.isEmpty(dealParams.getIexecArgs())) ? + dealParams.getIexecArgs() : cmd; + return StringUtils.isEmpty(args) ? appEnclaveConfiguration.getEntrypoint() : + appEnclaveConfiguration.getEntrypoint() + " " + args; } /** @@ -138,7 +174,7 @@ public boolean isEligibleToContributeAndFinalize() { * @param chainTask On-chain task from PoCo smart contracts * @return the created taskDescription */ - public static TaskDescription toTaskDescription(ChainDeal chainDeal, ChainTask chainTask) { + public static TaskDescription toTaskDescription(final ChainDeal chainDeal, final ChainTask chainTask) { if (chainDeal == null || chainTask == null) { return null; } @@ -155,6 +191,12 @@ public static TaskDescription toTaskDescription(ChainDeal chainDeal, ChainTask c final String tag = chainDeal.getTag(); return TaskDescription.builder() .chainTaskId(chainTask.getChainTaskId()) + .appOwner(chainDeal.getDappOwner()) + .appPrice(chainDeal.getDappPrice()) + .datasetOwner(chainDeal.getDataOwner()) + .datasetPrice(chainDeal.getDataPrice()) + .workerpoolOwner(chainDeal.getPoolOwner()) + .workerpoolPrice(chainDeal.getPoolPrice()) .requester(chainDeal .getRequester()) .beneficiary(chainDeal @@ -183,6 +225,7 @@ public static TaskDescription toTaskDescription(ChainDeal chainDeal, ChainTask c .getIexecResultStorageProxy()) .secrets(chainDeal.getParams() .getIexecSecrets()) + .dealParams(chainDeal.getParams()) .datasetAddress(datasetAddress) .datasetUri(datasetUri) .datasetName(datasetName) diff --git a/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java b/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java index 9b60845..5a32525 100644 --- a/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java +++ b/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java @@ -1,18 +1,15 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * - * Licensed under the Apache License, -Version 2.0 (the "License"); + * 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 + * 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. + * 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. */ @@ -25,54 +22,131 @@ import com.iexec.commons.poco.tee.TeeFramework; import com.iexec.commons.poco.tee.TeeUtils; import com.iexec.commons.poco.utils.BytesUtils; -import org.junit.jupiter.api.Assertions; 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; +import org.junit.jupiter.params.provider.NullSource; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.stream.Stream; + +import static com.iexec.commons.poco.utils.BytesUtils.EMPTY_ADDRESS; +import static org.junit.jupiter.api.Assertions.*; class TaskDescriptionTests { - public static final String CHAIN_TASK_ID = "chainTaskId"; - public static final String REQUESTER = "requester"; - public static final String BENEFICIARY = "beneficiary"; - public static final String CALLBACK = "callback"; - public static final DappType APP_TYPE = DappType.DOCKER; - public static final String APP_URI = "https://uri"; - public static final String APP_ADDRESS = "appAddress"; - public static final TeeEnclaveConfiguration enclaveConfig = TeeEnclaveConfiguration.builder().build(); - public static final String CMD = "cmd"; - public static final int MAX_EXECUTION_TIME = 1; - public static final boolean IS_TEE_TASK = true; - public static final TeeFramework TEE_FRAMEWORK = TeeFramework.SCONE; - public static final int BOT_SIZE = 1; - public static final int BOT_FIRST = 2; - public static final int TASK_IDX = 3; - public static final String DATASET_ADDRESS = "datasetAddress"; - public static final String DATASET_URI = "https://datasetUri"; - public static final String DATASET_NAME = "datasetName"; - public static final String DATASET_CHECKSUM = "datasetChecksum"; - public static final List INPUT_FILES = Collections.singletonList("inputFiles"); - public static final boolean IS_CALLBACK_REQUESTED = true; - public static final boolean IS_RESULT_ENCRYPTION = true; - public static final String RESULT_STORAGE_PROVIDER = "resultStorageProvider"; - public static final String RESULT_STORAGE_PROXY = "resultStorageProxy"; - public static final BigInteger TRUST = BigInteger.ONE; + private static final String APP_OWNER = "0x1"; + private static final BigInteger APP_PRICE = BigInteger.ZERO; + private static final String DATA_OWNER = "0x2"; + private static final BigInteger DATA_PRICE = BigInteger.ONE; + private static final String WORKERPOOL_OWNER = "0x3"; + private static final BigInteger WORKERPOOL_PRICE = BigInteger.TEN; + + private static final String CHAIN_TASK_ID = "chainTaskId"; + private static final String REQUESTER = "requester"; + private static final String BENEFICIARY = "beneficiary"; + private static final String CALLBACK = "callback"; + private static final DappType APP_TYPE = DappType.DOCKER; + private static final String APP_URI = "https://uri"; + private static final String APP_ADDRESS = "appAddress"; + private static final String ENTRYPOINT = "entrypoint"; + private static final String CMD = "cmd"; + private static final int MAX_EXECUTION_TIME = 1; + private static final boolean IS_TEE_TASK = true; + private static final TeeFramework TEE_FRAMEWORK = TeeFramework.SCONE; + private static final int BOT_SIZE = 1; + private static final int BOT_FIRST = 2; + private static final int TASK_IDX = 3; + private static final String DATASET_ADDRESS = "datasetAddress"; + private static final String DATASET_URI = "https://datasetUri"; + private static final String DATASET_NAME = "datasetName"; + private static final String DATASET_CHECKSUM = "datasetChecksum"; + private static final List INPUT_FILES = Collections.singletonList("inputFiles"); + private static final boolean IS_RESULT_ENCRYPTION = true; + private static final String RESULT_STORAGE_PROVIDER = "resultStorageProvider"; + private static final String RESULT_STORAGE_PROXY = "resultStorageProxy"; + private static final BigInteger TRUST = BigInteger.ONE; + + @Test + void toTaskDescriptionWithNullDeal() { + assertNull(TaskDescription.toTaskDescription(null, null)); + assertNull(TaskDescription.toTaskDescription(ChainDeal.builder().build(), null)); + assertNull(TaskDescription.toTaskDescription(null, ChainTask.builder().build())); + } @Test - void shouldBuildAndGetTaskDescription() { - TaskDescription task = TaskDescription.builder() + void toTaskDescription() { + final ChainCategory chainCategory = ChainCategory.builder() + .maxExecutionTime(MAX_EXECUTION_TIME) + .build(); + final TeeEnclaveConfiguration enclaveConfiguration = TeeEnclaveConfiguration.builder() + .entrypoint(ENTRYPOINT) + .build(); + final ChainApp chainApp = ChainApp.builder() + .chainAppId(APP_ADDRESS) + .type(APP_TYPE.toString()) + .uri(BytesUtils.bytesToString(APP_URI.getBytes(StandardCharsets.UTF_8))) + .enclaveConfiguration(enclaveConfiguration) + .build(); + final ChainDataset chainDataset = ChainDataset.builder() + .chainDatasetId(DATASET_ADDRESS) + .name(DATASET_NAME) + .uri(BytesUtils.bytesToString(DATASET_URI.getBytes(StandardCharsets.UTF_8))) + .checksum(DATASET_CHECKSUM) + .build(); + final DealParams dealParams = DealParams.builder() + .iexecArgs(CMD) + .iexecInputFiles(INPUT_FILES) + .iexecResultStorageProvider(RESULT_STORAGE_PROVIDER) + .iexecResultStorageProxy(RESULT_STORAGE_PROXY) + .iexecResultEncryption(IS_RESULT_ENCRYPTION) + .build(); + final ChainDeal chainDeal = ChainDeal.builder() + .dappOwner(APP_OWNER) + .dappPrice(APP_PRICE) + .dataOwner(DATA_OWNER) + .dataPrice(DATA_PRICE) + .poolOwner(WORKERPOOL_OWNER) + .poolPrice(WORKERPOOL_PRICE) + .requester(REQUESTER) + .beneficiary(BENEFICIARY) + .callback(CALLBACK) + .chainApp(chainApp) + .params(dealParams) + .chainDataset(chainDataset) + .tag(TeeUtils.TEE_SCONE_ONLY_TAG) // any supported TEE tag + .chainCategory(chainCategory) + .botFirst(BigInteger.valueOf(BOT_FIRST)) + .botSize(BigInteger.valueOf(BOT_SIZE)) + .trust(TRUST) + .build(); + final ChainTask chainTask = ChainTask.builder() + .dealid(chainDeal.getChainDealId()) + .chainTaskId(CHAIN_TASK_ID) + .idx(TASK_IDX) + .build(); + + final TaskDescription task = TaskDescription.toTaskDescription(chainDeal, chainTask); + + final TaskDescription expectedTaskDescription = TaskDescription.builder() .chainTaskId(CHAIN_TASK_ID) + .appOwner(APP_OWNER) + .appPrice(APP_PRICE) + .datasetOwner(DATA_OWNER) + .datasetPrice(DATA_PRICE) + .workerpoolOwner(WORKERPOOL_OWNER) + .workerpoolPrice(WORKERPOOL_PRICE) .requester(REQUESTER) .beneficiary(BENEFICIARY) .callback(CALLBACK) .appType(APP_TYPE) .appUri(APP_URI) .appAddress(APP_ADDRESS) - .appEnclaveConfiguration(enclaveConfig) - .cmd(CMD) + .appEnclaveConfiguration(enclaveConfiguration) .maxExecutionTime(MAX_EXECUTION_TIME) .isTeeTask(IS_TEE_TASK) .teeFramework(TEE_FRAMEWORK) @@ -83,160 +157,24 @@ void shouldBuildAndGetTaskDescription() { .datasetUri(DATASET_URI) .datasetName(DATASET_NAME) .datasetChecksum(DATASET_CHECKSUM) + .cmd(CMD) .inputFiles(INPUT_FILES) .isResultEncryption(IS_RESULT_ENCRYPTION) .resultStorageProvider(RESULT_STORAGE_PROVIDER) .resultStorageProxy(RESULT_STORAGE_PROXY) + .secrets(Collections.emptyMap()) + .dealParams(dealParams) .trust(TRUST) .build(); - Assertions.assertEquals(CHAIN_TASK_ID, - task.getChainTaskId()); - Assertions.assertEquals(REQUESTER, - task.getRequester()); - Assertions.assertEquals(BENEFICIARY, - task.getBeneficiary()); - Assertions.assertEquals(CALLBACK, - task.getCallback()); - Assertions.assertEquals(APP_TYPE, - task.getAppType()); - Assertions.assertEquals(APP_URI, - task.getAppUri()); - Assertions.assertEquals(APP_ADDRESS, - task.getAppAddress()); - Assertions.assertEquals(enclaveConfig, - task.getAppEnclaveConfiguration()); - Assertions.assertEquals(CMD, - task.getCmd()); - Assertions.assertEquals(MAX_EXECUTION_TIME, - task.getMaxExecutionTime()); - Assertions.assertEquals(IS_TEE_TASK, - task.isTeeTask()); - Assertions.assertEquals(TEE_FRAMEWORK, - task.getTeeFramework()); - Assertions.assertEquals(TASK_IDX, - task.getBotIndex()); - Assertions.assertEquals(BOT_SIZE, - task.getBotSize()); - Assertions.assertEquals(BOT_FIRST, - task.getBotFirstIndex()); - Assertions.assertEquals(DATASET_URI, - task.getDatasetUri()); - Assertions.assertEquals(DATASET_NAME, - task.getDatasetName()); - Assertions.assertEquals(DATASET_CHECKSUM, - task.getDatasetChecksum()); - Assertions.assertEquals(INPUT_FILES, - task.getInputFiles()); - Assertions.assertEquals(IS_CALLBACK_REQUESTED, - task.containsCallback()); - Assertions.assertEquals(IS_RESULT_ENCRYPTION, - task.isResultEncryption()); - Assertions.assertEquals(RESULT_STORAGE_PROVIDER, - task.getResultStorageProvider()); - Assertions.assertEquals(RESULT_STORAGE_PROXY, - task.getResultStorageProxy()); - Assertions.assertEquals(TRUST, - task.getTrust()); - Assertions.assertTrue(task.containsDataset()); - } - @Test - void toTaskDescriptionWithNullDeal() { - Assertions.assertNull(TaskDescription.toTaskDescription(null, null)); - } - - @Test - void toTaskDescription() { - ChainDeal chainDeal = ChainDeal.builder() - .requester(REQUESTER) - .beneficiary(BENEFICIARY) - .callback(CALLBACK) - .chainApp(ChainApp.builder() - .chainAppId(APP_ADDRESS) - .type(APP_TYPE.toString()) - .uri(BytesUtils.bytesToString(APP_URI.getBytes(StandardCharsets.UTF_8))) - .build()) - .params(DealParams.builder() - .iexecArgs(CMD) - .iexecInputFiles(INPUT_FILES) - .iexecResultStorageProvider(RESULT_STORAGE_PROVIDER) - .iexecResultStorageProxy(RESULT_STORAGE_PROXY) - .iexecResultEncryption(IS_RESULT_ENCRYPTION) - .build()) - .chainDataset(ChainDataset.builder() - .chainDatasetId(DATASET_ADDRESS) - .name(DATASET_NAME) - .uri(BytesUtils.bytesToString(DATASET_URI.getBytes(StandardCharsets.UTF_8))) - .checksum(DATASET_CHECKSUM).build()) - .tag(TeeUtils.TEE_SCONE_ONLY_TAG) // any supported TEE tag - .chainCategory(ChainCategory.builder() - .maxExecutionTime(MAX_EXECUTION_TIME) - .build()) - .botFirst(BigInteger.valueOf(BOT_FIRST)) - .botSize(BigInteger.valueOf(BOT_SIZE)) - .trust(TRUST) - .build(); - - ChainTask chainTask = ChainTask.builder() - .dealid(chainDeal.getChainDealId()) - .chainTaskId(CHAIN_TASK_ID) - .idx(TASK_IDX) - .build(); - - TaskDescription task = - TaskDescription.toTaskDescription(chainDeal, chainTask); - - Assertions.assertEquals(CHAIN_TASK_ID, - task.getChainTaskId()); - Assertions.assertEquals(REQUESTER, - task.getRequester()); - Assertions.assertEquals(BENEFICIARY, - task.getBeneficiary()); - Assertions.assertEquals(CALLBACK, - task.getCallback()); - Assertions.assertEquals(APP_TYPE, - task.getAppType()); - Assertions.assertEquals(APP_URI, - task.getAppUri()); - Assertions.assertEquals(APP_ADDRESS, - task.getAppAddress()); - Assertions.assertEquals(CMD, - task.getCmd()); - Assertions.assertEquals(MAX_EXECUTION_TIME, - task.getMaxExecutionTime()); - Assertions.assertEquals(IS_TEE_TASK, - task.isTeeTask()); - Assertions.assertEquals(TEE_FRAMEWORK, - task.getTeeFramework()); - Assertions.assertEquals(TASK_IDX, - task.getBotIndex()); - Assertions.assertEquals(BOT_SIZE, - task.getBotSize()); - Assertions.assertEquals(BOT_FIRST, - task.getBotFirstIndex()); - Assertions.assertEquals(DATASET_URI, - task.getDatasetUri()); - Assertions.assertEquals(DATASET_NAME, - task.getDatasetName()); - Assertions.assertEquals(DATASET_CHECKSUM, - task.getDatasetChecksum()); - Assertions.assertEquals(INPUT_FILES, - task.getInputFiles()); - Assertions.assertEquals(IS_CALLBACK_REQUESTED, - task.containsCallback()); - Assertions.assertEquals(IS_RESULT_ENCRYPTION, - task.isResultEncryption()); - Assertions.assertEquals(RESULT_STORAGE_PROVIDER, - task.getResultStorageProvider()); - Assertions.assertEquals(RESULT_STORAGE_PROXY, - task.getResultStorageProxy()); - Assertions.assertEquals(TRUST, - task.getTrust()); + assertEquals(expectedTaskDescription, task); + assertTrue(task.containsCallback()); } + // region containsDataset @Test void shouldContainDataset() { - Assertions.assertTrue(TaskDescription.builder() + assertTrue(TaskDescription.builder() .datasetAddress(DATASET_ADDRESS) .datasetUri(DATASET_URI) .datasetName(DATASET_NAME) @@ -244,7 +182,7 @@ void shouldContainDataset() { .build() .containsDataset()); - Assertions.assertTrue(TaskDescription.builder() + assertTrue(TaskDescription.builder() .datasetAddress(DATASET_ADDRESS) .datasetUri(DATASET_URI) // .datasetName(DATASET_NAME) @@ -255,7 +193,7 @@ void shouldContainDataset() { @Test void shouldNotContainDataset() { - Assertions.assertFalse(TaskDescription.builder() + assertFalse(TaskDescription.builder() // .datasetAddress(DATASET_ADDRESS) .datasetUri(DATASET_URI) .datasetName(DATASET_NAME) @@ -263,7 +201,15 @@ void shouldNotContainDataset() { .build() .containsDataset()); - Assertions.assertFalse(TaskDescription.builder() + assertFalse(TaskDescription.builder() + .datasetAddress(EMPTY_ADDRESS) + .datasetUri(DATASET_URI) + .datasetName(DATASET_NAME) + .datasetChecksum(DATASET_CHECKSUM) + .build() + .containsDataset()); + + assertFalse(TaskDescription.builder() .datasetAddress(DATASET_ADDRESS) // .datasetUri(DATASET_URI) .datasetName(DATASET_NAME) @@ -271,7 +217,7 @@ void shouldNotContainDataset() { .build() .containsDataset()); - Assertions.assertFalse(TaskDescription.builder() + assertFalse(TaskDescription.builder() .datasetAddress(DATASET_ADDRESS) .datasetUri(DATASET_URI) .datasetName(DATASET_NAME) @@ -279,10 +225,12 @@ void shouldNotContainDataset() { .build() .containsDataset()); } + // endregion + // region containsCallback @Test void shouldContainCallback() { - Assertions.assertTrue(TaskDescription.builder() + assertTrue(TaskDescription.builder() .callback(CALLBACK) .build() .containsCallback()); @@ -290,19 +238,21 @@ void shouldContainCallback() { @Test void shouldNotContainCallback() { - Assertions.assertFalse(TaskDescription.builder() - .callback(BytesUtils.EMPTY_ADDRESS) + assertFalse(TaskDescription.builder() + .callback(EMPTY_ADDRESS) .build() .containsCallback()); - Assertions.assertFalse(TaskDescription.builder() + assertFalse(TaskDescription.builder() // .callback(CALLBACK) .build() .containsCallback()); } + // endregion + // region containsInputFiles @Test void shouldContainInputFiles() { - Assertions.assertTrue(TaskDescription.builder() + assertTrue(TaskDescription.builder() .chainTaskId(CHAIN_TASK_ID) .inputFiles(List.of("http://file1", "http://file2")) .build() @@ -310,14 +260,86 @@ void shouldContainInputFiles() { } @Test - void shouldNotContainInputFiles() { - Assertions.assertFalse(TaskDescription.builder() + void shouldContainsInputFilesFromDealParams() { + assertTrue(TaskDescription.builder() + .chainTaskId(CHAIN_TASK_ID) + .dealParams(DealParams.builder().iexecInputFiles(List.of("http://file1", "http://file2")).build()) + .build() + .containsInputFiles()); + } + + @ParameterizedTest + @NullSource + @MethodSource("provideTaskDescriptionWithoutInputFiles") + void shouldNotContainInputFilesWhenNullDealParams(final DealParams dealParams) { + assertFalse(TaskDescription.builder() + .chainTaskId(CHAIN_TASK_ID) + .dealParams(dealParams) + .build() + .containsInputFiles()); + } + + private static Stream provideTaskDescriptionWithoutInputFiles() { + return Stream.of( + Arguments.of(DealParams.builder().build()), + Arguments.of(DealParams.builder().iexecInputFiles(null).build()) + ); + } + + @Test + void shouldNotContainInputFilesWhenEmptyInputFiles() { + assertFalse(TaskDescription.builder() .chainTaskId(CHAIN_TASK_ID) - // .inputFiles(List.of("http://file1", "http://file2")) + .inputFiles(List.of()) .build() .containsInputFiles()); } + @Test + void shouldNotContainInputFilesWhenNullInputFiles() { + assertFalse(TaskDescription.builder() + .chainTaskId(CHAIN_TASK_ID) + .inputFiles(null) + .build() + .containsInputFiles()); + } + // endregion + + // region getAppCommand + @Test + void shouldGenerateAppCommandWithEntrypointWhenEmptyDealParams() { + final TaskDescription taskDescription = TaskDescription.builder() + .appEnclaveConfiguration(TeeEnclaveConfiguration.builder().entrypoint(ENTRYPOINT).build()) + .dealParams(DealParams.builder().build()) + .build(); + assertEquals(ENTRYPOINT, taskDescription.getAppCommand()); + } + + @Test + void shouldGenerateAppCommandWithEntrypointWhenNullDealParams() { + final TaskDescription taskDescription = TaskDescription.builder() + .appEnclaveConfiguration(TeeEnclaveConfiguration.builder().entrypoint(ENTRYPOINT).build()) + .dealParams(null) + .build(); + assertEquals(ENTRYPOINT, taskDescription.getAppCommand()); + } + + @Test + void shouldGenerateAppCommandWithEntrypointAndArgs() { + assertEquals(ENTRYPOINT + " " + CMD, TaskDescription.builder() + .appEnclaveConfiguration(TeeEnclaveConfiguration.builder().entrypoint(ENTRYPOINT).build()) + .dealParams(DealParams.builder().iexecArgs(CMD).build()) + .build() + .getAppCommand()); + assertEquals(ENTRYPOINT + " " + CMD, TaskDescription.builder() + .appEnclaveConfiguration(TeeEnclaveConfiguration.builder().entrypoint(ENTRYPOINT).build()) + .dealParams(DealParams.builder().build()) + .cmd(CMD) + .build() + .getAppCommand()); + } + // endregion + // region isEligibleToContributeAndFinalize @Test void shouldBeEligibleToContributeAndFinalize() { @@ -327,7 +349,7 @@ void shouldBeEligibleToContributeAndFinalize() { .callback("") .build(); - Assertions.assertTrue(taskDescription.isEligibleToContributeAndFinalize()); + assertTrue(taskDescription.isEligibleToContributeAndFinalize()); } @Test @@ -338,7 +360,7 @@ void shouldNotBeEligibleToContributeAndFinalizeSinceNotTee() { .callback("") .build(); - Assertions.assertFalse(taskDescription.isEligibleToContributeAndFinalize()); + assertFalse(taskDescription.isEligibleToContributeAndFinalize()); } @Test @@ -349,7 +371,7 @@ void shouldNotBeEligibleToContributeAndFinalizeSinceWrongTrust() { .callback("") .build(); - Assertions.assertFalse(taskDescription.isEligibleToContributeAndFinalize()); + assertFalse(taskDescription.isEligibleToContributeAndFinalize()); } @Test @@ -360,7 +382,7 @@ void shouldNotBeEligibleToContributeAndFinalizeSinceCallback() { .callback(CALLBACK) .build(); - Assertions.assertFalse(taskDescription.isEligibleToContributeAndFinalize()); + assertFalse(taskDescription.isEligibleToContributeAndFinalize()); } // endregion } From 90287ebfcbf09f36fed503c2397586fcadb048e8 Mon Sep 17 00:00:00 2001 From: nabil-Tounarti <117689544+nabil-Tounarti@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:42:52 +0100 Subject: [PATCH 07/10] Make TEE tasks with callback eligible to contributeAndFinalize flow (#109) --- CHANGELOG.md | 1 + .../commons/poco/task/TaskDescription.java | 4 +--- .../poco/task/TaskDescriptionTests.java | 19 +++++-------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21abdd4..fbecfe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### New Features - Add `getUserGasPrice` method to `Web3jAbstractService`. (#104) +- Make **TEE tasks** with callback eligible to `contributeAndFinalize` flow. (#109) ### Bug Fixes diff --git a/src/main/java/com/iexec/commons/poco/task/TaskDescription.java b/src/main/java/com/iexec/commons/poco/task/TaskDescription.java index c0e1606..a0028ec 100644 --- a/src/main/java/com/iexec/commons/poco/task/TaskDescription.java +++ b/src/main/java/com/iexec/commons/poco/task/TaskDescription.java @@ -161,9 +161,7 @@ public String getAppCommand() { * @return {@literal true} if eligible, {@literal false} otherwise. */ public boolean isEligibleToContributeAndFinalize() { - return isTeeTask - && BigInteger.ONE.equals(trust) - && !containsCallback(); + return isTeeTask && BigInteger.ONE.equals(trust); } /** diff --git a/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java b/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java index 5a32525..f9c0e34 100644 --- a/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java +++ b/src/test/java/com/iexec/commons/poco/task/TaskDescriptionTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import java.math.BigInteger; import java.nio.charset.StandardCharsets; @@ -341,12 +342,13 @@ void shouldGenerateAppCommandWithEntrypointAndArgs() { // endregion // region isEligibleToContributeAndFinalize - @Test - void shouldBeEligibleToContributeAndFinalize() { + @ParameterizedTest + @ValueSource(strings = {"", CALLBACK}) + void shouldBeEligibleToContributeAndFinalize(final String callback) { final TaskDescription taskDescription = TaskDescription.builder() .isTeeTask(true) .trust(BigInteger.ONE) - .callback("") + .callback(callback) .build(); assertTrue(taskDescription.isEligibleToContributeAndFinalize()); @@ -373,16 +375,5 @@ void shouldNotBeEligibleToContributeAndFinalizeSinceWrongTrust() { assertFalse(taskDescription.isEligibleToContributeAndFinalize()); } - - @Test - void shouldNotBeEligibleToContributeAndFinalizeSinceCallback() { - final TaskDescription taskDescription = TaskDescription.builder() - .isTeeTask(true) - .trust(BigInteger.ONE) - .callback(CALLBACK) - .build(); - - assertFalse(taskDescription.isEligibleToContributeAndFinalize()); - } // endregion } From 0976d590444b98c4b644de111954a0e3ed0ddab5 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Mon, 2 Dec 2024 14:39:39 +0100 Subject: [PATCH 08/10] Upgrade to testcontainers 1.20.4 (#110) --- CHANGELOG.md | 1 + build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbecfe6..29ba909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. ### Dependency Upgrades - Upgrade to Gradle 8.10.2. (#105) +- Upgrade to `testcontainers` 1.20.4. (#110) ## [[4.1.0]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/v4.1.0) 2024-06-17 diff --git a/build.gradle b/build.gradle index 41a7983..a72f7a3 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { group = 'com.iexec.commons' ext { - testContainersVersion = '1.19.3' + testContainersVersion = '1.20.4' } if (!project.hasProperty('gitBranch')) { From 06ee735b9d37abc4cb10af5a2758eea903f98afb Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Thu, 12 Dec 2024 15:24:43 +0100 Subject: [PATCH 09/10] Add accessors to read on-chain deployed PoCo Smart Contracts configurations (#111) --- CHANGELOG.md | 2 + .../poco/chain/IexecHubAbstractService.java | 45 ++++++++++++++++- .../poco/encoding/AccessorsEncoder.java | 48 +++++++++++++++++++ .../iexec/commons/poco/itest/ChainTests.java | 18 +++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/iexec/commons/poco/encoding/AccessorsEncoder.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ba909..e710d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. - Add `getUserGasPrice` method to `Web3jAbstractService`. (#104) - Make **TEE tasks** with callback eligible to `contributeAndFinalize` flow. (#109) +- Add accessors to read on-chain deployed PoCo Smart Contracts configurations: + `callbackgas`, `contribution_deadline_ratio` and `final_deadline_ratio`. (#111) ### Bug Fixes diff --git a/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java b/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java index c5d6b0b..f087431 100644 --- a/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java +++ b/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java @@ -26,13 +26,16 @@ import org.awaitility.core.ConditionTimeoutException; import org.web3j.crypto.Credentials; import org.web3j.ens.EnsResolutionException; +import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.RemoteCall; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.tuples.generated.Tuple3; import org.web3j.tx.RawTransactionManager; import org.web3j.tx.gas.ContractGasProvider; import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; +import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -41,6 +44,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import static com.iexec.commons.poco.encoding.AccessorsEncoder.*; import static com.iexec.commons.poco.encoding.AssetDataEncoder.getAssetAddressFromReceipt; import static com.iexec.commons.poco.tee.TeeEnclaveConfiguration.buildEnclaveConfigurationFromJsonString; import static com.iexec.commons.poco.utils.BytesUtils.isNonZeroedBytes32; @@ -889,7 +893,7 @@ public long getMaxNbOfPeriodsForConsensus() { private void setMaxNbOfPeriodsForConsensus() { try { - maxNbOfPeriodsForConsensus = iexecHubContract.contribution_deadline_ratio().send().longValue(); + maxNbOfPeriodsForConsensus = getContributionDeadlineRatio().longValue(); } catch (Exception e) { log.error("Failed to get maxNbOfPeriodsForConsensus from the chain", e); maxNbOfPeriodsForConsensus = -1; @@ -951,6 +955,45 @@ public boolean isTeeTask(String chainTaskId) { return taskDescription.isTeeTask(); } + // region accessors + + /** + * Send call to callbackgas() PoCo method. + * + * @return callbackgas value + * @throws IOException if communication fails + */ + public BigInteger getCallbackGas() throws IOException { + return sendCallWithFunctionSelector(CALLBACKGAS_SELECTOR); + } + + /** + * Send call to contribution_deadline_ratio() PoCo method. + * + * @return contribution_deadline_ratio value + * @throws IOException if communication fails + */ + public BigInteger getContributionDeadlineRatio() throws IOException { + return sendCallWithFunctionSelector(CONTRIBUTION_DEADLINE_RATIO_SELECTOR); + } + + /** + * Send call to final_deadline_ratio() PoCo method. + * + * @return final_deadline_ratio value + * @throws IOException if communication fails + */ + public BigInteger getFinalDeadlineRatio() throws IOException { + return sendCallWithFunctionSelector(FINAL_DEADLINE_RATIO_SELECTOR); + } + + private BigInteger sendCallWithFunctionSelector(final String functionSelector) throws IOException { + return Numeric.toBigInt( + txManager.sendCall(iexecHubAddress, functionSelector, DefaultBlockParameterName.LATEST)); + } + + // endregion + // region Purge /** diff --git a/src/main/java/com/iexec/commons/poco/encoding/AccessorsEncoder.java b/src/main/java/com/iexec/commons/poco/encoding/AccessorsEncoder.java new file mode 100644 index 0000000..22205de --- /dev/null +++ b/src/main/java/com/iexec/commons/poco/encoding/AccessorsEncoder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.commons.poco.encoding; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * Class containing Ethereum function selectors to read PoCo Smart Contracts configurations. + *

+ * Current accessors allow to read callback gas, contribution deadline ratio and final deadline ratio. + * + * @see PoCo accessors + * @see Ethereum Contract ABI Specification + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AccessorsEncoder { + + /** + * keccak256(callbackgas()) + */ + public static final String CALLBACKGAS_SELECTOR = "0xe63ec07d"; + + /** + * keccak256(contribution_deadline_ratio()) + */ + public static final String CONTRIBUTION_DEADLINE_RATIO_SELECTOR = "0x74ed5244"; + + /** + * keccak256(final_deadline_ratio()) + */ + public static final String FINAL_DEADLINE_RATIO_SELECTOR = "0xdb8aaa26"; + +} diff --git a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java index e8ceaec..dd709d7 100644 --- a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java @@ -104,6 +104,24 @@ void shouldNotGetBlockNumber() { assertThat(badWeb3jService.getLatestBlockNumber()).isZero(); } + @Test + void shouldGetCallbackGas() throws IOException { + final BigInteger callbackGas = iexecHubService.getCallbackGas(); + assertThat(callbackGas).isEqualTo(BigInteger.valueOf(200_000)); + } + + @Test + void shouldGetContributionDeadlineRatio() throws IOException { + final BigInteger contributionDeadlineRatio = iexecHubService.getContributionDeadlineRatio(); + assertThat(contributionDeadlineRatio).isEqualTo(BigInteger.valueOf(7)); + } + + @Test + void shouldGetFinalDeadlineRatio() throws IOException { + final BigInteger finalDeadlineRatio = iexecHubService.getFinalDeadlineRatio(); + assertThat(finalDeadlineRatio).isEqualTo(BigInteger.valueOf(10)); + } + @ParameterizedTest @MethodSource("categoryProvider") void shouldGetCategory(long id, ChainCategory expectedCategory) { From fd0e09b1333812a649cd9f78355fffa53995db2c Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Wed, 18 Dec 2024 10:26:49 +0100 Subject: [PATCH 10/10] Release 4.2.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e710d99..efc9b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [[NEXT]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/vNEXT) 2024 +## [[4.2.0]](https://github.com/iExecBlockchainComputing/iexec-commons-poco/releases/tag/v4.2.0) 2024-12-18 ### New Features diff --git a/gradle.properties b/gradle.properties index c5fecb3..a381b0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=4.1.0 +version=4.2.0 nexusUser nexusPassword