Skip to content

Commit

Permalink
feat(core): add support for eip-4844 blob transactions (#3)
Browse files Browse the repository at this point in the history
* feat(core): add support for eip-4844 blob transactions

* chore(core): make TxBlob.Sidecar a data class

* feat(core): add "blobGasUsed" and "excessBlobGas" as Block fields

* simplify some code
  • Loading branch information
ArtificialPB authored Jan 1, 2024
1 parent dcba89b commit d8098d9
Show file tree
Hide file tree
Showing 17 changed files with 620 additions and 40 deletions.
22 changes: 22 additions & 0 deletions ethers-core/src/main/kotlin/io/ethers/core/types/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ data class BlockWithHashes(
override val uncles: List<Hash>,
override val withdrawals: List<Withdrawal>?,
override val withdrawalsRoot: Hash?,
override val blobGasUsed: Long,
override val excessBlobGas: Long,
override val otherFields: Map<String, JsonNode> = emptyMap(),
) : Block<Hash>

Expand Down Expand Up @@ -71,6 +73,8 @@ data class BlockWithTransactions(
override val uncles: List<Hash>,
override val withdrawals: List<Withdrawal>?,
override val withdrawalsRoot: Hash?,
override val blobGasUsed: Long,
override val excessBlobGas: Long,
override val otherFields: Map<String, JsonNode> = emptyMap(),
) : Block<RPCTransaction>

Expand Down Expand Up @@ -98,6 +102,8 @@ interface Block<T> {
val uncles: List<Hash>
val withdrawals: List<Withdrawal>?
val withdrawalsRoot: Hash?
val blobGasUsed: Long
val excessBlobGas: Long
val otherFields: Map<String, JsonNode>
}

Expand Down Expand Up @@ -141,6 +147,8 @@ private class BlockWithHashesDeserializer : GenericBlockDeserializer<Hash, Block
uncles: List<Hash>,
withdrawals: List<Withdrawal>?,
withdrawalsRoot: Hash?,
blobGasUsed: Long,
excessBlobGas: Long,
otherFields: Map<String, JsonNode>,
): BlockWithHashes {
return BlockWithHashes(
Expand All @@ -167,6 +175,8 @@ private class BlockWithHashesDeserializer : GenericBlockDeserializer<Hash, Block
uncles,
withdrawals,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
otherFields,
)
}
Expand Down Expand Up @@ -201,6 +211,8 @@ private class BlockWithTransactionDeserialize : GenericBlockDeserializer<RPCTran
uncles: List<Hash>,
withdrawals: List<Withdrawal>?,
withdrawalsRoot: Hash?,
blobGasUsed: Long,
excessBlobGas: Long,
otherFields: Map<String, JsonNode>,
): BlockWithTransactions {
return BlockWithTransactions(
Expand All @@ -227,6 +239,8 @@ private class BlockWithTransactionDeserialize : GenericBlockDeserializer<RPCTran
uncles,
withdrawals,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
otherFields,
)
}
Expand Down Expand Up @@ -261,6 +275,8 @@ private abstract class GenericBlockDeserializer<TX, T : Block<TX>> : JsonDeseria
var uncles: List<Hash>? = null
var withdrawals: List<Withdrawal>? = null
var withdrawalsRoot: Hash? = null
var blobGasUsed: Long = -1L
var excessBlobGas: Long = -1L
var otherFields: MutableMap<String, JsonNode>? = null

p.forEachObjectField { field ->
Expand Down Expand Up @@ -291,6 +307,8 @@ private abstract class GenericBlockDeserializer<TX, T : Block<TX>> : JsonDeseria
"uncles" -> uncles = p.readOrNull { readListOfHashes() }
"withdrawals" -> withdrawals = p.readOrNull { readListOf(Withdrawal::class.java) }
"withdrawalsRoot" -> withdrawalsRoot = p.readOrNull { readHash() }
"blobGasUsed" -> blobGasUsed = p.readHexLong()
"excessBlobGas" -> excessBlobGas = p.readHexLong()
else -> {
if (otherFields == null) {
otherFields = HashMap()
Expand Down Expand Up @@ -324,6 +342,8 @@ private abstract class GenericBlockDeserializer<TX, T : Block<TX>> : JsonDeseria
uncles ?: emptyList(),
withdrawals,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
otherFields ?: emptyMap(),
)
}
Expand Down Expand Up @@ -354,6 +374,8 @@ private abstract class GenericBlockDeserializer<TX, T : Block<TX>> : JsonDeseria
uncles: List<Hash>,
withdrawals: List<Withdrawal>?,
withdrawalsRoot: Hash?,
blobGasUsed: Long,
excessBlobGas: Long,
otherFields: Map<String, JsonNode>,
): T
}
Expand Down
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) {
// 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 {
val tx = TxBlob.rlpDecode(rlp) ?: return null
val signature = rlp.decode(Signature) ?: return null

TransactionSigned(tx, signature)
}
}
}

else -> null
null -> null
}
}
}
Expand Down
Loading

0 comments on commit d8098d9

Please sign in to comment.