diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d732a6bf29..322e3942c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -179,7 +179,7 @@ jobs: uses: hiero-ledger/hiero-solo-action@dd0048139ef1e40fd6067f01bf94eb42a67294f4 # v0.15 with: installMirrorNode: true - hieroVersion: v0.69.0-alpha.1 + hieroVersion: v0.70 - name: Build SDK run: ./gradlew assemble @@ -302,7 +302,7 @@ jobs: uses: hiero-ledger/hiero-solo-action@dd0048139ef1e40fd6067f01bf94eb42a67294f4 # v0.15 with: installMirrorNode: true - hieroVersion: v0.69.0-alpha.1 + hieroVersion: v0.70 - name: Build SDK run: ./gradlew assemble diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransaction.java index df0a53fb0e..d7c0054fac 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransaction.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransaction.java @@ -59,7 +59,7 @@ public byte[] getEthereumData() { } /** - * Sets the raw Ethereum transaction (RLP encoded type 0, 1, and 2). Complete + * Sets the raw Ethereum transaction (RLP encoded type 0, 1, 2 and 4). Complete * unless the callDataFileId is set. * * @param ethereumData raw ethereum transaction bytes diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionData.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionData.java index cec713e552..9d4ea45c2b 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionData.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionData.java @@ -24,7 +24,13 @@ static EthereumTransactionData fromBytes(byte[] bytes) { if (rlpItem.isList()) { return EthereumTransactionDataLegacy.fromBytes(bytes); } else { - return EthereumTransactionDataEip1559.fromBytes(bytes); + var typeByte = rlpItem.asByte(); + + return switch (typeByte) { + case 0x02 -> EthereumTransactionDataEip1559.fromBytes(bytes); + case 0x04 -> EthereumTransactionDataEip7702.fromBytes(bytes); + default -> throw new IllegalArgumentException("rlp type byte " + typeByte + "is not supported"); + }; } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702.java new file mode 100644 index 0000000000..0949f5146b --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702.java @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import com.esaulpaugh.headlong.rlp.RLPDecoder; +import com.esaulpaugh.headlong.rlp.RLPEncoder; +import com.esaulpaugh.headlong.rlp.RLPItem; +import com.esaulpaugh.headlong.util.Integers; +import com.google.common.base.MoreObjects; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.bouncycastle.util.encoders.Hex; + +/** + * The ethereum transaction data, in the format defined in + * EIP-7702 + */ +public class EthereumTransactionDataEip7702 extends EthereumTransactionData { + + /** + * ID of the chain. + */ + public byte[] chainId; + + /** + * Transaction's nonce. + */ + public byte[] nonce; + + /** + * An 'optional' additional fee in Ethereum that is paid directly to miners in order to incentivize them to include + * your transaction in a block. Not used in Hedera. + */ + public byte[] maxPriorityGas; + + /** + * The maximum amount, in tinybars, that the payer of the hedera transaction is willing to pay to complete the + * transaction. + */ + public byte[] maxGas; + + /** + * The amount of gas available for the transaction. + */ + public byte[] gasLimit; + + /** + * The receiver of the transaction. + */ + public byte[] to; + + /** + * The transaction value. + */ + public byte[] value; + + /** + * Specifies an array of addresses and storage keys that the transaction plans to access. + */ + public List accessList; + + /** + * The list of delegation authorizations. + */ + public List authorizationList; + + /** + * Recovery parameter used to ease the signature verification. + */ + public byte[] recoveryId; + + /** + * The R value of the signature. + */ + public byte[] r; + + /** + * The S value of the signature. + */ + public byte[] s; + + EthereumTransactionDataEip7702( + HeaderData headerData, + byte[] callData, + List accessList, + List authorizationList, + SignatureData signatureData) { + super(callData); + + this.chainId = headerData.chainId(); + this.nonce = headerData.nonce(); + this.maxPriorityGas = headerData.maxPriorityGas(); + this.maxGas = headerData.maxGas(); + this.gasLimit = headerData.gasLimit(); + this.to = headerData.to(); + this.value = headerData.value(); + this.accessList = accessList; + this.authorizationList = authorizationList; + this.recoveryId = signatureData.recoveryId(); + this.r = signatureData.r(); + this.s = signatureData.s(); + } + + /** + * Convert a byte array to an ethereum transaction data. + * + * @param bytes the byte array + * @return the ethereum transaction data + */ + public static EthereumTransactionDataEip7702 fromBytes(byte[] bytes) { + var decoder = RLPDecoder.RLP_STRICT.sequenceIterator(bytes); + var rlpItem = decoder.next(); + + // typed transaction? + byte typeByte = rlpItem.asByte(); + if (typeByte != 4) { + throw new IllegalArgumentException("rlp type byte " + typeByte + " is not supported"); + } + rlpItem = decoder.next(); + if (!rlpItem.isList()) { + throw new IllegalArgumentException("expected RLP element list"); + } + List rlpList = rlpItem.asRLPList().elements(); + if (rlpList.size() != 13) { + throw new IllegalArgumentException("expected 13 RLP encoded elements, found " + rlpList.size()); + } + + var accessList = new ArrayList(); + for (var accessListItem : rlpList.get(8).asRLPList().elements()) { + accessList.add(accessListItem.data()); + } + + var authorizationList = new ArrayList(); + for (var authorizationTuple : rlpList.get(9).asRLPList().elements()) { + var tupleElements = authorizationTuple.asRLPList().elements(); + if (tupleElements.size() != 6) { + throw new IllegalArgumentException("invalid authorization list entry: must have 6 elements"); + } + authorizationList.add(new AuthorizationTuple( + tupleElements.get(0).data(), + tupleElements.get(1).data(), + tupleElements.get(2).data(), + tupleElements.get(3).data(), + tupleElements.get(4).data(), + tupleElements.get(5).data())); + } + + var headerData = new HeaderData( + rlpList.get(0).data(), + rlpList.get(1).data(), + rlpList.get(2).data(), + rlpList.get(3).data(), + rlpList.get(4).data(), + rlpList.get(5).data(), + rlpList.get(6).data()); + + var signatureData = new SignatureData( + rlpList.get(10).data(), rlpList.get(11).data(), rlpList.get(12).data()); + + return new EthereumTransactionDataEip7702( + headerData, rlpList.get(7).data(), accessList, authorizationList, signatureData); + } + + public byte[] toBytes() { + List encodedAuthorizationList = new ArrayList<>(); + for (var tuple : authorizationList) { + encodedAuthorizationList.add( + List.of(tuple.chainId(), tuple.address(), tuple.nonce(), tuple.yParity(), tuple.r(), tuple.s())); + } + + List encodedAccessList = new ArrayList<>(accessList); + + return RLPEncoder.sequence( + Integers.toBytes(0x04), + List.of( + chainId, + nonce, + maxPriorityGas, + maxGas, + gasLimit, + to, + value, + callData, + encodedAccessList, + encodedAuthorizationList, + recoveryId, + r, + s)); + } + + public String toString() { + return MoreObjects.toStringHelper(this) + .add("chainId", Hex.toHexString(chainId)) + .add("nonce", Hex.toHexString(nonce)) + .add("maxPriorityGas", Hex.toHexString(maxPriorityGas)) + .add("maxGas", Hex.toHexString(maxGas)) + .add("gasLimit", Hex.toHexString(gasLimit)) + .add("to", Hex.toHexString(to)) + .add("value", Hex.toHexString(value)) + .add("callData", Hex.toHexString(callData)) + .add("accessList", accessList.stream().map(Hex::toHexString).collect(Collectors.toList())) + .add( + "authorizationList", + authorizationList.stream() + .map(AuthorizationTuple::toString) + .collect(Collectors.toList())) + .add("recoveryId", Hex.toHexString(recoveryId)) + .add("r", Hex.toHexString(r)) + .add("s", Hex.toHexString(s)) + .toString(); + } + + /** + * A helper record to hold core transaction fields for EIP-7702 transactions. + */ + record HeaderData( + byte[] chainId, + byte[] nonce, + byte[] maxPriorityGas, + byte[] maxGas, + byte[] gasLimit, + byte[] to, + byte[] value) {} + + /** + * A helper record to hold signature data for EIP-7702 transactions. + */ + record SignatureData(byte[] recoveryId, byte[] r, byte[] s) {} + + /** + * A tuple describing an authorization entry for EIP-7702 transactions. + */ + public record AuthorizationTuple(byte[] chainId, byte[] address, byte[] nonce, byte[] yParity, byte[] r, byte[] s) { + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("chainId", Hex.toHexString(chainId)) + .add("address", Hex.toHexString(address)) + .add("nonce", Hex.toHexString(nonce)) + .add("yParity", Hex.toHexString(yParity)) + .add("r", Hex.toHexString(r)) + .add("s", Hex.toHexString(s)) + .toString(); + } + } +} diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702Test.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702Test.java new file mode 100644 index 0000000000..9dfcd8b483 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/EthereumTransactionDataEip7702Test.java @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +package com.hedera.hashgraph.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.primitives.Bytes; +import com.hedera.hashgraph.sdk.EthereumTransactionDataEip7702.AuthorizationTuple; +import java.util.List; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +public class EthereumTransactionDataEip7702Test { + + @Test + public void eip7702ToFromBytes() { + var authorizationTuple = new AuthorizationTuple( + Hex.decode("012a"), + Hex.decode("0102030405060708090a0b0c0d0e0f1011121314"), + Hex.decode("01"), + Hex.decode("00"), + Hex.decode("11"), + Hex.decode("12")); + + var headerData = new EthereumTransactionDataEip7702.HeaderData( + Hex.decode("012a"), + Hex.decode("02"), + Hex.decode("2f"), + Hex.decode("2f"), + Hex.decode("018000"), + Hex.decode("7e3a9eaf9bcc39e2ffa38eb30bf7a93feacbc181"), + Hex.decode("0de0b6b3a7640000")); + + var signatureData = new EthereumTransactionDataEip7702.SignatureData( + Hex.decode("01"), + Hex.decode("df48f2efd10421811de2bfb125ab75b2d3c44139c4642837fb1fccce911fd479"), + Hex.decode("1aaf7ae92bee896651dfc9d99ae422a296bf5d9f1ca49b2d96d82b79eb112d66")); + + var data = new EthereumTransactionDataEip7702( + headerData, Hex.decode("123456"), List.of(), List.of(authorizationTuple), signatureData); + + var encodedHex = Hex.toHexString(data.toBytes()); + var decoded = (EthereumTransactionDataEip7702) EthereumTransactionData.fromBytes(Hex.decode(encodedHex)); + + assertThat(encodedHex).isEqualTo(Hex.toHexString(decoded.toBytes())); + assertThat(Hex.toHexString(decoded.chainId)).isEqualTo("012a"); + assertThat(Hex.toHexString(decoded.nonce)).isEqualTo("02"); + assertThat(Hex.toHexString(decoded.maxPriorityGas)).isEqualTo("2f"); + assertThat(Hex.toHexString(decoded.maxGas)).isEqualTo("2f"); + assertThat(Hex.toHexString(decoded.gasLimit)).isEqualTo("018000"); + assertThat(Hex.toHexString(decoded.to)).isEqualTo("7e3a9eaf9bcc39e2ffa38eb30bf7a93feacbc181"); + assertThat(Hex.toHexString(decoded.value)).isEqualTo("0de0b6b3a7640000"); + assertThat(Hex.toHexString(decoded.callData)).isEqualTo("123456"); + assertThat(decoded.accessList).isEmpty(); + assertThat(decoded.authorizationList).hasSize(1); + + var decodedAuth = decoded.authorizationList.get(0); + assertThat(Hex.toHexString(decodedAuth.chainId())).isEqualTo("012a"); + assertThat(Hex.toHexString(decodedAuth.address())).isEqualTo("0102030405060708090a0b0c0d0e0f1011121314"); + assertThat(Hex.toHexString(decodedAuth.nonce())).isEqualTo("01"); + assertThat(Hex.toHexString(decodedAuth.yParity())).isEqualTo("00"); + assertThat(Hex.toHexString(decodedAuth.r())).isEqualTo("11"); + assertThat(Hex.toHexString(decodedAuth.s())).isEqualTo("12"); + + assertThat(Hex.toHexString(decoded.recoveryId)).isEqualTo("01"); + assertThat(Hex.toHexString(decoded.r)) + .isEqualTo("df48f2efd10421811de2bfb125ab75b2d3c44139c4642837fb1fccce911fd479"); + assertThat(Hex.toHexString(decoded.s)) + .isEqualTo("1aaf7ae92bee896651dfc9d99ae422a296bf5d9f1ca49b2d96d82b79eb112d66"); + } + + @Test + void manualEncodingMatchesHeadlongSequence() { + var authorizationTuple = new AuthorizationTuple( + Hex.decode("012a"), + Hex.decode("0102030405060708090a0b0c0d0e0f1011121314"), + Hex.decode("01"), + Hex.decode("00"), + Hex.decode("11"), + Hex.decode("12")); + + var headerData = new EthereumTransactionDataEip7702.HeaderData( + Hex.decode("012a"), + Hex.decode("00"), + Hex.decode("00"), + Hex.decode("d1385c7bf0"), + Hex.decode("07a120"), + Hex.decode("7e3a9eaf9bcc39e2ffa38eb30bf7a93feacbc181"), + Hex.decode("00")); + + var signatureData = new EthereumTransactionDataEip7702.SignatureData( + Hex.decode("01"), + Hex.decode("df48f2efd10421811de2bfb125ab75b2d3c44139c4642837fb1fccce911fd479"), + Hex.decode("1aaf7ae92bee896651dfc9d99ae422a296bf5d9f1ca49b2d96d82b79eb112d66")); + + var data = new EthereumTransactionDataEip7702( + headerData, Hex.decode("123456"), List.of(), List.of(authorizationTuple), signatureData); + + var manualPayload = com.esaulpaugh.headlong.rlp.RLPEncoder.list( + data.chainId, + data.nonce, + data.maxPriorityGas, + data.maxGas, + data.gasLimit, + data.to, + data.value, + data.callData, + List.of(), + List.of(List.of( + authorizationTuple.chainId(), + authorizationTuple.address(), + authorizationTuple.nonce(), + authorizationTuple.yParity(), + authorizationTuple.r(), + authorizationTuple.s())), + data.recoveryId, + data.r, + data.s); + + var manualBytes = Bytes.concat(new byte[] {0x04}, manualPayload); + + assertThat(data.toBytes()).containsExactly(manualBytes); + assertThat(EthereumTransactionDataEip7702.fromBytes(manualBytes)).isNotNull(); + assertThat(EthereumTransactionDataEip7702.fromBytes(manualBytes).toBytes()) + .containsExactly(manualBytes); + } + + @Test + void toBytesAndFromBytesPreserveAllFieldValues() { + var original = createEip7702Data(); + + var bytes = original.toBytes(); + var decoded = EthereumTransactionDataEip7702.fromBytes(bytes); + + assertThat(decoded.chainId).containsExactly(original.chainId); + assertThat(decoded.nonce).containsExactly(original.nonce); + assertThat(decoded.maxPriorityGas).containsExactly(original.maxPriorityGas); + assertThat(decoded.maxGas).containsExactly(original.maxGas); + assertThat(decoded.gasLimit).containsExactly(original.gasLimit); + assertThat(decoded.to).containsExactly(original.to); + assertThat(decoded.value).containsExactly(original.value); + assertThat(decoded.callData).containsExactly(original.callData); + assertThat(decoded.recoveryId).containsExactly(original.recoveryId); + assertThat(decoded.r).containsExactly(original.r); + assertThat(decoded.s).containsExactly(original.s); + + assertThat(decoded.authorizationList).hasSize(1); + + var originalAuth = original.authorizationList.get(0); + var decodedAuth = decoded.authorizationList.get(0); + assertThat(decodedAuth.chainId()).containsExactly(originalAuth.chainId()); + assertThat(decodedAuth.address()).containsExactly(originalAuth.address()); + assertThat(decodedAuth.nonce()).containsExactly(originalAuth.nonce()); + assertThat(decodedAuth.yParity()).containsExactly(originalAuth.yParity()); + assertThat(decodedAuth.r()).containsExactly(originalAuth.r()); + assertThat(decodedAuth.s()).containsExactly(originalAuth.s()); + + assertThat(decoded.toBytes()).containsExactly(bytes); + } + + private EthereumTransactionDataEip7702 createEip7702Data() { + var authData = new EthereumTransactionDataEip7702.AuthorizationTuple( + Hex.decode("012a"), + Hex.decode("00000000000000000000000000000000000003f9"), + Hex.decode("00"), + Hex.decode("01"), + Hex.decode("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), + Hex.decode("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")); + + var headerData = new EthereumTransactionDataEip7702.HeaderData( + Hex.decode("012a"), + Hex.decode("00"), + Hex.decode("01"), + Hex.decode("d1385c7bf0"), + Hex.decode("07A120"), + Hex.decode("00000000000000000000000000000000000003f9"), + new byte[0]); + + var signatureData = new EthereumTransactionDataEip7702.SignatureData( + Hex.decode("01"), + Hex.decode("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"), + Hex.decode("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")); + + return new EthereumTransactionDataEip7702( + headerData, Hex.decode("123456"), List.of(), List.of(authData), signatureData); + } +} diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/EthereumTransactionIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/EthereumTransactionIntegrationTest.java index 77a56cc2ab..6d8cf9f130 100644 --- a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/EthereumTransactionIntegrationTest.java +++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/EthereumTransactionIntegrationTest.java @@ -9,14 +9,17 @@ import com.hedera.hashgraph.sdk.ContractDeleteTransaction; import com.hedera.hashgraph.sdk.ContractExecuteTransaction; import com.hedera.hashgraph.sdk.ContractFunctionParameters; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.EthereumTransaction; import com.hedera.hashgraph.sdk.FileCreateTransaction; import com.hedera.hashgraph.sdk.FileDeleteTransaction; +import com.hedera.hashgraph.sdk.FileId; import com.hedera.hashgraph.sdk.Hbar; import com.hedera.hashgraph.sdk.PrivateKey; import com.hedera.hashgraph.sdk.PrivateKeyECDSA; import com.hedera.hashgraph.sdk.TransferTransaction; import java.math.BigInteger; +import java.util.Arrays; import java.util.List; import java.util.Objects; import org.bouncycastle.util.encoders.Hex; @@ -26,11 +29,13 @@ public class EthereumTransactionIntegrationTest { private static final String SMART_CONTRACT_BYTECODE = - "608060405234801561001057600080fd5b506040516104d73803806104d78339818101604052602081101561003357600080fd5b810190808051604051939291908464010000000082111561005357600080fd5b90830190602082018581111561006857600080fd5b825164010000000081118282018810171561008257600080fd5b82525081516020918201929091019080838360005b838110156100af578181015183820152602001610097565b50505050905090810190601f1680156100dc5780820380516001836020036101000a031916815260200191505b506040525050600080546001600160a01b0319163317905550805161010890600190602084019061010f565b50506101aa565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061015057805160ff191683800117855561017d565b8280016001018555821561017d579182015b8281111561017d578251825591602001919060010190610162565b5061018992915061018d565b5090565b6101a791905b808211156101895760008155600101610193565b90565b61031e806101b96000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063368b87721461004657806341c0e1b5146100ee578063ce6d41de146100f6575b600080fd5b6100ec6004803603602081101561005c57600080fd5b81019060208101813564010000000081111561007757600080fd5b82018360208201111561008957600080fd5b803590602001918460018302840111640100000000831117156100ab57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610173945050505050565b005b6100ec6101a2565b6100fe6101ba565b6040805160208082528351818301528351919283929083019185019080838360005b83811015610138578181015183820152602001610120565b50505050905090810190601f1680156101655780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000546001600160a01b0316331461018a5761019f565b805161019d906001906020840190610250565b505b50565b6000546001600160a01b03163314156101b85733ff5b565b60018054604080516020601f600260001961010087891615020190951694909404938401819004810282018101909252828152606093909290918301828280156102455780601f1061021a57610100808354040283529160200191610245565b820191906000526020600020905b81548152906001019060200180831161022857829003601f168201915b505050505090505b90565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061029157805160ff19168380011785556102be565b828001600101855582156102be579182015b828111156102be5782518255916020019190600101906102a3565b506102ca9291506102ce565b5090565b61024d91905b808211156102ca57600081556001016102d456fea264697066735822122084964d4c3f6bc912a9d20e14e449721012d625aa3c8a12de41ae5519752fc89064736f6c63430006000033"; + "608060405234801561001057600080fd5b506040516104d73803806104d78339818101604052602081101561003357600080fd5b810190808051604051939291908464010000000082111561005357600080fd5b90830190602082018581111561006857600080fd5b825164010000000081118282018810171561008257600080fd5b82525081516020918201929091019080838360005b838110156100af578181015183820152602001610097565b50505050905090810190601f1680156100dc5780820380516001836020036101000a031916815260200191505b506040525050600080546001600160a01b0319163317905550805161010890600190602084019061010f565b50506101aa565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061015057805160ff191683800117855561017d565b8280016001018555821561017d579182015b828111156102be578251825591602001919060010190610162565b5061018992915061018d565b5090565b6101a791905b808211156101895760008155600101610193565b90565b61031e806101b96000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063368b87721461004657806341c0e1b5146100ee578063ce6d41de146100f6575b600080fd5b6100ec6004803603602081101561005c57600080fd5b81019060208101813564010000000081111561007757600080fd5b82018360208201111561008957600080fd5b803590602001918460018302840111640100000000831117156100ab57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610173945050505050565b005b6100ec6101a2565b6100fe6101ba565b6040805160208082528351818301528351919283929083019185019080838360005b83811015610138578181015183820152602001610120565b50505050905090810190601f1680156101655780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000546001600160a01b0316331461018a5761019f565b805161019d906001906020840190610250565b505b50565b6000546001600160a01b03163314156101b85733ff5b565b60018054604080516020601f600260001961010087891615020190951694909404938401819004810282018101909252828152606093909290918301828280156102455780601f1061021a57610100808354040283529160200191610245565b820191906000526020600020905b81548152906001019060200180831161022857829003601f168201915b505050505090505b90565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061029157805160ff19168380011785556102be565b828001600101855582156102be579182015b828111156102be5782518255916020019190600101906102a3565b506102ca9291506102ce565b5090565b61024d91905b808211156102ca57600081556001016102d456fea264697066735822122084964d4c3f6bc912a9d20e14e449721012d625aa3c8a12de41ae5519752fc89064736f6c63430006000033"; private static final String SMART_CONTRACT_BYTECODE_JUMBO = "6080604052348015600e575f5ffd5b506101828061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80631e0a3f051461002d575b5f5ffd5b610047600480360381019061004291906100d0565b61005d565b6040516100549190610133565b60405180910390f35b5f5f905092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126100905761008f61006f565b5b8235905067ffffffffffffffff8111156100ad576100ac610073565b5b6020830191508360018202830111156100c9576100c8610077565b5b9250929050565b5f5f602083850312156100e6576100e5610067565b5b5f83013567ffffffffffffffff8111156101035761010261006b565b5b61010f8582860161007b565b92509250509250929050565b5f819050919050565b61012d8161011b565b82525050565b5f6020820190506101465f830184610124565b9291505056fea26469706673582212202829ebd1cf38c443e4fd3770cd4306ac4c6bb9ac2828074ae2b9cd16121fcfea64736f6c634300081e0033"; + private static final String EMPTY_CONTRACT_BYTECODE = + "608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063f8a8fd6d14602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b6000811515905091905056fea2646970667358221220b4a7b9f1eedd2080ba6dc510555bb650f1ab8aa6ee958ba753ad2cd1665559bd64736f6c63430008000033"; /** * @notice E2E-HIP-844 * @url https://hips.hedera.com/hip/hip-844 @@ -237,4 +242,179 @@ void jumboEthereumTransactionWithLargeCalldata() throws Exception { .getReceipt(testEnv.client); } } + + @Test + @DisplayName("EIP-7702 Ethereum transaction with authorization list") + void eip7702EthereumTransactionWithAuthorizationList() throws Exception { + try (var testEnv = new IntegrationTestEnv(1)) { + var privateKey = PrivateKey.generateECDSA(); + var newAccountAliasId = privateKey.toAccountId(0, 0); + + new TransferTransaction() + .addHbarTransfer(testEnv.operatorId, new Hbar(10).negated()) + .addHbarTransfer(newAccountAliasId, new Hbar(10)) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + var createdEntities = createContract(testEnv); + var fileId = createdEntities.fileId; + var contractId = createdEntities.contractId; + + var params = new EthereumTransactionParams( + Hex.decode("012a"), + 0, + Hex.decode("00"), + Hex.decode("d1385c7bf0"), + Hex.decode("07A120"), + Hex.decode(contractId.toEvmAddress()), + Integers.toBytesUnsigned(BigInteger.ONE), + new ContractExecuteTransaction() + .setFunction("test", new ContractFunctionParameters()) + .getFunctionParameters() + .toByteArray()); + + List encodedAuthorizationList = + createSignedAuthorizationList(privateKey, params.chainId, params.to, params.nonce); + + byte[] ethereumData = createSignedEthereumTransactionData(privateKey, params, encodedAuthorizationList); + + EthereumTransaction ethereumTransaction = new EthereumTransaction().setEthereumData(ethereumData); + var ethereumTransactionResponse = ethereumTransaction.execute(testEnv.client); + var ethereumTransactionRecord = ethereumTransactionResponse.getRecord(testEnv.client); + + assertThat(ethereumTransactionRecord.contractFunctionResult.signerNonce) + .isEqualTo(1); + + cleanup(testEnv, contractId, fileId); + } + } + + private ContractCreationResult createContract(IntegrationTestEnv testEnv) throws Exception { + var fileCreateTransactionResponse = new FileCreateTransaction() + .setKeys(testEnv.operatorKey) + .setContents(EMPTY_CONTRACT_BYTECODE) + .setMaxTransactionFee(new Hbar(2)) + .execute(testEnv.client); + + var fileId = Objects.requireNonNull(fileCreateTransactionResponse.getReceipt(testEnv.client).fileId); + + var contractCreateTransactionResponse = new ContractCreateTransaction() + .setAdminKey(testEnv.operatorKey) + .setGas(300000) + .setBytecodeFileId(fileId) + .setContractMemo("[e2e::ContractCreateTransaction]") + .execute(testEnv.client); + + var contractId = + Objects.requireNonNull(contractCreateTransactionResponse.getReceipt(testEnv.client).contractId); + + return new ContractCreationResult(fileId, contractId); + } + + private void cleanup(IntegrationTestEnv testEnv, ContractId contractId, FileId fileId) throws Exception { + new ContractDeleteTransaction() + .setTransferAccountId(testEnv.operatorId) + .setContractId(contractId) + .execute(testEnv.client) + .getReceipt(testEnv.client); + + new FileDeleteTransaction().setFileId(fileId).execute(testEnv.client).getReceipt(testEnv.client); + } + + private List createSignedAuthorizationList(PrivateKey privateKey, byte[] chainId, byte[] to, int nonce) { + byte[] authorizationNonce = Integers.toBytesUnsigned(BigInteger.valueOf(nonce)); + byte[] authorizationPayload = RLPEncoder.sequence(chainId, to, authorizationNonce); + byte[] authorizationMessage = new byte[1 + authorizationPayload.length]; + authorizationMessage[0] = 0x05; + System.arraycopy(authorizationPayload, 0, authorizationMessage, 1, authorizationPayload.length); + + byte[] authorizationSignature = privateKey.sign(authorizationMessage); + final byte[] authorizationR = Arrays.copyOfRange(authorizationSignature, 0, 32); + final byte[] authorizationS = Arrays.copyOfRange(authorizationSignature, 32, 64); + final int authorizationRecId = + ((PrivateKeyECDSA) privateKey).getRecoveryId(authorizationR, authorizationS, authorizationMessage); + + return List.of(List.of( + chainId, to, authorizationNonce, Integers.toBytes(authorizationRecId), authorizationR, authorizationS)); + } + + private byte[] createSignedEthereumTransactionData( + PrivateKey privateKey, EthereumTransactionParams params, List encodedAuthorizationList) { + + var sequence = RLPEncoder.sequence( + Integers.toBytes(4), + List.of( + params.chainId, + Integers.toBytes(params.nonce), + params.maxPriorityGas, + params.maxGas, + params.gasLimit, + params.to, + params.value, + params.callData, + List.of(), + encodedAuthorizationList)); + + byte[] signedBytes = privateKey.sign(sequence); + final byte[] r = Arrays.copyOfRange(signedBytes, 0, 32); + final byte[] s = Arrays.copyOfRange(signedBytes, 32, 64); + final int recId = ((PrivateKeyECDSA) privateKey).getRecoveryId(r, s, sequence); + + return RLPEncoder.sequence( + Integers.toBytes(0x04), + List.of( + params.chainId, + Integers.toBytes(params.nonce), + params.maxPriorityGas, + params.maxGas, + params.gasLimit, + params.to, + params.value, + params.callData, + List.of(), + encodedAuthorizationList, + Integers.toBytes(recId), + r, + s)); + } + + private static class EthereumTransactionParams { + final byte[] chainId; + final int nonce; + final byte[] maxPriorityGas; + final byte[] maxGas; + final byte[] gasLimit; + final byte[] to; + final byte[] value; + final byte[] callData; + + EthereumTransactionParams( + byte[] chainId, + int nonce, + byte[] maxPriorityGas, + byte[] maxGas, + byte[] gasLimit, + byte[] to, + byte[] value, + byte[] callData) { + this.chainId = chainId; + this.nonce = nonce; + this.maxPriorityGas = maxPriorityGas; + this.maxGas = maxGas; + this.gasLimit = gasLimit; + this.to = to; + this.value = value; + this.callData = callData; + } + } + + private static class ContractCreationResult { + final FileId fileId; + final ContractId contractId; + + ContractCreationResult(FileId fileId, ContractId contractId) { + this.fileId = fileId; + this.contractId = contractId; + } + } }