All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.node.app.hapi.utils.ethereum.EthTxData Maven / Gradle / Ivy

There is a newer version: 0.56.6
Show newest version
/*
 * Copyright (C) 2022-2024 Hedera Hashgraph, LLC
 *
 * 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.hedera.node.app.hapi.utils.ethereum;

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.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.jcajce.provider.digest.Keccak;

public record EthTxData(
        byte[] rawTx,
        EthTransactionType type,
        byte[] chainId,
        long nonce,
        byte[] gasPrice,
        byte[] maxPriorityGas,
        byte[] maxGas,
        long gasLimit,
        byte[] to,
        BigInteger value, // weibar, always positive - note that high-bit might be ON in RLP encoding: still positive
        byte[] callData,
        byte[] accessList,
        int recId, // "recovery id" part of a v,r,s ECDSA signature - range 0..1
        byte[] v, // actual `v` value, incoming, recovery id (`recId` above) (possibly) encoded with chain id
        byte[] r,
        byte[] s) {

    /**
     * A "wiebar" is 10⁻¹⁸ of an hbar.  The relationship is weibar : hbar as wei : ether.  Ethereum
     * transactions come in with transfer amounts in units of weibar.  Elsewhere in Hedera we use
     * units of tinybar (10⁻⁸ of an hbar), and here is the conversion factor:
     */
    public static final BigInteger WEIBARS_IN_A_TINYBAR = BigInteger.valueOf(10_000_000_000L);

    // Copy of constants from besu-native, remove when next besu-native publishes
    static final int SECP256K1_FLAGS_TYPE_COMPRESSION = 1 << 1;
    static final int SECP256K1_FLAGS_BIT_COMPRESSION = 1 << 8;
    static final int SECP256K1_EC_COMPRESSED = (SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION);

    // EIP155 note support for v = 27|28 cases in unprotected transaction cases
    static final BigInteger LEGACY_V_BYTE_SIGNATURE_0 = BigInteger.valueOf(27);
    static final BigInteger LEGACY_V_BYTE_SIGNATURE_1 = BigInteger.valueOf(28);

    // The specific transaction bytes that are used to deploy the Deterministic Deployer contract
    // see -  https://github.com/Arachnid/deterministic-deployment-proxy?tab=readme-ov-file#deployment-transaction
    public static final byte[] DETERMINISTIC_DEPLOYER_TRANSACTION = HexFormat.of()
            .parseHex(
                    "f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222");

    public static EthTxData populateEthTxData(byte[] data) {
        try {
            var decoder = RLPDecoder.RLP_STRICT.sequenceIterator(data);
            var rlpItem = decoder.next();
            if (rlpItem.isList()) {
                return populateLegacyEthTxData(rlpItem, data);
            }

            return switch (rlpItem.asByte()) {
                case 1 -> populateEip2390EthTxData(decoder.next(), data);
                case 2 -> populateEip1559EthTxData(decoder.next(), data);
                case 3 -> null; // We don't currently support Cancun "blob" transactions
                default -> null;
            };

        } catch (IllegalArgumentException | NoSuchElementException e) {
            return null;
        }
    }

    public EthTxData replaceCallData(final byte[] newCallData) {
        return new EthTxData(
                null,
                type,
                chainId,
                nonce,
                gasPrice,
                maxPriorityGas,
                maxGas,
                gasLimit,
                to,
                value,
                newCallData,
                accessList,
                recId,
                v,
                r,
                s);
    }

    @VisibleForTesting
    EthTxData replaceOfferedGasPrice(@NonNull final BigInteger replacementGasPrice) {
        return new EthTxData(
                null,
                type,
                chainId,
                nonce,
                replacementGasPrice.toByteArray(),
                maxPriorityGas,
                maxGas,
                gasLimit,
                to,
                value,
                callData,
                accessList,
                recId,
                v,
                r,
                s);
    }

    @VisibleForTesting
    EthTxData replaceValue(@NonNull final BigInteger replacementValue) {
        return new EthTxData(
                null,
                type,
                chainId,
                nonce,
                gasPrice,
                maxPriorityGas,
                maxGas,
                gasLimit,
                to,
                replacementValue,
                callData,
                accessList,
                recId,
                v,
                r,
                s);
    }

    // For more information on "recovery id" see
    // https://coinsbench.com/understanding-digital-signatures-the-role-of-v-r-s-in-cryptographic-security-and-signature-b9d2b89bbc0c

    // For more information on encoding `v` see EIP-155 - https://eips.ethereum.org/EIPS/eip-155

    public byte[] encodeTx() {
        if (accessList != null && accessList.length > 0) {
            throw new IllegalStateException("Re-encoding access list is unsupported");
        }
        return switch (type) {
            case LEGACY_ETHEREUM -> RLPEncoder.encodeAsList(
                    Integers.toBytes(nonce),
                    gasPrice,
                    Integers.toBytes(gasLimit),
                    to,
                    Integers.toBytesUnsigned(value),
                    callData,
                    v,
                    r,
                    s);
            case EIP2930 -> RLPEncoder.encodeSequentially(
                    Integers.toBytes(0x01),
                    List.of(
                            chainId,
                            Integers.toBytes(nonce),
                            gasPrice,
                            Integers.toBytes(gasLimit),
                            to,
                            Integers.toBytesUnsigned(value),
                            callData,
                            List.of(/*accessList*/ ),
                            Integers.toBytes(recId),
                            r,
                            s));
            case EIP1559 -> RLPEncoder.encodeSequentially(
                    Integers.toBytes(0x02),
                    List.of(
                            chainId,
                            Integers.toBytes(nonce),
                            maxPriorityGas,
                            maxGas,
                            Integers.toBytes(gasLimit),
                            to,
                            Integers.toBytesUnsigned(value),
                            callData,
                            List.of(/*accessList*/ ),
                            Integers.toBytes(recId),
                            r,
                            s));
        };
    }

    public long getAmount() {
        return value.divide(WEIBARS_IN_A_TINYBAR).longValueExact();
    }

    public BigInteger getMaxGasAsBigInteger(final long tinybarGasPrice) {
        long multiple = 1L;
        if (type == EthTransactionType.LEGACY_ETHEREUM && Arrays.equals(rawTx, DETERMINISTIC_DEPLOYER_TRANSACTION)) {
            multiple = tinybarGasPrice;
        }
        return switch (type) {
            case LEGACY_ETHEREUM -> new BigInteger(1, gasPrice).multiply(BigInteger.valueOf(multiple));
            case EIP2930 -> new BigInteger(1, gasPrice);
            case EIP1559 -> new BigInteger(1, maxGas);
        };
    }

    /**
     * Returns the effective offered gas price for this transaction, defined as the minimum of the
     * nominal offered gas price in tinybars and {@code (Long.MAX_VALUE / gasLimit)}.
     *
     * 

Clearly the latter value would always be un-payable, since the transaction would cost more * than the entire hbar supply. We just do this to avoid integral overflow. * * @param weibarGasPrice the current gas price in weibars * @return the effective offered gas price in tinybars */ public long effectiveOfferedGasPriceInTinybars(final long weibarGasPrice) { return BigInteger.valueOf(Long.MAX_VALUE) .min(getMaxGasAsBigInteger(weibarGasPrice).divide(WEIBARS_IN_A_TINYBAR)) .longValueExact(); } /** * Returns the effective tinybar value of this transaction, defined as the minimum of the nominal * value in tinybars and {@code Long.MAX_VALUE}. * *

Clearly the latter value would always be un-payable, since the transaction would send more * than the entire hbar supply. We just do this to avoid integral overflow. * * @return the effective tinybar value */ public long effectiveTinybarValue() { return BigInteger.valueOf(Long.MAX_VALUE) .min(value.divide(WEIBARS_IN_A_TINYBAR)) .longValueExact(); } public byte[] getEthereumHash() { return new Keccak.Digest256().digest(rawTx == null ? encodeTx() : rawTx); } public enum EthTransactionType { LEGACY_ETHEREUM, EIP2930, EIP1559, } @Override public boolean equals(final Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; final EthTxData ethTxData = (EthTxData) other; return (nonce == ethTxData.nonce) && (gasLimit == ethTxData.gasLimit) && (recId == ethTxData.recId) && (Arrays.equals(rawTx, ethTxData.rawTx)) && (type == ethTxData.type) && (Arrays.equals(chainId, ethTxData.chainId)) && (Arrays.equals(gasPrice, ethTxData.gasPrice)) && (Arrays.equals(maxPriorityGas, ethTxData.maxPriorityGas)) && (Arrays.equals(maxGas, ethTxData.maxGas)) && (Arrays.equals(to, ethTxData.to)) && (Objects.equals(value, ethTxData.value)) && (Arrays.equals(callData, ethTxData.callData)) && (Arrays.equals(accessList, ethTxData.accessList)) && (Arrays.equals(v, ethTxData.v)) && (Arrays.equals(r, ethTxData.r)) && (Arrays.equals(s, ethTxData.s)); } @Override public int hashCode() { int result = Arrays.hashCode(rawTx); result = 31 * result + (type != null ? type.hashCode() : 0); result = 31 * result + Arrays.hashCode(chainId); result = 31 * result + (int) (nonce ^ (nonce >>> 32)); result = 31 * result + Arrays.hashCode(gasPrice); result = 31 * result + Arrays.hashCode(maxPriorityGas); result = 31 * result + Arrays.hashCode(maxGas); result = 31 * result + (int) (gasLimit ^ (gasLimit >>> 32)); result = 31 * result + Arrays.hashCode(to); result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + Arrays.hashCode(callData); result = 31 * result + Arrays.hashCode(accessList); result = 31 * result + recId; result = 31 * result + Arrays.hashCode(v); result = 31 * result + Arrays.hashCode(r); result = 31 * result + Arrays.hashCode(s); return result; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("rawTx", rawTx == null ? null : Hex.encodeHexString(rawTx)) .add("type", type) .add("chainId", chainId == null ? null : Hex.encodeHexString(chainId)) .add("nonce", nonce) .add("gasPrice", gasPrice == null ? null : Hex.encodeHexString(gasPrice)) .add("maxPriorityGas", maxPriorityGas == null ? null : Hex.encodeHexString(maxPriorityGas)) .add("maxGas", maxGas == null ? null : Hex.encodeHexString(maxGas)) .add("gasLimit", gasLimit) .add("to", to == null ? null : Hex.encodeHexString(to)) .add("value", value) .add("callData", Hex.encodeHexString(callData)) .add("accessList", accessList == null ? null : Hex.encodeHexString(accessList)) .add("recId", recId) .add("v", v == null ? null : Hex.encodeHexString(v)) .add("r", Hex.encodeHexString(r)) .add("s", Hex.encodeHexString(s)) .toString(); } public boolean hasCallData() { return callData != null && callData.length > 0; } public boolean hasToAddress() { return to != null && to.length > 0; } public boolean matchesChainId(final byte[] hederaChainId) { // first two checks handle the unprotected ethereum transactions // before EIP155 - source: https://eips.ethereum.org/EIPS/eip-155 return chainId == null || chainId.length == 0 || Arrays.compare(chainId, hederaChainId) == 0; } @VisibleForTesting public EthTxData replaceTo(final byte[] newTo) { return new EthTxData( null, type, chainId, nonce, gasPrice, maxPriorityGas, maxGas, gasLimit, newTo, value, callData, accessList, recId, v, r, s); } /** * Encodes the transaction data into a EthTxData according to legacy RLP format. * * @return the encoded transaction data */ private static EthTxData populateLegacyEthTxData(RLPItem rlpItem, byte[] rawTx) { List rlpList = rlpItem.asRLPList().elements(); if (rlpList.size() != 9) { return null; } byte[] chainId = null; byte[] val = rlpList.get(6).asBytes(); BigInteger vBI = new BigInteger(1, val); byte recId = vBI.testBit(0) ? (byte) 0 : 1; // https://eips.ethereum.org/EIPS/eip-155 if (vBI.compareTo(BigInteger.valueOf(34)) > 0) { // after EIP155 the chain id is equal to // CHAIN_ID = (v - {0,1} - 35) / 2 chainId = vBI.subtract(BigInteger.valueOf(35)).shiftRight(1).toByteArray(); } else if (isLegacyUnprotectedEtx(vBI)) { // before EIP155 the chain id is considered equal to 0 chainId = new byte[0]; } return new EthTxData( rawTx, EthTransactionType.LEGACY_ETHEREUM, chainId, rlpList.get(0).asLong(), // nonce rlpList.get(1).asBytes(), // gasPrice null, // maxPriorityGas null, // maxGas rlpList.get(2).asLong(), // gasLimit rlpList.get(3).data(), // to rlpList.get(4).asBigInt(), // value rlpList.get(5).data(), // callData null, // accessList recId, val, rlpList.get(7).data(), // r rlpList.get(8).data() // s ); } /** * Encodes the transaction data into a EthTxData according to EIP 1559 RLP format. * * @return the encoded transaction data */ private static EthTxData populateEip1559EthTxData(RLPItem rlpItem, byte[] rawTx) { if (!rlpItem.isList()) { return null; } List rlpList = rlpItem.asRLPList().elements(); if (rlpList.size() != 12) { return null; } return new EthTxData( rawTx, EthTransactionType.EIP1559, rlpList.get(0).data(), // chainId rlpList.get(1).asLong(), // nonce null, // gasPrice rlpList.get(2).data(), // maxPriorityGas rlpList.get(3).data(), // maxGas rlpList.get(4).asLong(), // gasLimit rlpList.get(5).data(), // to rlpList.get(6).asBigInt(), // value rlpList.get(7).data(), // callData rlpList.get(8).data(), // accessList rlpList.get(9).asByte(), // recId null, // v rlpList.get(10).data(), // r rlpList.get(11).data() // s ); } /** * Encodes the transaction data into a EthTxData according to EIP 2930 RLP format. * * @return the encoded transaction data */ private static EthTxData populateEip2390EthTxData(RLPItem rlpItem, byte[] rawTx) { if (!rlpItem.isList()) { return null; } List rlpList = rlpItem.asRLPList().elements(); if (rlpList.size() != 11) { return null; } return new EthTxData( rawTx, EthTransactionType.EIP2930, rlpList.get(0).data(), // chainId rlpList.get(1).asLong(), // nonce rlpList.get(2).data(), // gasPrice null, // maxPriorityGas null, // maxGas rlpList.get(3).asLong(), // gasLimit rlpList.get(4).data(), // to rlpList.get(5).asBigInt(), // value rlpList.get(6).data(), // callData rlpList.get(7).data(), // accessList rlpList.get(8).asByte(), // recId null, // v rlpList.get(9).data(), // r rlpList.get(10).data() // s ); } // before EIP155 the value of v in // (unprotected) ethereum transactions is either 27 or 28 private static boolean isLegacyUnprotectedEtx(@NonNull BigInteger vBI) { return vBI.compareTo(LEGACY_V_BYTE_SIGNATURE_0) == 0 || vBI.compareTo(LEGACY_V_BYTE_SIGNATURE_1) == 0; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy