Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add support for eip-4844 blob transactions #3

Merged
merged 4 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading