| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE |
| * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file |
| * to You 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 org.apache.tuweni.eth; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static java.util.Objects.requireNonNull; |
| import static org.apache.tuweni.crypto.Hash.keccak256; |
| |
| import org.apache.tuweni.bytes.Bytes; |
| import org.apache.tuweni.crypto.SECP256K1; |
| import org.apache.tuweni.rlp.RLP; |
| import org.apache.tuweni.rlp.RLPException; |
| import org.apache.tuweni.rlp.RLPReader; |
| import org.apache.tuweni.rlp.RLPWriter; |
| import org.apache.tuweni.units.bigints.UInt256; |
| import org.apache.tuweni.units.ethereum.Gas; |
| import org.apache.tuweni.units.ethereum.Wei; |
| |
| import java.math.BigInteger; |
| import javax.annotation.Nullable; |
| |
| import com.google.common.base.Objects; |
| |
| /** |
| * An Ethereum transaction. |
| */ |
| public final class Transaction { |
| |
| // The base of the signature v-value |
| private static final int V_BASE = 27; |
| |
| /** |
| * Deserialize a transaction from RLP encoded bytes. |
| * |
| * @param encoded The RLP encoded transaction. |
| * @return The de-serialized transaction. |
| * @throws RLPException If there is an error decoding the transaction. |
| */ |
| public static Transaction fromBytes(Bytes encoded) { |
| return fromBytes(encoded, false); |
| } |
| |
| /** |
| * Deserialize a transaction from RLP encoded bytes. |
| * |
| * @param encoded The RLP encoded transaction. |
| * @param lenient If {@code true}, the RLP decoding will be lenient toward any non-minimal encoding. |
| * @return The de-serialized transaction. |
| * @throws RLPException If there is an error decoding the transaction. |
| */ |
| public static Transaction fromBytes(Bytes encoded, boolean lenient) { |
| requireNonNull(encoded); |
| return RLP.decode(encoded, lenient, (reader) -> { |
| Transaction tx = reader.readList(Transaction::readFrom); |
| if (!reader.isComplete()) { |
| throw new RLPException("Additional bytes present at the end of the encoded transaction"); |
| } |
| return tx; |
| }); |
| } |
| |
| /** |
| * Deserialize a transaction from an RLP input. |
| * |
| * @param reader The RLP reader. |
| * @return The de-serialized transaction. |
| * @throws RLPException If there is an error decoding the transaction. |
| */ |
| public static Transaction readFrom(RLPReader reader) { |
| UInt256 nonce = reader.readUInt256(); |
| Wei gasPrice = Wei.valueOf(reader.readUInt256()); |
| Gas gasLimit = Gas.valueOf(reader.readLong()); |
| Bytes addressBytes = reader.readValue(); |
| Address address; |
| try { |
| address = addressBytes.isEmpty() ? null : Address.fromBytes(addressBytes); |
| } catch (IllegalArgumentException e) { |
| throw new RLPException("Value is the wrong size to be an address", e); |
| } |
| Wei value = Wei.valueOf(reader.readUInt256()); |
| Bytes payload = reader.readValue(); |
| int encodedV = reader.readInt(); |
| Bytes rbytes = reader.readValue(); |
| if (rbytes.size() > 32) { |
| throw new RLPException("r-value of the signature is " + rbytes.size() + ", it should be at most 32 bytes"); |
| } |
| BigInteger r = rbytes.toUnsignedBigInteger(); |
| Bytes sbytes = reader.readValue(); |
| if (sbytes.size() > 32) { |
| throw new RLPException("s-value of the signature is " + sbytes.size() + ", it should be at most 32 bytes"); |
| } |
| BigInteger s = sbytes.toUnsignedBigInteger(); |
| if (!reader.isComplete()) { |
| throw new RLPException("Additional bytes present at the end of the encoding"); |
| } |
| |
| byte v; |
| Integer chainId = null; |
| |
| if (encodedV == V_BASE || encodedV == (V_BASE + 1)) { |
| v = (byte) (encodedV - V_BASE); |
| } else if (encodedV > 35) { |
| chainId = (encodedV - 35) / 2; |
| v = (byte) (encodedV - (2 * chainId + 35)); |
| } else { |
| throw new RLPException("Invalid v encoded value " + encodedV); |
| } |
| |
| SECP256K1.Signature signature; |
| try { |
| signature = SECP256K1.Signature.create(v, r, s); |
| } catch (IllegalArgumentException e) { |
| throw new RLPException("Invalid signature: " + e.getMessage()); |
| } |
| try { |
| return new Transaction(nonce, gasPrice, gasLimit, address, value, payload, chainId, signature); |
| } catch (IllegalArgumentException e) { |
| throw new RLPException(e.getMessage(), e); |
| } |
| } |
| |
| private final UInt256 nonce; |
| private final Wei gasPrice; |
| private final Gas gasLimit; |
| @Nullable |
| private final Address to; |
| private final Wei value; |
| private final SECP256K1.Signature signature; |
| private final Bytes payload; |
| private final Integer chainId; |
| private volatile Hash hash; |
| private volatile Address sender; |
| private volatile Boolean validSignature; |
| |
| /** |
| * Create a transaction. |
| * |
| * @param nonce The transaction nonce. |
| * @param gasPrice The transaction gas price. |
| * @param gasLimit The transaction gas limit. |
| * @param to The target contract address, if any. |
| * @param value The amount of Eth to transfer. |
| * @param payload The transaction payload. |
| * @param keyPair A keypair to generate the transaction signature with. |
| */ |
| public Transaction( |
| UInt256 nonce, |
| Wei gasPrice, |
| Gas gasLimit, |
| @Nullable Address to, |
| Wei value, |
| Bytes payload, |
| SECP256K1.KeyPair keyPair) { |
| this(nonce, gasPrice, gasLimit, to, value, payload, keyPair, null); |
| } |
| |
| /** |
| * Create a transaction. |
| * |
| * @param nonce The transaction nonce. |
| * @param gasPrice The transaction gas price. |
| * @param gasLimit The transaction gas limit. |
| * @param to The target contract address, if any. |
| * @param value The amount of Eth to transfer. |
| * @param payload The transaction payload. |
| * @param keyPair A keypair to generate the transaction signature with. |
| * @param chainId the chain ID. |
| */ |
| public Transaction( |
| UInt256 nonce, |
| Wei gasPrice, |
| Gas gasLimit, |
| @Nullable Address to, |
| Wei value, |
| Bytes payload, |
| SECP256K1.KeyPair keyPair, |
| @Nullable Integer chainId) { |
| this( |
| nonce, |
| gasPrice, |
| gasLimit, |
| to, |
| value, |
| payload, |
| chainId, |
| generateSignature(nonce, gasPrice, gasLimit, to, value, payload, chainId, keyPair)); |
| } |
| |
| /** |
| * Create a transaction. |
| * |
| * @param nonce The transaction nonce. |
| * @param gasPrice The transaction gas price. |
| * @param gasLimit The transaction gas limit. |
| * @param to The target contract address, if any. |
| * @param value The amount of Eth to transfer. |
| * @param payload The transaction payload. |
| * @param chainId The chain id. |
| * @param signature The transaction signature. |
| */ |
| public Transaction( |
| UInt256 nonce, |
| Wei gasPrice, |
| Gas gasLimit, |
| @Nullable Address to, |
| Wei value, |
| Bytes payload, |
| @Nullable Integer chainId, |
| SECP256K1.Signature signature) { |
| requireNonNull(nonce); |
| checkArgument(nonce.compareTo(UInt256.ZERO) >= 0, "nonce must be >= 0"); |
| requireNonNull(gasPrice); |
| requireNonNull(value); |
| requireNonNull(signature); |
| requireNonNull(payload); |
| this.nonce = nonce; |
| this.gasPrice = gasPrice; |
| this.gasLimit = gasLimit; |
| this.to = to; |
| this.value = value; |
| this.signature = signature; |
| this.payload = payload; |
| this.chainId = chainId; |
| } |
| |
| /** |
| * @return The transaction nonce. |
| */ |
| public UInt256 getNonce() { |
| return nonce; |
| } |
| |
| /** |
| * @return The transaction gas price. |
| */ |
| public Wei getGasPrice() { |
| return gasPrice; |
| } |
| |
| /** |
| * @return The transaction gas limit. |
| */ |
| public Gas getGasLimit() { |
| return gasLimit; |
| } |
| |
| /** |
| * @return The target contract address, or null if not present. |
| */ |
| @Nullable |
| public Address getTo() { |
| return to; |
| } |
| |
| /** |
| * @return {@code true} if the transaction is a contract creation ({@code to} address is {@code null}). |
| */ |
| public boolean isContractCreation() { |
| return to == null; |
| } |
| |
| /** |
| * @return The amount of Eth to transfer. |
| */ |
| public Wei getValue() { |
| return value; |
| } |
| |
| /** |
| * @return The transaction signature. |
| */ |
| public SECP256K1.Signature getSignature() { |
| return signature; |
| } |
| |
| /** |
| * @return The transaction payload. |
| */ |
| public Bytes getPayload() { |
| return payload; |
| } |
| |
| /** |
| * @return the chain id of the transaction, or null if no chain id was encoded on the transaction. |
| * @see <a href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md">EIP-155</a> |
| */ |
| public Integer getChainId() { |
| return chainId; |
| } |
| |
| /** |
| * Calculate and return the hash for this transaction. |
| * |
| * @return The hash. |
| */ |
| public Hash getHash() { |
| if (hash != null) { |
| return hash; |
| } |
| Bytes rlp = toBytes(); |
| hash = Hash.hash(rlp); |
| return hash; |
| } |
| |
| /** |
| * @return The sender of the transaction, or {@code null} if the signature is invalid. |
| */ |
| @Nullable |
| public Address getSender() { |
| if (validSignature != null) { |
| return sender; |
| } |
| return verifySignatureAndGetSender(); |
| } |
| |
| @Nullable |
| private Address verifySignatureAndGetSender() { |
| Bytes data = signatureData(nonce, gasPrice, gasLimit, to, value, payload, chainId); |
| |
| SECP256K1.PublicKey publicKey = SECP256K1.PublicKey.recoverFromSignature(data, signature); |
| if (publicKey == null) { |
| validSignature = false; |
| } else { |
| sender = Address.fromBytes(Bytes.wrap(keccak256(publicKey.bytesArray()), 12, 20)); |
| validSignature = true; |
| } |
| |
| return sender; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof Transaction)) { |
| return false; |
| } |
| Transaction that = (Transaction) obj; |
| return nonce.equals(that.nonce) |
| && gasPrice.equals(that.gasPrice) |
| && gasLimit.equals(that.gasLimit) |
| && Objects.equal(to, that.to) |
| && value.equals(that.value) |
| && signature.equals(that.signature) |
| && payload.equals(that.payload); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(nonce, gasPrice, gasLimit, to, value, signature, payload); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format( |
| "Transaction{nonce=%s, gasPrice=%s, gasLimit=%s, to=%s, value=%s, signature=%s, payload=%s", |
| nonce, |
| gasPrice, |
| gasLimit, |
| to, |
| value, |
| signature, |
| payload); |
| } |
| |
| /** |
| * @return The RLP serialized form of this transaction. |
| */ |
| public Bytes toBytes() { |
| return RLP.encodeList(this::writeTo); |
| } |
| |
| /** |
| * Write this transaction to an RLP output. |
| * |
| * @param writer The RLP writer. |
| */ |
| public void writeTo(RLPWriter writer) { |
| writer.writeUInt256(nonce); |
| writer.writeUInt256(gasPrice.toUInt256()); |
| writer.writeLong(gasLimit.toLong()); |
| writer.writeValue((to != null) ? to.toBytes() : Bytes.EMPTY); |
| writer.writeUInt256(value.toUInt256()); |
| writer.writeValue(payload); |
| if (chainId != null) { |
| int v = signature.v() + V_BASE + 8 + chainId * 2; |
| writer.writeInt(v); |
| } else { |
| writer.writeInt((int) signature.v() + V_BASE); |
| } |
| writer.writeBigInteger(signature.r()); |
| writer.writeBigInteger(signature.s()); |
| } |
| |
| private static SECP256K1.Signature generateSignature( |
| UInt256 nonce, |
| Wei gasPrice, |
| Gas gasLimit, |
| @Nullable Address to, |
| Wei value, |
| Bytes payload, |
| @Nullable Integer chainId, |
| SECP256K1.KeyPair keyPair) { |
| return SECP256K1.sign(signatureData(nonce, gasPrice, gasLimit, to, value, payload, chainId), keyPair); |
| } |
| |
| public static Bytes signatureData( |
| UInt256 nonce, |
| Wei gasPrice, |
| Gas gasLimit, |
| @Nullable Address to, |
| Wei value, |
| Bytes payload, |
| @Nullable Integer chainId) { |
| return RLP.encodeList(writer -> { |
| writer.writeUInt256(nonce); |
| writer.writeValue(gasPrice.toMinimalBytes()); |
| writer.writeValue(gasLimit.toMinimalBytes()); |
| writer.writeValue((to != null) ? to.toBytes() : Bytes.EMPTY); |
| writer.writeValue(value.toMinimalBytes()); |
| writer.writeValue(payload); |
| if (chainId != null) { |
| writer.writeInt(chainId); |
| writer.writeUInt256(UInt256.ZERO); |
| writer.writeUInt256(UInt256.ZERO); |
| } |
| }); |
| } |
| } |