Skip to content

Commit

Permalink
feat(core): add support for eip-4844 blob transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtificialPB committed Nov 29, 2023
1 parent 50dd060 commit aea86c7
Show file tree
Hide file tree
Showing 16 changed files with 616 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import io.ethers.core.readAddress
import io.ethers.core.readBytesEmptyAsNull
import io.ethers.core.readHash
import io.ethers.core.readHexBigInteger
import io.ethers.core.readHexInt
import io.ethers.core.readHexLong
import io.ethers.core.readListOf
import io.ethers.core.readOrNull
import io.ethers.core.types.transaction.ChainId
import io.ethers.core.types.transaction.TransactionRecovered
import io.ethers.core.types.transaction.TxBlob
import io.ethers.core.types.transaction.TxType
import java.math.BigInteger

Expand All @@ -41,8 +43,14 @@ data class RPCTransaction(
val v: Long,
val r: BigInteger,
val s: BigInteger,
val yParity: Int,
override val blobVersionedHashes: List<Hash>?,
override val blobFeeCap: BigInteger?,
val otherFields: Map<String, JsonNode> = emptyMap(),
) : TransactionRecovered
) : TransactionRecovered {
override val blobGas: Long
get() = blobVersionedHashes?.size?.toLong()?.times(TxBlob.GAS_PER_BLOB) ?: 0
}

private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): RPCTransaction {
Expand All @@ -69,6 +77,9 @@ private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
var v = -1L
lateinit var r: BigInteger
lateinit var s: BigInteger
var yParity = -1
var blobVersionedHashes: List<Hash>? = null
var blobFeeCap: BigInteger? = null
var otherFields: MutableMap<String, JsonNode>? = null

p.forEachObjectField { field ->
Expand All @@ -92,6 +103,9 @@ private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
"v" -> v = p.readHexLong()
"r" -> r = p.readHexBigInteger()
"s" -> s = p.readHexBigInteger()
"y" -> yParity = p.readHexInt()
"blobVersionedHashes" -> blobVersionedHashes = p.readListOf(Hash::class.java)
"maxFeePerBlobGas" -> blobFeeCap = p.readHexBigInteger()
else -> {
if (otherFields == null) {
otherFields = HashMap()
Expand Down Expand Up @@ -121,6 +135,9 @@ private class RPCTransactionDeserializer : JsonDeserializer<RPCTransaction>() {
v,
r,
s,
yParity,
blobVersionedHashes,
blobFeeCap,
otherFields ?: emptyMap(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ interface Transaction {
val chainId: Long
val accessList: List<AccessList.Item>?
val type: TxType
val blobFeeCap: BigInteger?
val blobVersionedHashes: List<Hash>?
val blobGas: Long
}

/**
Expand All @@ -39,6 +42,21 @@ enum class TxType(val value: Int) {
LEGACY(0x0),
ACCESS_LIST(0x1),
DYNAMIC_FEE(0x2),
BLOB(0x3),
;

companion object {
// optimization to avoid allocating an iterator
fun findOrNull(value: Int): TxType? {
for (i in entries.indices) {
val entry = entries[i]
if (entry.value == value) {
return entry
}
}
return null
}
}
}

object ChainId {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class TransactionSigned @JvmOverloads constructor(
override val hash: Hash
get() {
if (_hash == null) {
_hash = Hash(Hashing.keccak256(toRlp()))
val hashRlp = RlpEncoder().also { rlpEncode(it, true) }.toByteArray()
_hash = Hash(Hashing.keccak256(hashRlp))
}
return _hash!!
}
Expand All @@ -45,7 +46,7 @@ class TransactionSigned @JvmOverloads constructor(
override val from: Address
get() {
if (_from == null) {
if (!hasValidSignature) {
if (!isSignatureValid) {
throw IllegalStateException("Unable to recover sender, invalid signature")
}
}
Expand All @@ -58,7 +59,7 @@ class TransactionSigned @JvmOverloads constructor(
val fromOrNull: Address?
get() {
if (_from == null) {
if (!hasValidSignature) {
if (!isSignatureValid) {
return null
}
}
Expand All @@ -68,8 +69,7 @@ class TransactionSigned @JvmOverloads constructor(
/**
* Check if transaction has a valid signature by trying to recover signer address from signature hash.
*/
@get:JvmName("hasValidSignature")
val hasValidSignature: Boolean
val isSignatureValid: Boolean
get() {
if (_isValidSignature == -1) {
val from = signature.recoverFromHash(tx.signatureHash())
Expand Down Expand Up @@ -102,44 +102,45 @@ class TransactionSigned @JvmOverloads constructor(
return "TransactionSigned(tx=$tx, signature=$signature)"
}

override fun rlpEncode(rlp: RlpEncoder) {
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.value.toByte())
}

rlp.encodeList {
tx.rlpEncodeFields(this)
signature.rlpEncode(this)
// 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)
}
}
}

companion object : RlpDecodable<TransactionSigned> {
@JvmStatic
override fun rlpDecode(rlp: RlpDecoder): TransactionSigned? {
val type = rlp.peekByte().toUByte().toInt()
return when {
type == TxType.ACCESS_LIST.value -> {
rlp.readByte()

rlp.decodeList {
val tx = TxAccessList.rlpDecode(rlp)
val signature = rlp.decode(Signature) ?: return null
TransactionSigned(tx, signature)
}
}

type == TxType.DYNAMIC_FEE.value -> {
rlp.readByte()

rlp.decodeList {
val tx = TxDynamicFee.rlpDecode(rlp)
val signature = rlp.decode(Signature) ?: return null
TransactionSigned(tx, signature)
}
}

type >= 0xc0 -> rlp.decodeList {
// legacy tx
if (type >= 0xc0) {
return rlp.decodeList {
val nonce = rlp.decodeLong()
val gasPrice = rlp.decodeBigInteger() ?: BigInteger.ZERO
val gas = rlp.decodeLong()
Expand All @@ -164,8 +165,58 @@ class TransactionSigned @JvmOverloads constructor(

TransactionSigned(tx, signature)
}
}

return when (TxType.findOrNull(type)) {
TxType.LEGACY -> throw IllegalStateException("Should not happen")
TxType.ACCESS_LIST -> {
rlp.readByte()

rlp.decodeList {
val tx = TxAccessList.rlpDecode(rlp)
val signature = rlp.decode(Signature) ?: return null
TransactionSigned(tx, signature)
}
}

TxType.DYNAMIC_FEE -> {
rlp.readByte()

rlp.decodeList {
val tx = TxDynamicFee.rlpDecode(rlp)
val signature = rlp.decode(Signature) ?: return null
TransactionSigned(tx, signature)
}
}

TxType.BLOB -> {
rlp.readByte()

rlp.decodeList {
val isNetworkEncoding = rlp.isNextElementList()
if (!isNetworkEncoding) {
val tx = TxBlob.rlpDecode(rlp) ?: return null
val signature = rlp.decode(Signature) ?: return null

TransactionSigned(tx, signature)
} else {
// see: https://eips.ethereum.org/EIPS/eip-4844#networking
lateinit var tx: TxBlob
lateinit var signature: Signature
rlp.decodeList {
tx = TxBlob.rlpDecode(rlp) ?: return null
signature = rlp.decode(Signature) ?: return null
}

val sidecar = rlp.decode(TxBlob.Sidecar) ?: return null

// TODO avoid creating a copy just with sidecar
TransactionSigned(tx.copy(sidecar = sidecar), signature)
}
}
}

else -> null
null -> null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,32 @@ sealed interface TransactionUnsigned : Transaction {
*/
fun rlpDecode(rlp: RlpDecoder, chainId: Long): TransactionUnsigned? {
val type = rlp.peekByte().toUByte().toInt()
val ret = when {
type == TxType.ACCESS_LIST.value -> {

// legacy tx
if (type >= 0xc0) {
return rlp.decodeList { TxLegacy.rlpDecode(rlp, chainId).also { dropEmptyRSV() } }
}

return when (TxType.findOrNull(type)) {
TxType.LEGACY -> throw IllegalStateException("Should not happen")

TxType.ACCESS_LIST -> {
rlp.readByte()
rlp.decodeList { TxAccessList.rlpDecode(rlp).also { dropEmptyRSV() } }
}

type == TxType.DYNAMIC_FEE.value -> {
TxType.DYNAMIC_FEE -> {
rlp.readByte()
rlp.decodeList { TxDynamicFee.rlpDecode(rlp).also { dropEmptyRSV() } }
}

type >= 0xc0 -> rlp.decodeList { TxLegacy.rlpDecode(rlp, chainId).also { dropEmptyRSV() } }
else -> null
}
TxType.BLOB -> {
rlp.readByte()
rlp.decodeList { TxBlob.rlpDecode(rlp).also { dropEmptyRSV() } }
}

return ret
null -> null
}
}

// Decode and ignore empty r, s, v signature fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.ethers.core.types.transaction
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.rlp.RlpDecodable
import io.ethers.rlp.RlpDecoder
import io.ethers.rlp.RlpEncoder
Expand Down Expand Up @@ -38,6 +39,15 @@ class TxAccessList(
override val type: TxType
get() = TxType.ACCESS_LIST

override val blobFeeCap: BigInteger?
get() = null

override val blobVersionedHashes: List<Hash>?
get() = null

override val blobGas: Long
get() = 0

override fun rlpEncodeFields(rlp: RlpEncoder) {
rlp.encode(chainId)
rlp.encode(nonce)
Expand Down
Loading

0 comments on commit aea86c7

Please sign in to comment.