Blockchain

Deep dive into EIP-4844

Blob Carrying Transactions

작성자: Choi.eth

29 min read

image.png

Intro

이더리움은 베를린 하드포크(2021-04-15) 이후 EIP1559 transaction(EIP-1559)과 Access List Transaction(EIP-2930)를 도입하며, 기존에 사용하던 transaction(Legacy Transaction)과 함께 3가지의 transaction이 메인넷에 도입됬다. 이때, transaction을 전달받는 네트워크가 transaction를 구분하기 위해 EIP-2718를 도입하여 아래와 같이 type을 지정했다.

name type
Legacy Transaction 0x00
Access List Transaction 0x01
EIP-1559 Transaction 0x02
Blob Carrying Transactions 0x03

type은 RLP 인코딩 단계에서 가장 앞단에 배치되며, 네트워크는 이를 통해 transaction을 구분할 수 있게 됬다.그리고 이더리움의 덴쿤 업그레이드(2024-3-13)에서 EIP-4844가 도입되며, type 0x03을 지정받은 Blob Carrying Transactions(이하 Blob Tx)이 새로 등장한다.

What is Blob?

Blob Tx는 다른 트랜잭션과 다르게 "Blob"이라는 새로운 필드(field)가 포함하는 트랜잭션 구성(format)을 갖추고 있다. Blob이란 “다수의 데이터가 압축된 데이터”를 저장하기 위해 암호학적으로 설계된 “저장 공간”이다.

그리고 이더리움은 KZG Polynomial Commitments Scheme(이하 KZG)을 활용하여 Blob에 저장된 “다수의 데이터가 압축된 데이터”“특정 데이터”가 포함되었는지 검증 할 수 있다. 이더리움은 Blob을 롤업 체인들이 제공하는 데이터를 효율적으로 저장하기 위해 새롭게 만든 “저장 공간”이다. 하지만, 롤업 데이터 외에도 데이터를 압축하고 증명이 필요한 경우 언제든지 활용할 수 있다.

Blob Layout

0 <= x < 52435875175126190479447740508185965837690552500527637822603658699938581184513

A blob is a vector of 4096 field elements, numbers within the range
출처 @vbuterin note

_@vbuterin note_에 작성된 내용에 의하면 필드에 넣을 수 있는 데이터(x)는 0 <= x < 5243 …. 4513 범위 내의 데이터만 삽입할 수 있다. 그 이유는 암호학적으로 검증하기 위해 사용하는 KZG가 타원곡선의 종류 중 하나인 BLS12–381를 사용하기 때문이다. 여기서 5243 …. 4513는 BLS12–381의 Field element (또는 modulus)이다.

블롭 가스 수수료 (Blob Gas Fee)

Blob Tx은 가스 수수료(gas fee)외에, Blob라는 저장공간을 사용하기 위한 블롭 가스 수수료(Blob Gas Fee)를 추가로 지불해야 한다. 이때 지불하는 Blob 수수료는 Blob Tx의 수요와 공급에 따라 달라진다.

Constant Value
MAX_BLOB_GAS_PER_BLOCK 786432
TARGET_BLOB_GAS_PER_BLOCK 393216
MIN_BLOB_BASE_FEE 1
BLOB_BASE_FEE_UPDATE_FRACTION 3338477
GAS_PER_BLOB 2**17 (131,072)

출처 EIP-4844 | https://eips.ethereum.org/EIPS/eip-4844#parameters

이더리움은 덴쿤 업데이트를 통해 EIP-4844를 도입했으며, 메인 네트워크에 위와 같은 상수(Constant)를 프로토콜에 추가했다. 그리고 블롭 가스 수수료는 위 상수들을 조합한 수식과 알고리즘에 의해 결정된다. 블롭 가스 수수료(Blob Gas Fee)는 아래와 같이 정리된다.

Blob Gas Fee = 사용한 Blob의 개수 * 블롭 당 가스(Blob Per Gas) * Blob Gas 가격(Blob Base Fee)

각각의 변수가 어떤 의미를 갖는지, 또 어떻게 구할 수 있는지 알아보자.

블롭 당 가스(Blob Per Gas)

https://github.com/ethereum/go-ethereum/blob/e31709db6570e302557a9bccd681034ea0dcc246/params/protocol_params.go#L169

**GAS_PER_BLOB**는 Blob 당 가스(Blob Per Gas)를 정의한 상수이며, 2172^{17}는 131,072이 된다. 위에서 언급한 Blob의 용량(blobSize)인 131,072 bytes와 동일한 값이다. 즉 Blob의 가스는 Blob에 저장하는 데이터 1 bytes당 1 gas가 측정된다. 하지만, 내가 Blob의 4096개 필드 중에서 단 하나만( 32 bytes )만 사용했다고 해서 32 gas의 수수료만 지불할 수 있는건 아니다.

https://github.com/ethereum/go-ethereum/blob/e31709db6570e302557a9bccd681034ea0dcc246/core/state_transition.go#L496

  • len(st.msg.BlobHashes) : msg(=transaction)로 부터 전달받은 BlobHashes의 개수
  • BlobTxBlobGasPerBlob : 1 << 17 (=131,072)

여기서 BlobHashes는 4096개의 데이터를 하나로 압축한 해시값을 의미한다. 이때 BlobHashes는 4096개 필드 중 하나만 사용하더라도 0으로 채워진 4095개 필드를 같이 압축한다. 그렇게 만들어진 blobHash가 한개(1 Blob)일 경우 len(st.msg.BlobHashes)는 1이 되며, Blob Tx 발신자는 총 Blob Gas 131,072 만큼의 수수료를 지불해야한다

즉, Geth에서 Blob Tx에서 사용한 Blob Gas를 return하는 함수 _blobGasUsed()_는 사용한 블롭 개수 * 131,072를 리턴한다.

만약, Blob 1개로 부족하여 두번째 Blob을 사용한다면 len(st.msg.BlobHashes)는 2가 될것이고, 두번째 Blob 또한 131,072 bytes를 모두 사용하지 않는 경우에도 131,072 bytes의 블롭 공간에 대한 비용을 지불해야 하니, Blob Tx 발신자는 총 Blob Gas 262,144 (2 * 131,072) 만큼의 수수료를 지불해야한다

위 내용을 한줄로 정리하자면 결국, Blob 당 가스(Blob Per Gas)는 131,072(GAS_PER_BLOB)로 고정이다.

블록 당 사용할 수 있는 블롭 가스 (Block Per Blob Gas)

Blob Tx는 블록 생성자에 의해 블록에 실리게 된다. 이때, 이더리움은 블록 당 사용할 수 있는 블롭 가스(Block Per Blob Gas)를 가변적으로 설정하여 기대값으로 3개 ( ≈ 0.375MB), 그리고 네트워크 혼잡도에 따라 최대값 6개( ≈ 0.75MB)까지 Blob을 하나의 블록에 저장 할 수 있도록 설계했다

TARGET_BLOB_GAS_PER_BLOCK은 블록 당 사용할 수 있는 Blob Gas의 “기대값”을 정의한 상수이며, Blob 3개의 Blob Gas 값인 393,216 (131,072 * 3)가 정의 되어 있다. 그리고 MAX_BLOB_GAS_PER_BLOCK는 네트워크 혼잡도에 따라 블록 당 최대로 확장할 수 있는 Blob Gas “최대값”을 정의한 상수이며, Blob 6개의 Blob Gas 값인 786,432 (131,072 * 6)가 정의 되어 있다.

https://github.com/ethereum/go-ethereum/blob/e31709db6570e302557a9bccd681034ea0dcc246/core/txpool/validation.go#L132

geth에서 txpool에 담긴 트랜잭션을 검증하는 _ValidateTransaction()_의 BlobTxType 부분을 보면 Blob Tx에 담긴 BlobHash.length, 즉 Blob의 개수가 최대값인 6개 보다 크면 “too many blobs in transaction” 애러를 return 하는걸 볼 수 있다. 추가로 BlobHash.length가 0, 즉 Blob이 하나도 없는 경우에도 "blobless blob transaction” 애러를 return 한다. EIP4844의 “Throughput”에 의하면 기대값과 최대값은 EIP4844의 도입 후, 블록의 크기가 갑작스럽게 커지는 상황으로 인해 발생하는 네트워크의 부담을 최소화하기 위함이다.

하지만, 기대값과 최대값의 핵심은 네트워크 상황에 따라 Blob Gas 가격(Blob Base Fee)을 유동적으로 결정해주는데 기여한다.

Blob Base Fee

블롭 가스 수수료는 Blob 당 가스(Blob Per Gas) * Blob Base Fee 수식에 의해 결정된다. EIP-4844 표준의 “Blob base fee update rule”에 의하면 Blob Base Fee는 아래 수식에 의해 결정된다.

blob_base_fee=MIN_BLOB_BASE_FEE×eexcess_blob_gasBLOB_BASE_FEE_UPDATE_FRACTION\mathrm{blob\_base\_fee} = \mathrm{MIN\_BLOB\_BASE\_FEE} \times e^{ \frac{ \mathrm{excess\_blob\_gas} }{ \mathrm{BLOB\_BASE\_FEE\_UPDATE\_FRACTION} } }
  • MIN_BLOB_BASE_FEE : 1
  • BLOB_BASE_FEE_UPDATE_FRACTION : 333,8477
  • e : ≈ 2.71828

MIN_BLOB_BASE_FEEBLOB_BASE_FEE_UPDATE_FRACTION는 프로토콜에서 정한 상수이다. 결국 Blob Base Fee는 상수 값을 제외하면 excess_blob_gas 값의 영향을 받아 결정된다.

https://github.com/ethereum/go-ethereum/blob/e31709db6570e302557a9bccd681034ea0dcc246/core/types/block.go#L65

EIP4844가 도입되면서, 기존의 Block Header 구성요소에 **blob_gas_usedexcess_blob_gas**가 추가되었다.

  • blob_gas_used : 해당 블록에서 Blob이 사용한 총 가스량
  • excess_blob_gas : 이전 블록까지 누적된 Blob의 수가 기댓값에 비해 초과한 가스량

여기서 우리가 Blob Base Fee를 구하기 위해 주목해야하는 변수는 **excess_blob_gas**이다.

![- Geth Permalink

geth에서 **excess_blob_gas**을 계산하는 _CalcExcessBlobGas()_을 봤을 때, 두가지 상황을 고려할 수 있다.

  1. “이전 블록에서 초과 사용된 Blob Gas + 현재 블록에서 사용된 Blob Gas”의 합이 BlobTxTargetBlobGasPerBlock보다 작으면,excess_blob_gas = 0으로 설정한다.
  2. 이전 블록에서 초과 사용된 Blob Gas + 현재 블록에서 사용된 Blob Gas의 합이 BlobTxTargetBlobGasPerBlock보다 크면, excess_blob_gas"이전 블록에서 초과 사용된 Blob Gas + 현재 블록에서 사용된 Blob Gas"의 합에서 BlobTxTargetBlobGasPerBlock을 뺀 값으로 설정한다.

이렇게 구한 excess_blob_gas은 Block Header에 저장되어 다음 블록에 실리는 Blob Tx의 Blob Base Fee를 결정하는데 사용된다.

https://dune.com/ncitron/blob-fee-market

이렇게 구한 excess_blob_gas은 Block Header에 저장되어 다음 블록에 실리는 Blob Tx의 Blob Base Fee를 결정하는데 사용된다.

이처럼 이더리움이 생각하는 기댓값과 최대값을 통해 Blob Base Fee를 구해봤을 때, 가장 이상적인 상황으로 블록에 Blob이 3개 이하가 포함된다면, 블록 당 Blob Base Fee는 1 wei을 유지할 것이다. 하지만 트랜잭션이 네트워크에 몰리면서 블록에 수용할 수 있는 Blob의 최대값인 6개가 포함된다면, 블록 당 Blob Base Fee는 최대 ≈ 1.125 wei로 오를 것이다.

Blob Carrying Transactions

Blob Carrying Transactions의 Payload는 아래와 같다.

{
  chain_id: "0x01",
  nonce: "0x00",
  max_priority_fee_per_gas: "0x00",
  max_fee_per_gas: "0x00",
  gas_limit: "0x00",
  to: "0xcccccccccccccccccccccccccccccccccccccccc",
  value: "0x0186a0",
  data: "0x",
  access_list: [],
  max_fee_per_blob_gas: "0xfff",
  blob_versioned_hashes:[],
  y_parity: "0x01",
  r: "0xafb6e247b1c490e284053c87ab5f6b59e219d51f743f7a4d83e400782bc7e4b9",
  s: "0x479a268e0e0acd4de3f1e28e4fac2a6b32a4195e8dfa9d19147abe8807aa6f64"
}

// 실제 사용하지 않는 예시 데이터입니다

출처 eip-4844 Blob transaction | https://eips.ethereum.org/EIPS/eip-4844#blob-transaction

기존의 eip-1559 transaction payload에서 max_fee_per_blob_gasblob_versioned_hashes가 추가 된 모습니다. 이는 트랜잭션 발신자가 기존의 트랜잭션의 역할을 그대로 수행하면서 동시에 Blob을 사용 할 수 있도록 하기 위함이다.

다른 트랜잭션과 차이점이 딱 하나 있다. Blob Tx는 “to” 필드를 비울 수 없다. 기존의 트랜잭션에서 새로운 스마트 컨트랙트를 배포하는 경우 “to” 필드를 비워서 네트워크로 전송했다. 즉, Blob Tx은 스마트 컨트랙트 배포를 수행하는 트랜잭션으로 사용할 수 없다는 뜻이다.

max_fee_per_blob_gas

max_fee_per_blob_gas는 최대로 지불할 수 있는 Blob Gas 가격(Blob Base Fee)를 의미한다. 위에서 다뤘던것 처럼 Blob Base Fee는 네트워크 상황에 따라 변동된다. 이때 Blob Tx 발신자는 최대로 지불할 수 있는 Blob Base Fee를 max_fee_per_blob_gas에 작성하여 네트워크로 전송할 수 있다. 이렇게 되면, 블록 검증자는 현재 Blob Base Fee가 발신자가 작성한 max_fee_per_blob_gas 보다 큰 경우, 블록에 실지 않는다.

blob_versioned_hashes

blob_versioned_hashes는 네트워크에 저장하는 blob의 versioned_hash 리스트을 의미한다. Blob은 하나당 32 bytes의 용량를 갖는 4096개의 필드로 구성되어 있다. 그리고 KZG로 이를 압축하면 48 bytes의 데이터(commitment)로 변환된다. 압축된 commitment를 아래와 같은 과정으로 전처리되어 versioned_hash가 된다.

versioned_hash = “0x01” + SHA256(commitment)[1:0]

여기서 “0x01”은 이더리움에서 사용하는 hash의 버전을 의미하며 eip4844의 Parameters에 VERSIONED_HASH_VERSION_KZG 변수로 정의되어 있다. 즉, **versioned_hash**는 VERSIONED_HASH_VERSION_KZG(0x01)과 commitment을 SHA256 함수를 통해 32 bytes로 변환한 후, 마지막 31 bytes를 합친 데이터이다.

위 과정으로 봤을 때, **versioned_hash**는 Blob 한개당 1개가 만들어진다. blob_versioned_hashes는 배열(list)의 데이터 타입이다. Blob Tx로 전달하는 Blob이 1개 이상이라면, 다수의 **versioned_hash**은 blob_versioned_hashes에 전부 담아 전달된다.|

참고로, 부타린이 작성한 Proto-Danksharding FAQ에 의하면 commitment을 SHA256으로 한번 더 해시 연산을 해준 이유는, 48 bytes의 commitment를 32 bytes로 변환시켜, 이더리움 네트워크에 최적화된 EVM 연산과의 호환성을 높이기 위해서다.

VERSIONED_HASH_VERSION_KZG라는 버전 필드를 추가해준 것도 KZG 외 다른 방식을 사용하는 추후의 호환성을 위함이다.

Signature y_parity, r, and s

이더리움의 트랜잭션은 소유권을 증명하기 위해 본인의 개인키로 만든 secp256k1 서명 데이터를 트랜잭션 payload에 담아 전달한다. Blob Tx의 서명 데이터는 아래와 같이 정해진 구조의 데이터를 secp256k1로 서명하여 만들어진다.

keccak256(BLOB_TX_TYPE || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, max_fee_per_blob_gas, blob_versioned_hashes]))
keccak256(
	BLOB_TX_TYPE || 
	rlp([chain_id, 
			 nonce, 
			 max_priority_fee_per_gas, 
			 max_fee_per_gas, 
			 gas_limit, 
			 to, 
			 value, 
			 data, 
			 access_list, 
			 max_fee_per_blob_gas, 
			 blob_versioned_hashes])
			 )

여기서 BLOB_TX_TYPE는 이 글의 Intro 단계에서 설명했던 트랜잭션 type을 의미하며, Blob Tx는 0x03을 갖고 있다.

서명을 위한 원본 데이터는 BLOB_TX_TYPE(0x03)와 서명 데이터를 제외한 Payload의 데이터를 순서대로 나열하여 RLP Encoding한 데이터를 합친 후, keccak256 해시 처리하여 만든다. 그리고, 그렇게 만들어진 데이터를 secp256k1를 통해 연산하면 y_parity, r, s 서명 데이터가 만들어진다.

Network Wrapper (Blob Carrying) Transactions

Network Wrapper (Blob Carrying) Transactions

Network Wrapper (Blob Carrying) Transactions(이하 network transactions )은 네트워크에 전송하기전 마지막으로 가공한 상태의 트랜잭션를 의미한다. 기존의 트랜잭션들은 만들어진 Payload을 정해진 순서에 따라 RLP Encoding하면 블록체인 네트워크가 수용하는 완성된 트랜잭션이 된다. 하지만, Blob Tx는 RLP Encoding에 Payload와 더불어, 3가지 필드 blobs, commitments, proofs을 추가하여 RLP Encoding 을 해야한다. 최종적으로 완성되는 Blob Carrying Transactions은 아래와 같다.

Blob Carrying Transactions = rlp([payload, blobs[], commitments[], proofs[]])

지금까지 payload를 알아 봤으니, blobs[], commitments[], proofs[]에 대해 알아보자.

Blob Sidecars : blobs, commitments and proofs

**blobs**은 원본 데이터, 즉 32 bytes의 용량를 갖는 4096개의 필드를 하나로 이어 붙힌 데이터이다.

**Commitment**는 KZG 암호학을 사용하여 48 bytes 크기의 blob 원본 데이터를 압축한 데이터이다.

프로토 댕크샤딩에서는 문제가 없는 내용이지만, 나중에 댕크샤딩이 도입되면 이더리움은 blob의 Data Avail 문제를 해결하기 위해, 모든 노드가 blob 원본 데이터를 갖고 있지 않게 될 예정이다. 이때, blob의 일부와 Commitment만 보관하는 노드 입장에서 갖고 있는 blob의 일부가 Commitment에 압축된 4096개의 데이터에 포함되는지, 전체 blob의 갖고 있지 않는한 알 수 없다.’

때문에, 트랜잭션 작성자는 이더리움이 수용하는 Commitment가 올바른 데이터인지 암호학적으로 검증할 수 있도록 Challenge라는 과정을 통해 **proofs**(48 bytes)를 만들어 같이 전달해야한다. 그리고, 이더리움은 전달받은 blobs, commitments, proofs을 통해 Open라는 과정을 진행하여 검증을 수행한다.

blobs, commitments, proofs은 Blob당 하나씩 생성된다. blobs, commitments, proofs는 리스트의 형태(배열)이니 ,만약 Blob Tx에 Blob이 1개 이상이라면 Blob 개수에 맞춰 blobs, commitments, proofs를 만들어 리스트에 순서대로 넣어주면 된다. blob 정보가 담긴 network transactions은 위원회(블록 생성자와 검증자)에 의해 검증이 진행되며, 이때 blobs, commitments, proofs“Sidecar”라는 객체에 담겨 비콘체인에 저장된다.

Sidecar는 블롭을 “일정 기간 동안 보관한다.”는 정책으로 구현되어 있다. 때문에, Blob의 원본이 저장된 Sidecar는 4096 epoch, 약 18일 동안만 조회가 가능하며, 기간은 EIP-4844의 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS에 정의되어 있다.

Reference

Keep reading

모두 보기