# [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) point evaluation precompile
**Question: What if we could implement the precompile in EVM?**
This lead to trying to write code in etk and Yul, but quickly turning to Solidity after realising it is more complex interacting with EIP-2537. While writing, found a number of questions which are not immediately clear from the spec.
(Incomplete) implementation: https://github.com/axic/4844-contracts
Notes:
- the `assert` statements cause OOG or just abort execution with failure? (asked [previously here](https://ethereum-magicians.org/t/eip-4844-shard-blob-transactions/8430/20))
- currently the precompile seem to allow trailing bytes in the input, should it?
- does it reject short inputs? should it?
- `hash()` is actually sha256, but not mentioned in EIP-4844 ([change PR](https://github.com/ethereum/EIPs/pull/6345))
- `commitment` and `kzg_proof` as values are serialised g1 field elements (some specs: [1](https://docs.rs/bls12_381/0.1.1/bls12_381/notes/serialization/index.html) [2](https://github.com/pairingwg/bls_standard/issues/16) [3](https://github.com/ethereum/consensus-specs/blob/2c34a9734118cca2e0be6c727252c82b4ae82028/specs/bls_verify.md) [4](https://github.com/ethereum/consensus-specs/issues/184))
- `z` and `y` are little endian encoded
- the validation steps the precompile is supposed to do (some implicitly via the helpers):
- (top 3-bits are `flags`, the other 381-bits is the `value`)
- `bytes_to_bls_field` rejects values `>= BLS_MODULUS` (this affects `z` and `x` inputs of the precompile) (this is mentioned in the EIP)
- `commitment` and `kzg_proof` is validated against the bls12-381 deserialisation algo
- bit 0 ("compressed") must be set
- if bit 1 ("infinity") is set, the value as well as bit 3 must be zero
- if bit 2 ("y negative") is set, the y coordinate must be flipped
- pricing of 50000 gas seems low compared to [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) prices:
- Needs 1x g2_mul (45000 gas), 1x g2_add (800), 1x g1_mul (12000 gas), 1x g1_add (500 gas), 2 point pairing (43000*k+65000 => 151000)
- Furthermore `decompress_G1` must be manually implemented, plus hashing + data/stack shuffling.
- The parts supported in EIP-2537 alone add up to 209300 gas
- This may be partially explained with the potential for keeping points in the same form (e.g. affine vs jacobian, montgomery reduction context, etc.) and less need to serialise between precompile interfaces vs. internal representation (but still feels like a deep discount)
- EIP-2537 is missing functions for g1/g2 deserialisation, and these are expensive to implement on EVM. Short sketch (readable implementation [here](https://github.com/ethereum/py_ecc/blob/v6.0.0/py_ecc/bls/point_compression.py#L77-L116)):
```
bit 0: compressed flag (set means compressed)
bit 1: point at inifinity flag
bit 2: negative y coordinate (is this similar to flipped coordinates?)
bit 3..384: the field element
Below is modulo BLS_MODULUS:
x = fe_from_be(value) // read BE-encoded field element
y = x*x*x+b
y = sqrt(y) // also check that sqrt(y)*sqrt(y)==y
if isNegative(y) == bit_2:
y = q - y
```
Current conclusion:
1. Hashing using sha256 instead keccak256 adds an overhead in the EVM.
2. Need [evmmax](https://ethereum-magicians.org/t/eip-5843-evm-modular-arithmetic-extensions/12425) or EIP-2537 extended to support g1/g2 deserialisation.
3. Little endian ordering adds a slight overhead in EVM. (Mentioned [previously here](https://ethereum-magicians.org/t/eip-4844-shard-blob-transactions/8430/19))
4. Edge cases / validity rules of the precompile should be reviewed.
5. Proposed gas costs of the precompile should be reviewed.
6. These validation requirements are good candidates for creating tests.
### Combined reference code
Sources from EIP and `consensus-specs`.
```python
# FIELD_ELEMENTS_PER_BLOB 4096
# BLS_MODULUS 52435875175126190479447740508185965837690552500527637822603658699938581184513
# BLOB_COMMITMENT_VERSION_KZG Bytes1(0x01)
# ENDIANNESS 'little'
# hash ?? (assume sha256)
#
# BLSFieldElement uint256 x < BLS_MODULUS
# KZGCommitment Bytes48 Same as BLS standard “is valid pubkey” check but also allows 0x00..00 for point-at-infinity
# KZGProof Bytes48 Same as for KZGCommitment
def kzg_to_versioned_hash(kzg: KZGCommitment) -> VersionedHash:
return BLOB_COMMITMENT_VERSION_KZG + hash(kzg)[1:]
def bytes_to_bls_field(b: Bytes32) -> BLSFieldElement:
"""
Convert 32-byte value to a BLS scalar field element.
This function does not accept inputs greater than the BLS modulus.
"""
field_element = int.from_bytes(b, ENDIANNESS)
assert field_element < BLS_MODULUS
return BLSFieldElement(field_element)
def verify_kzg_proof(polynomial_kzg: KZGCommitment,
z: Bytes32,
y: Bytes32,
kzg_proof: KZGProof) -> bool:
"""
Verify KZG proof that ``p(z) == y`` where ``p(z)`` is the polynomial represented by ``polynomial_kzg``.
Receives inputs as bytes.
Public method.
"""
return verify_kzg_proof_impl(polynomial_kzg, bytes_to_bls_field(z), bytes_to_bls_field(y), kzg_proof)
def verify_kzg_proof_impl(polynomial_kzg: KZGCommitment,
z: BLSFieldElement,
y: BLSFieldElement,
kzg_proof: KZGProof) -> bool:
"""
Verify KZG proof that ``p(z) == y`` where ``p(z)`` is the polynomial represented by ``polynomial_kzg``.
"""
# Verify: P - y = Q * (X - z)
X_minus_z = bls.add(bls.bytes96_to_G2(KZG_SETUP_G2[1]), bls.multiply(bls.G2, BLS_MODULUS - z))
P_minus_y = bls.add(bls.bytes48_to_G1(polynomial_kzg), bls.multiply(bls.G1, BLS_MODULUS - y))
# Checks that pairing equals FQ12.one
return bls.pairing_check([
[P_minus_y, bls.neg(bls.G2)],
[bls.bytes48_to_G1(kzg_proof), X_minus_z]
])
# aliased as bytes48_to_G1
def pubkey_to_G1(pubkey: BLSPubkey) -> G1Uncompressed:
z = os2ip(pubkey) # bigendian
return decompress_G1(G1Compressed(z))
def point_evaluation_precompile(input: Bytes) -> Bytes:
"""
Verify p(z) = y given commitment that corresponds to the polynomial p(x) and a KZG proof.
Also verify that the provided commitment matches the provided versioned_hash.
"""
# The data is encoded as follows: versioned_hash | z | y | commitment | proof |
versioned_hash = input[:32]
z = input[32:64]
y = input[64:96]
commitment = input[96:144]
kzg_proof = input[144:192]
# Verify commitment matches versioned_hash
assert kzg_to_versioned_hash(commitment) == versioned_hash
# Verify KZG proof
assert verify_kzg_proof(commitment, z, y, kzg_proof)
# Return FIELD_ELEMENTS_PER_BLOB and BLS_MODULUS as padded 32 byte big endian values
return Bytes(U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + U256(BLS_MODULUS).to_be_bytes32())
```
`verify_kzg_proof` can be simplified by precalculating some fields:
```python
def verify_kzg_proof_impl(polynomial_kzg: KZGCommitment,
z: BLSFieldElement,
y: BLSFieldElement,
kzg_proof: KZGProof) -> bool:
"""
Verify KZG proof that ``p(z) == y`` where ``p(z)`` is the polynomial represented by ``polynomial_kzg``.
"""
# Verify: P - y = Q * (X - z)
X_minus_z = bls.add(KZG_SETUP_G2, bls.multiply(bls.G2, BLS_MODULUS - z))
P_minus_y = bls.add(bls.bytes48_to_G1(polynomial_kzg), bls.multiply(bls.G1, BLS_MODULUS - y))
# Checks that pairing equals FQ12.one
return bls.pairing_check([
[P_minus_y, bls.G2_NEG],
[bls.bytes48_to_G1(kzg_proof), X_minus_z]
])
```