Skip to content

Commit

Permalink
refactor!: make access list property immutable on unsigned txs, rep…
Browse files Browse the repository at this point in the history
…lace `rlpEncodeFields` with `rlpEncodeEnveloped` (#136)

* add additional test assertions

* make accessList property on unsigned txs immutable

* move enveloped tx encoding to specific unsigned tx classes
  • Loading branch information
ArtificialPB authored Jun 14, 2024
1 parent c672064 commit da0dbf5
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 302 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class TransactionSigned @JvmOverloads constructor(
override val hash: Hash
get() {
if (_hash == null) {
val hashRlp = RlpEncoder().also { rlpEncode(it, true) }.toByteArray()
val hashRlp = RlpEncoder()
.also { tx.rlpEncodeEnveloped(it, signature, true) }
.toByteArray()

_hash = Hash(Hashing.keccak256(hashRlp))
}
return _hash!!
Expand Down Expand Up @@ -107,35 +110,8 @@ class TransactionSigned @JvmOverloads constructor(
return "TransactionSigned(tx=$tx, signature=$signature)"
}

override fun rlpEncode(rlp: RlpEncoder) = rlpEncode(rlp, false)

private fun rlpEncode(rlp: RlpEncoder, hashEncoding: Boolean) {
// non-legacy txs are enveloped based on eip2718
if (tx.type != TxType.Legacy) {
rlp.appendRaw(tx.type.type.toByte())
}

rlp.encodeList {
// If blob tx has sidecar, encode as network encoding - but only if not encoding for hash. For hash, we use
// canonical encoding.
//
// Network encoding: 'type || rlp([tx_payload_body, blobs, commitments, proofs])'
// Canonical encoding: 'type || rlp(tx_payload_body)', where 'tx_payload_body' is a list of tx fields with
// signature values.
//
// See: https://eips.ethereum.org/EIPS/eip-4844#networking
if (!hashEncoding && tx.type == TxType.Blob && (tx as TxBlob).sidecar != null) {
rlp.encodeList {
tx.rlpEncodeFields(this)
signature.rlpEncode(this)
}

tx.sidecar!!.rlpEncode(this)
} else {
tx.rlpEncodeFields(this)
signature.rlpEncode(this)
}
}
override fun rlpEncode(rlp: RlpEncoder) {
tx.rlpEncodeEnveloped(rlp, signature, false)
}

companion object : RlpDecodable<TransactionSigned> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,71 +1,57 @@
package io.ethers.core.types.transaction

import io.ethers.core.types.Signature
import io.ethers.crypto.Hashing
import io.ethers.rlp.RlpDecodable
import io.ethers.rlp.RlpDecoder
import io.ethers.rlp.RlpEncodable
import io.ethers.rlp.RlpEncoder
import java.math.BigInteger

/**
* An unsigned [Transaction] with functions for signing.
* */
sealed interface TransactionUnsigned : Transaction {
sealed interface TransactionUnsigned : Transaction, RlpEncodable {
/**
* RLP encode transaction fields.
*/
fun rlpEncodeFields(rlp: RlpEncoder)
* RLP encode transaction with optional signature according to [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718)
* enveloped format. If [hashEncoding] is true, the transaction is encoded for hash calculation, either signature
* hash (if [signature] is null) or transaction hash (if [signature] is not null).
* */
fun rlpEncodeEnveloped(rlp: RlpEncoder, signature: Signature?, hashEncoding: Boolean)

/**
* Get hash used for signing the transaction.
* */
fun signatureHash(): ByteArray {
val encoder = RlpEncoder()
rlpEncode(encoder, true)
rlpEncodeEnveloped(encoder, null, true)
return Hashing.keccak256(encoder.toByteArray())
}

/**
* Encode [TransactionUnsigned] into provided [RlpEncoder].
* Encode [TransactionUnsigned] via provided [RlpEncoder]. This RLP format includes all-zero signature fields and
* is the inverse of [rlpDecode].
*/
fun rlpEncode(encoder: RlpEncoder, forSignatureHash: Boolean = false) {
// non-legacy txs are enveloped based on eip2718
if (type != TxType.Legacy) {
encoder.appendRaw(type.type.toByte())
}

encoder.encodeList {
rlpEncodeFields(encoder)

if (!forSignatureHash) {
// Encode empty r, s, v signature fields
encoder.encode(0)
encoder.encode(0)
encoder.encode(0)
return@encodeList
}

if (type == TxType.Legacy && ChainId.isValid(chainId)) {
// EIP-155 support for LegacyTx, applies only if we have a valid chainId
// see: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
encoder.encode(chainId)
encoder.encode(0)
encoder.encode(0)
}
}
override fun rlpEncode(rlp: RlpEncoder) {
rlpEncodeEnveloped(rlp, EMPTY_SIGNATURE, true)
}

companion object : RlpDecodable<TransactionUnsigned> {
private val EMPTY_SIGNATURE = Signature(BigInteger.ZERO, BigInteger.ZERO, 0L)

override fun rlpDecode(rlp: RlpDecoder) = rlpDecode(rlp, ChainId.NONE)

/**
* Decode RLP data array. Compared to base [rlpDecode], this function provides additional [chainId]
* parameter which is needed for correct replay-protected [TxType.LEGACY] transaction decoding.
* parameter which is needed for correct replay-protected [TxType.Legacy] transaction decoding.
*/
fun rlpDecode(data: ByteArray, chainId: Long) = rlpDecode(RlpDecoder(data), chainId)

/**
* Decode [rlp]. Compared to base [rlpDecode], this function provides additional [chainId]
* parameter which is needed for correct replay-protected [TxType.LEGACY] transaction decoding.
* parameter which is needed for correct replay-protected [TxType.Legacy] transaction decoding.
*
* This function is inverse of [rlpEncode].
*/
fun rlpDecode(rlp: RlpDecoder, chainId: Long): TransactionUnsigned? {
val type = rlp.peekByte().toUByte().toInt()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.ethers.core.types.AccessList
import io.ethers.core.types.Address
import io.ethers.core.types.Bytes
import io.ethers.core.types.Hash
import io.ethers.core.types.Signature
import io.ethers.rlp.RlpDecodable
import io.ethers.rlp.RlpDecoder
import io.ethers.rlp.RlpEncoder
Expand All @@ -14,15 +15,15 @@ import java.math.BigInteger
*
* - [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)
* */
class TxAccessList(
data class TxAccessList(
override val to: Address?,
override val value: BigInteger,
override val nonce: Long,
override val gas: Long,
override val gasPrice: BigInteger,
override val data: Bytes?,
override val chainId: Long,
override var accessList: List<AccessList.Item>,
override val accessList: List<AccessList.Item>,
) : TransactionUnsigned {
init {
if (!ChainId.isValid(chainId)) {
Expand All @@ -45,74 +46,21 @@ class TxAccessList(
override val blobVersionedHashes: List<Hash>?
get() = null

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(chainId)
rlp.encode(nonce)
rlp.encode(gasPrice)
rlp.encode(gas)
rlp.encode(to)
rlp.encode(value)
rlp.encode(data)
rlp.encodeList(accessList)
}

/**
* Copy or override `this` parameters into new [TxAccessList] object.
*/
fun copy(
to: Address? = this.to,
value: BigInteger = this.value,
nonce: Long = this.nonce,
gas: Long = this.gas,
gasPrice: BigInteger = this.gasPrice,
data: Bytes? = this.data,
chainId: Long = this.chainId,
accessList: List<AccessList.Item> = this.accessList,
): TxAccessList {
return TxAccessList(
to = to,
value = value,
nonce = nonce,
gas = gas,
gasPrice = gasPrice,
data = data,
chainId = chainId,
accessList = accessList,
)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as TxAccessList
override fun rlpEncodeEnveloped(rlp: RlpEncoder, signature: Signature?, hashEncoding: Boolean) {
rlp.appendRaw(type.type.toByte())

if (to != other.to) return false
if (value != other.value) return false
if (nonce != other.nonce) return false
if (gas != other.gas) return false
if (gasPrice != other.gasPrice) return false
if (data != other.data) return false
if (chainId != other.chainId) return false
if (accessList != other.accessList) return false
rlp.encodeList {
rlp.encode(chainId)
rlp.encode(nonce)
rlp.encode(gasPrice)
rlp.encode(gas)
rlp.encode(to)
rlp.encode(value)
rlp.encode(data)
rlp.encodeList(accessList)

return true
}

override fun hashCode(): Int {
var result = to?.hashCode() ?: 0
result = 31 * result + value.hashCode()
result = 31 * result + nonce.hashCode()
result = 31 * result + gas.hashCode()
result = 31 * result + gasPrice.hashCode()
result = 31 * result + (data?.hashCode() ?: 0)
result = 31 * result + chainId.hashCode()
result = 31 * result + accessList.hashCode()
return result
}

override fun toString(): String {
return "TxAccessList(to=$to, value=$value, nonce=$nonce, gas=$gas, gasPrice=$gasPrice, data=$data, chainId=$chainId, accessList=$accessList)"
signature?.rlpEncode(this)
}
}

companion object : RlpDecodable<TxAccessList> {
Expand Down
Loading

0 comments on commit da0dbf5

Please sign in to comment.