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

Add support for raw blob txs in automine mode #341

Closed
fvictorio opened this issue Mar 26, 2024 · 11 comments · Fixed by NomicFoundation/hardhat#5042 or #398
Closed

Add support for raw blob txs in automine mode #341

fvictorio opened this issue Mar 26, 2024 · 11 comments · Fixed by NomicFoundation/hardhat#5042 or #398
Assignees
Milestone

Comments

@fvictorio
Copy link
Member

fvictorio commented Mar 26, 2024

This issue is about adding support for blob transactions in a limited context:

  • Blob transactions sent with the eth_sendRawTransaction method
  • Only when automining
  • Only when the mempool is empty

Definition of done

  • Valid blob transactions can be sent with eth_sendRawTransaction
  • The BLOBHASH opcode can be used to access the blobs in the transaction
  • If eth_sendTransaction is used to send a blob tx, we show an error that includes a link to Add support for blob transactions sent with eth_sendTransaction hardhat#5023
  • If a blob tx is sent and automining is disabled (or it's enabled but the mempool is not empty), we show an error that includes a link to Add support for blob transactions mempool hardhat#5024
  • Blob transactions can be fetched with the eth_getTransaction* methods
  • The block header fields related to EIP-4844 properly reflect the (single) blob transaction (blobGasUsed and excessBlobGas).
  • Add a new eth_blobBaseFee method that returns the current blob base fee.
  • If necessary, the ReplayBlock tool should be updated to take blobs into consideration.

Blob transactions

A blob transaction can have between 1 and 6 blobs. The EIP doesn't forbid blob transactions with 0 blobs AFAICT, but geth rejects them with a "blobless blob transaction" error.

BLOBHASH opcode

The BLOBHASH opcode can be used to read the blob hashes of the transaction that is being executed.

Given this contract:

contract BlobReader {
  bytes32 public blob1;
  bytes32 public blob2;
  bytes32 public blob3;
  bytes32 public blob4;
  bytes32 public blob5;
  bytes32 public blob6;

  function readBlobs() external {
    bytes32 b1;
    bytes32 b2;
    bytes32 b3;
    bytes32 b4;
    bytes32 b5;
    bytes32 b6;

    assembly {
      b1 := blobhash(0)
      b2 := blobhash(1)
      b3 := blobhash(2)
      b4 := blobhash(3)
      b5 := blobhash(4)
      b6 := blobhash(5)
    }

    blob1 = b1;
    blob2 = b2;
    blob3 = b3;
    blob4 = b4;
    blob5 = b5;
    blob6 = b6;
  }
}

whose deployment bytecode is

0x6080604052348015600e575f80fd5b5061021b8061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061007b575f3560e01c80635f5d7b07116100595780635f5d7b07146100c557806380d36689146100e35780639b1e6a8d14610101578063f02cfa011461011f5761007b565b80632069b0c71461007f57806342cd9eeb1461008957806352679165146100a7575b5f80fd5b61008761013d565b005b610091610191565b60405161009e91906101cc565b60405180910390f35b6100af610197565b6040516100bc91906101cc565b60405180910390f35b6100cd61019d565b6040516100da91906101cc565b60405180910390f35b6100eb6101a3565b6040516100f891906101cc565b60405180910390f35b6101096101a9565b60405161011691906101cc565b60405180910390f35b6101276101af565b60405161013491906101cc565b60405180910390f35b5f805f805f805f49955060014994506002499350600349925060044991506005499050855f819055508460018190555083600281905550826003819055508160048190555080600581905550505050505050565b60045481565b60015481565b60025481565b60055481565b60035481565b5f5481565b5f819050919050565b6101c6816101b4565b82525050565b5f6020820190506101df5f8301846101bd565b9291505056fea2646970667358221220ad92504abfd8a29e63e4f3267491d7eb9a8e8b87431974aed1c6f2fe37debea064736f6c63430008190033

Sending a blob transaction to its address with input data 0x2069b0c7 (the selector of readBlobs()) should set the respective storage slots to the blob hashes it received.

If an index that doesn't correspond to a blob is used, the opcode returns 32 bytes of zeroes.

Fetched blob transactions

The result of fething a blob transaction looks like this:

{
  "blockHash": "0x71c34e162580fd9d9957d6531130f2e2b2ca0ccf3a724609827af7d50c24fe6c",
  "blockNumber": "0x10",
  "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
  "gas": "0xf4240",
  "gasPrice": "0x3b9aca00",
  "maxFeePerGas": "0x3b9aca00",
  "maxPriorityFeePerGas": "0x3b9aca00",
  "maxFeePerBlobGas": "0x1",
  "hash": "0x88ea669a2536da5de3213283d4e4eebda8a8b31286b56323e74284fba627e679",
  "input": "0x2069b0c7",
  "nonce": "0xc",
  "to": "0xee1119f70731b8827a4773c1f3c942c687379387",
  "transactionIndex": "0x0",
  "value": "0x0",
  "type": "0x3",
  "accessList": [],
  "chainId": "0x539",
  "blobVersionedHashes": [
    "0x01a91b58b95793e43612612e62f97b7a82d4f51b74453aff3ffa840988ad9d1f"
  ],
  "v": "0x1",
  "r": "0xc221c1a30dcaba796276f4dafee5b36009b94a1f632f18f01b3db06a855e7b21",
  "s": "0x17c26fc381f6c58ae9930c73b7069b40d3a8e3648f2599b88e719d54f454b24d",
  "yParity": "0x1"
}

This is just like an EIP-1559 transaction with two extra fields: maxFeePerBlobGas and blobVersionedHashes.

Fetched receipts

Receipts from blob transactions don't have anything different from receipts from other transaction types.

Block header fields

The two important block header fields related to blob transactions are blobGasUsed and excessBlobGas.

blobGasUsed is simple: it's basically blobs_in_block * 0x20000, because each blob uses $2^{17}$ bytes, each byte costs 1 of gas, and you can't have fractional blobs.

excessBlobGas is a cumulative measure, which is computed like this (taken directly from geth's code, but self-explanatory):

func CalcExcessBlobGas(parentExcessBlobGas uint64, parentBlobGasUsed uint64) uint64 {
	excessBlobGas := parentExcessBlobGas + parentBlobGasUsed
	if excessBlobGas < params.BlobTxTargetBlobGasPerBlock {
		return 0
	}
	return excessBlobGas - params.BlobTxTargetBlobGasPerBlock
}

The target blocb gas per block is something that can change between hardforks, but right now is 0x60000. This means that blocks with 3 or fewer blobs will be under the target. Some examples might clarify this:

  • If the previous block had 0 blobs and the excess blob gas was 0, then it continues to be 0
  • If the previous block had 3 blobs and the excess blob gas was 0, then it continues to be 0
  • If the previous block had 4 blobs and the excess blob gas was 0, then it becomes 0x20000
  • If the previous block had 2 blobs and the excess blob gas was 0xA0000, then it becomes 0x80000
  • If the previous block had 0 blobs and the excess blob gas was 0x60000, then it becomes 0

Blob base fee update

The blob base fee grows exponentially with the excess blob gas in the block. If the excess blob gas is 0 or low, then the base fee is just 1. But as the excess blob gas accumulates, the base fee starts to grow.

The fee is computed using an integer approximation. Here's the typescript code:

const MIN_BLOB_BASE_FEE = 1n;
const BLOB_BASE_FEE_UPDATE_FRACTION = 3338477n;

function get_blob_base_fee(excess_blob_gas: bigint): bigint {
    return fake_exponential(
        MIN_BLOB_BASE_FEE,
        excess_blob_gas,
        BLOB_BASE_FEE_UPDATE_FRACTION
    )
}

function fake_exponential(factor: bigint, numerator: bigint, denominator: bigint): bigint {
  let i = 1n;
  let output = 0n;
  let numerator_accum = factor * denominator;
  while (numerator_accum > 0n) {
    output += numerator_accum;
    numerator_accum = (numerator_accum * numerator) / (denominator * i);
    i += 1n;
  }
  return output / denominator;
}

(Here's geth's version, in case it also helps.)

The constants come from the EIP, and we can take them from REVM's primitives.

BLOBBASEFEE opcode new eth_blobBaseFee method

These two are (hopefully) self-explanatory. In both cases you don't specify the block number; the value is the "current" blob base fee, which means the blob base fee computed from the latest block's fields.

@fvictorio fvictorio self-assigned this Mar 27, 2024
@Wodann Wodann self-assigned this Mar 28, 2024
@Wodann Wodann linked a pull request Mar 28, 2024 that will close this issue
@agostbiro agostbiro reopened this Mar 28, 2024
@fvictorio fvictorio removed their assignment Apr 5, 2024
@fvictorio
Copy link
Member Author

fvictorio commented Apr 5, 2024

Example of a raw blob transaction: blob.json

This was generated with private key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266).

It has a single blob that starts with "hello world", and then it's padded with zeroes. Other parameters of the tx:

  • nonce: 0
  • gas limit: 1_000_000
  • maxFeePerBlobGas: 1
  • maxFeePerGas: 1_000_000_000
  • maxPriorityFeePerGas: 1_000_000_000
  • to: 0x0000000000000000000000000000000000000000
  • data: 0x2069b0c7 (unnecessary, leftover from a previous test)
  • chainId: 1337 (notice that this is not the default 31337, because I tested this in geth)

@Wodann
Copy link
Collaborator

Wodann commented Apr 17, 2024

@fvictorio should eth_feeHistory be updated with additional fields in the result for blob gas?

See execution-spec and geth PR

@fvictorio
Copy link
Member Author

I'd say that can be done as a separate issue. I opened #390 for it.

@Wodann
Copy link
Collaborator

Wodann commented Apr 21, 2024

@fvictorio Is this a blob transaction request or a signed transaction?

Example of a raw blob transaction: blob.json

@fvictorio
Copy link
Member Author

Signed. You should be able to send it with eth_sendRawTransaction.

@Wodann
Copy link
Collaborator

Wodann commented Apr 23, 2024

@fvictorio do you happen to have the blob, commitment, proof, and blob hash for the example?

That'd make my test case easier :)

@fvictorio
Copy link
Member Author

Assuming I didn't make a mistake:
blobData.json

@Wodann
Copy link
Collaborator

Wodann commented Apr 23, 2024

I get this caller address when I recover it from the signature 0x411f95d82f4d0ecf92f9ac23a4ce12df3480a2de. Any idea why they diverge?

@fvictorio
Copy link
Member Author

I tried to recover the address using viem and I also got the wrong address at first (albeit a different one). This is the code I used:

import * as viem from "viem";
import fs from "fs"

const serializedTransaction = JSON.parse(fs.readFileSync("blob.json", "utf8"))

const transaction = viem.parseTransaction(serializedTransaction)

const signature =
  viem.signatureToHex({
    r: transaction.r,
    s: transaction.s,
    v: transaction.v,
    yParity: transaction.yParity,
  })

const serialized = viem.serializeTransaction({
  ...transaction,
  r: undefined,
  s: undefined,
  v: undefined,
  yParity: undefined,
})

console.log(await viem.recoverAddress({
  hash: viem.keccak256(serialized),
  signature,
}))

Then I made this change and got the right address:

diff --git a/index.mjs b/index.mjs
index caa31e4..2283676 100644
--- a/index.mjs
+++ b/index.mjs
@@ -19,6 +19,7 @@ const serialized = viem.serializeTransaction({
   s: undefined,
   v: undefined,
   yParity: undefined,
+  sidecars: undefined,
 })
 
 console.log(await viem.recoverAddress({

Maybe that helps? This means that the fields involved in the hash are:

[
  'blobVersionedHashes',
  'chainId',
  'type',
  'to',
  'gas',
  'data',
  'maxFeePerBlobGas',
  'maxFeePerGas',
  'maxPriorityFeePerGas',
  'value',
  'nonce'
]

which matches what the EIP says.

@fvictorio
Copy link
Member Author

Hash of that tx: 0x9dccf66bda0bd29f3a6fb35808360b041203b28c90236065cd4753cf97cfd5fd

@fvictorio
Copy link
Member Author

Transaction with 4 blobs:
tx-4-blobs.json

Same parameters as in #341 (comment), except that it has four blobs.

@Wodann Wodann added this to the EDR v0.3.8 milestone May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
3 participants