Commit 44201560 authored by Inphi's avatar Inphi Committed by GitHub

op-program: Generic precompile oracle (#9699)

* op-program: Generic precompile oracle

The generic precompile oracle replaces the point evaluation precompile
oracle. The new oracle can be used to retrieve the return data and
status of any precompile, including bn256Pairing and ecrecover.

* op-challenger: Use generic precompile oracle

Replace the KZG point evaluation oracle with a generic precompile oracle

* op-program: Simplify required gas logic.
Use FromHex instead of Hex2Bytes

* op-program: Set beacon URL when capturing and verifying sepolia chain data

* op-program: Actually use the beacon URL

* op-program: Use default l1-rpckind if not set rather than overriding to debug_geth

* Use freshly generated sepolia test data

* Use sepolia compatibility data

* Fix spelling

* update PreimageOracle.sol snapshot

---------
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>
parent e983f527
......@@ -257,8 +257,8 @@ func Run(ctx *cli.Context) error {
stopAtPreimageTypeByte = preimage.Sha256KeyType
case "blob":
stopAtPreimageTypeByte = preimage.BlobKeyType
case "kzg-point-evaluation":
stopAtPreimageTypeByte = preimage.KZGPointEvaluationKeyType
case "precompile":
stopAtPreimageTypeByte = preimage.PrecompileKeyType
case "any":
stopAtAnyPreimage = true
case "":
......
......@@ -52,9 +52,14 @@ type Addresses struct {
}
func NewEVMEnv(contracts *Contracts, addrs *Addresses) (*vm.EVM, *state.StateDB) {
chainCfg := params.MainnetChainConfig
offsetBlocks := uint64(1000) // blocks after shanghai fork
bc := &testChain{startTime: *chainCfg.ShanghaiTime + offsetBlocks*12}
// Temporary hack until Cancun is activated on mainnet
cpy := *params.MainnetChainConfig
chainCfg := &cpy // don't modify the global chain config
// Activate Cancun for EIP-4844 KZG point evaluation precompile
cancunActivation := *chainCfg.ShanghaiTime + 10
chainCfg.CancunTime = &cancunActivation
offsetBlocks := uint64(1000) // blocks after cancun fork
bc := &testChain{startTime: *chainCfg.CancunTime + offsetBlocks*12}
header := bc.GetHeader(common.Hash{}, 17034870+offsetBlocks)
db := rawdb.NewMemoryDatabase()
statedb := state.NewDatabase(db)
......
......@@ -136,15 +136,18 @@ func encodePreimageOracleInput(t *testing.T, wit *StepWitness, localContext Loca
wit.PreimageValue[8:])
require.NoError(t, err)
return input, nil
case preimage.KZGPointEvaluationKeyType:
case preimage.PrecompileKeyType:
if localOracle == nil {
return nil, fmt.Errorf("local oracle is required for point evaluation preimages")
return nil, fmt.Errorf("local oracle is required for precompile preimages")
}
preimage := localOracle.GetPreimage(preimage.Keccak256Key(wit.PreimageKey).PreimageKey())
precompile := common.BytesToAddress(preimage[:20])
callInput := preimage[20:]
input, err := preimageAbi.Pack(
"loadKZGPointEvaluationPreimagePart",
"loadPrecompilePreimagePart",
new(big.Int).SetUint64(uint64(wit.PreimageOffset)),
preimage,
precompile,
callInput,
)
require.NoError(t, err)
return input, nil
......@@ -199,8 +202,8 @@ func TestEVM(t *testing.T) {
// verify the post-state matches.
// TODO: maybe more readable to decode the evmPost state, and do attribute-wise comparison.
goPost := goState.state.EncodeWitness()
require.Equal(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM")
require.Equalf(t, hexutil.Bytes(goPost).String(), hexutil.Bytes(evmPost).String(),
"mipsevm produced different state than EVM at step %d", state.Step)
}
if exitGroup {
require.NotEqual(t, uint32(endAddr), goState.state.PC, "must not reach end")
......
......@@ -5,35 +5,36 @@
.ent test
# load hash at 0x30001000
# 0x47173285 a8d7341e 5e972fc6 77286384 f802f8ef 42a5ec5f 03bbfa25 4cb01fad = keccak("hello world")
# 0x06173285 a8d7341e 5e972fc6 77286384 f802f8ef 42a5ec5f 03bbfa25 4cb01fad = keccak("hello world").key (kzg)
# point evaluation precompile input - 01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a
# 0x0a44472c cb798bc5 954fc466 e6ee2c31 e1ca8a87 d000966c 629d679a 4a29921f = keccak(address(0xa) ++ precompile_input)
# 0x0644472c cb798bc5 954fc466 e6ee2c31 e1ca8a87 d000966c 629d679a 4a29921f = keccak(address(0xa) ++ precompile_input).key (precompile)
test:
lui $s0, 0x3000
ori $s0, 0x1000
lui $t0, 0x0617
ori $t0, 0x3285
lui $t0, 0x0644
ori $t0, 0x472c
sw $t0, 0($s0)
lui $t0, 0xa8d7
ori $t0, 0x341e
lui $t0, 0xcb79
ori $t0, 0x8bc5
sw $t0, 4($s0)
lui $t0, 0x5e97
ori $t0, 0x2fc6
lui $t0, 0x954f
ori $t0, 0xc466
sw $t0, 8($s0)
lui $t0, 0x7728
ori $t0, 0x6384
lui $t0, 0xe6ee
ori $t0, 0x2c31
sw $t0, 0xc($s0)
lui $t0, 0xf802
ori $t0, 0xf8ef
lui $t0, 0xe1ca
ori $t0, 0x8a87
sw $t0, 0x10($s0)
lui $t0, 0x42a5
ori $t0, 0xec5f
lui $t0, 0xd000
ori $t0, 0x966c
sw $t0, 0x14($s0)
lui $t0, 0x03bb
ori $t0, 0xfa25
lui $t0, 0x629d
ori $t0, 0x679a
sw $t0, 0x18($s0)
lui $t0, 0x4cb0
ori $t0, 0x1fad
lui $t0, 0x4a29
ori $t0, 0x921f
sw $t0, 0x1c($s0)
# preimage request - write(fdPreimageWrite, preimageData, 32)
......@@ -59,7 +60,7 @@ $writeloop:
li $a1, 0x31000004
li $v0, 4003
syscall
# read the 1 byte preimage data
# read the 1 byte precompile status and 3 bytes of return data
li $a1, 0x31000008
li $v0, 4003
syscall
......@@ -71,13 +72,21 @@ $writeloop:
sltiu $t6, $t0, 1
li $s1, 0x31000004
lw $t0, 0($s1)
# len should be 1
li $t4, 1
# should be 1 + len(blobPrecompileReturnValue) = 65
li $t4, 65
subu $t5, $t0, $t4
sltiu $v0, $t5, 1
and $v0, $v0, $t6
# most likely broken. need to check pc for exact case
# data at 0x31000008
# first byte is 01 status. Next 3 bytes are 0
lw $t0, 4($s1)
lui $t4, 0x0100
ori $t4, 0x0000
subu $t5, $t0, $t4
sltiu $v1, $t5, 1
and $v0, $v0, $v1
# save results
lui $s0, 0xbfff # Load the base address 0xbffffff0
ori $s0, 0xfff0
......
......@@ -267,18 +267,19 @@ func staticOracle(t *testing.T, preimageData []byte) *testOracle {
}
}
func staticPrecompileOracle(t *testing.T, preimageData []byte, result []byte) *testOracle {
func staticPrecompileOracle(t *testing.T, precompile common.Address, input []byte, result []byte) *testOracle {
return &testOracle{
hint: func(v []byte) {},
getPreimage: func(k [32]byte) []byte {
keyData := append(precompile.Bytes(), input...)
switch k[0] {
case byte(preimage.Keccak256KeyType):
if k != preimage.Keccak256Key(crypto.Keccak256Hash(preimageData)).PreimageKey() {
if k != preimage.Keccak256Key(crypto.Keccak256Hash(keyData)).PreimageKey() {
t.Fatalf("invalid preimage request for %x", k)
}
return preimageData
case byte(preimage.KZGPointEvaluationKeyType):
if k != preimage.KZGPointEvaluationKey(crypto.Keccak256Hash(preimageData)).PreimageKey() {
return keyData
case byte(preimage.PrecompileKeyType):
if k != preimage.PrecompileKey(crypto.Keccak256Hash(keyData)).PreimageKey() {
t.Fatalf("invalid preimage request for %x", k)
}
return result
......@@ -290,7 +291,10 @@ func staticPrecompileOracle(t *testing.T, preimageData []byte, result []byte) *t
func selectOracleFixture(t *testing.T, programName string) PreimageOracle {
if strings.HasPrefix(programName, "oracle_kzg") {
return staticPrecompileOracle(t, []byte("hello world"), []byte{0x1})
precompile := common.BytesToAddress([]byte{0xa})
input := common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
blobPrecompileReturnValue := common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")
return staticPrecompileOracle(t, precompile, input, append([]byte{0x1}, blobPrecompileReturnValue...))
} else if strings.HasPrefix(programName, "oracle") {
return staticOracle(t, []byte("hello world"))
} else {
......
This diff is collapsed.
This diff is collapsed.
......@@ -20,25 +20,25 @@ import (
)
const (
methodInitLPP = "initLPP"
methodAddLeavesLPP = "addLeavesLPP"
methodSqueezeLPP = "squeezeLPP"
methodLoadKeccak256PreimagePart = "loadKeccak256PreimagePart"
methodLoadSha256PreimagePart = "loadSha256PreimagePart"
methodLoadBlobPreimagePart = "loadBlobPreimagePart"
methodLoadKZGPointEvaluationPreimagePart = "loadKZGPointEvaluationPreimagePart"
methodProposalCount = "proposalCount"
methodProposals = "proposals"
methodProposalMetadata = "proposalMetadata"
methodProposalBlocksLen = "proposalBlocksLen"
methodProposalBlocks = "proposalBlocks"
methodPreimagePartOk = "preimagePartOk"
methodMinProposalSize = "minProposalSize"
methodChallengeFirstLPP = "challengeFirstLPP"
methodChallengeLPP = "challengeLPP"
methodChallengePeriod = "challengePeriod"
methodGetTreeRootLPP = "getTreeRootLPP"
methodMinBondSizeLPP = "MIN_BOND_SIZE"
methodInitLPP = "initLPP"
methodAddLeavesLPP = "addLeavesLPP"
methodSqueezeLPP = "squeezeLPP"
methodLoadKeccak256PreimagePart = "loadKeccak256PreimagePart"
methodLoadSha256PreimagePart = "loadSha256PreimagePart"
methodLoadBlobPreimagePart = "loadBlobPreimagePart"
methodLoadPrecompilePreimagePart = "loadPrecompilePreimagePart"
methodProposalCount = "proposalCount"
methodProposals = "proposals"
methodProposalMetadata = "proposalMetadata"
methodProposalBlocksLen = "proposalBlocksLen"
methodProposalBlocks = "proposalBlocks"
methodPreimagePartOk = "preimagePartOk"
methodMinProposalSize = "minProposalSize"
methodChallengeFirstLPP = "challengeFirstLPP"
methodChallengeLPP = "challengeLPP"
methodChallengePeriod = "challengePeriod"
methodGetTreeRootLPP = "getTreeRootLPP"
methodMinBondSizeLPP = "MIN_BOND_SIZE"
)
var (
......@@ -107,8 +107,11 @@ func (c *PreimageOracleContract) AddGlobalDataTx(data *types.PreimageOracleData)
data.BlobProof,
new(big.Int).SetUint64(uint64(data.OracleOffset)))
return call.ToTxCandidate()
case preimage.KZGPointEvaluationKeyType:
call := c.contract.Call(methodLoadKZGPointEvaluationPreimagePart, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize())
case preimage.PrecompileKeyType:
call := c.contract.Call(methodLoadPrecompilePreimagePart,
new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPrecompileAddress(),
data.GetPrecompileInput())
return call.ToTxCandidate()
default:
return txmgr.TxCandidate{}, fmt.Errorf("%w: %v", ErrUnsupportedKeyType, keyType)
......
......@@ -71,13 +71,14 @@ func TestPreimageOracleContract_AddGlobalDataTx(t *testing.T) {
stubRpc.VerifyTxCandidate(tx)
})
t.Run("KZGPointEvaluation", func(t *testing.T) {
t.Run("Precompile", func(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t)
input := testutils.RandomData(rand.New(rand.NewSource(23)), 200)
data := types.NewPreimageOracleData(common.Hash{byte(preimage.KZGPointEvaluationKeyType), 0xcc}.Bytes(), input, uint32(545))
stubRpc.SetResponse(oracleAddr, methodLoadKZGPointEvaluationPreimagePart, batching.BlockLatest, []interface{}{
data := types.NewPreimageOracleData(common.Hash{byte(preimage.PrecompileKeyType), 0xcc}.Bytes(), input, uint32(545))
stubRpc.SetResponse(oracleAddr, methodLoadPrecompilePreimagePart, batching.BlockLatest, []interface{}{
new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPreimageWithoutSize(),
data.GetPrecompileAddress(),
data.GetPrecompileInput(),
}, nil)
tx, err := oracle.AddGlobalDataTx(data)
......
......@@ -46,8 +46,8 @@ func (l *preimageLoader) LoadPreimage(proof *proofData) (*types.PreimageOracleDa
switch preimage.KeyType(proof.OracleKey[0]) {
case preimage.BlobKeyType:
return l.loadBlobPreimage(proof)
case preimage.KZGPointEvaluationKeyType:
return l.loadKZGPointEvaluationPreimage(proof)
case preimage.PrecompileKeyType:
return l.loadPrecompilePreimage(proof)
default:
return types.NewPreimageOracleData(proof.OracleKey, proof.OracleValue, proof.OracleOffset), nil
}
......@@ -102,7 +102,7 @@ func (l *preimageLoader) loadBlobPreimage(proof *proofData) (*types.PreimageOrac
return types.NewPreimageOracleBlobData(proof.OracleKey, claimWithLength, proof.OracleOffset, requiredFieldElement, commitment, kzgProof[:]), nil
}
func (l *preimageLoader) loadKZGPointEvaluationPreimage(proof *proofData) (*types.PreimageOracleData, error) {
func (l *preimageLoader) loadPrecompilePreimage(proof *proofData) (*types.PreimageOracleData, error) {
inputKey := preimage.Keccak256Key(proof.OracleKey).PreimageKey()
input, err := l.getPreimage(inputKey)
if err != nil {
......
......@@ -152,9 +152,9 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) {
})
}
func TestPreimageLoader_KZGPointEvaluationPreimage(t *testing.T) {
func TestPreimageLoader_PrecompilePreimage(t *testing.T) {
input := []byte("test input")
key := preimage.KZGPointEvaluationKey(crypto.Keccak256Hash(input)).PreimageKey()
key := preimage.PrecompileKey(crypto.Keccak256Hash(input)).PreimageKey()
proof := &proofData{
OracleKey: key[:],
}
......
......@@ -265,8 +265,8 @@ func FirstKeccakPreimageLoad() PreimageOpt {
return FirstPreimageLoadOfType("keccak")
}
func FirstKZGPointEvaluationPreimageLoad() PreimageOpt {
return FirstPreimageLoadOfType("kzg-point-evaluation")
func FirstPrecompilePreimageLoad() PreimageOpt {
return FirstPreimageLoadOfType("precompile")
}
func PreimageLargerThan(size int) PreimageOpt {
......
......@@ -54,6 +54,14 @@ func (p *PreimageOracleData) GetPreimageWithSize() []byte {
return p.oracleData
}
func (p *PreimageOracleData) GetPrecompileAddress() common.Address {
return common.BytesToAddress(p.oracleData[8:28])
}
func (p *PreimageOracleData) GetPrecompileInput() []byte {
return p.oracleData[28:]
}
// NewPreimageOracleData creates a new [PreimageOracleData] instance.
func NewPreimageOracleData(key []byte, data []byte, offset uint32) *PreimageOracleData {
return &PreimageOracleData{
......
......@@ -639,8 +639,13 @@ func (g *OutputGameHelper) uploadPreimage(ctx context.Context, data *types.Preim
g.require.NoError(err)
var tx *gethtypes.Transaction
switch data.OracleKey[0] {
case byte(preimage.KZGPointEvaluationKeyType):
tx, err = boundOracle.LoadKZGPointEvaluationPreimagePart(g.opts, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize())
case byte(preimage.PrecompileKeyType):
tx, err = boundOracle.LoadPrecompilePreimagePart(
g.opts,
new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPrecompileAddress(),
data.GetPrecompileInput(),
)
default:
tx, err = boundOracle.LoadKeccak256PreimagePart(g.opts, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize())
}
......
......@@ -334,7 +334,7 @@ func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) {
// Now the honest challenger is positioned as the defender of the execution game
// We then move to challenge it to induce a preimage load
preimageLoadCheck := game.CreateStepPreimageLoadCheck(ctx)
game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, cannon.FirstKZGPointEvaluationPreimageLoad(), preimageLoadCheck, preloadPreimage)
game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, cannon.FirstPrecompilePreimageLoad(), preimageLoadCheck, preloadPreimage)
// The above method already verified the image was uploaded and step called successfully
// So we don't waste time resolving the game - that's tested elsewhere.
}
......
......@@ -40,8 +40,8 @@ const (
Sha256KeyType KeyType = 4
// BlobKeyType is for blob point pre-images.
BlobKeyType KeyType = 5
// KZGPointEvaluationKeyType is for KZG point-evaluation pre-images.
KZGPointEvaluationKeyType KeyType = 6
// PrecompileKeyType is for precompile result pre-images.
PrecompileKeyType KeyType = 6
)
// LocalIndexKey is a key local to the program, indexing a special program input.
......@@ -104,20 +104,20 @@ func (k BlobKey) TerminalString() string {
return "0x" + hex.EncodeToString(k[:])
}
// KZGPointEvaluationKey is the hash of a KZG point-evaluation EVM call input-data
type KZGPointEvaluationKey [32]byte
// PrecompileKey is the hash of precompile address and its input data
type PrecompileKey [32]byte
func (k KZGPointEvaluationKey) PreimageKey() (out [32]byte) {
func (k PrecompileKey) PreimageKey() (out [32]byte) {
out = k
out[0] = byte(KZGPointEvaluationKeyType)
out[0] = byte(PrecompileKeyType)
return
}
func (k KZGPointEvaluationKey) String() string {
func (k PrecompileKey) String() string {
return "0x" + hex.EncodeToString(k[:])
}
func (k KZGPointEvaluationKey) TerminalString() string {
func (k PrecompileKey) TerminalString() string {
return "0x" + hex.EncodeToString(k[:])
}
......
......@@ -66,11 +66,11 @@ func TestPreimageKeyTypes(t *testing.T) {
t.Run("KZGPointEvaluationKey", func(t *testing.T) {
fauxHash := [32]byte{}
fauxHash[31] = 0xFF
actual := KZGPointEvaluationKey(fauxHash)
actual := PrecompileKey(fauxHash)
// PreimageKey encoding
expected := [32]byte{}
expected[0] = byte(KZGPointEvaluationKeyType)
expected[0] = byte(PrecompileKeyType)
expected[31] = 0xFF
require.Equal(t, expected, actual.PreimageKey())
......
......@@ -38,8 +38,8 @@ func WithVerification(source PreimageGetter) PreimageGetter {
case BlobKeyType:
// Can't verify an individual field element without having a kzg proof
return data, nil
case KZGPointEvaluationKeyType:
// Can't verify the KZG point evaluation result without having a kzg proof
case PrecompileKeyType:
// Can't verify precompile result without knowing the input preimage
return data, nil
default:
return nil, fmt.Errorf("%w: %v", ErrUnsupportedKeyType, key[0])
......
......@@ -41,7 +41,7 @@ func TestWithVerification(t *testing.T) {
},
{
name: "KZGPointEvaluationKey NoVerification",
key: KZGPointEvaluationKey([32]byte{1, 2, 3, 4}),
key: PrecompileKey([32]byte{1, 2, 3, 4}),
data: []byte{4, 3, 5, 7, 3},
expectedData: []byte{4, 3, 5, 7, 3},
},
......
......@@ -17,12 +17,17 @@ const cacheSize = 2000
// CachingOracle is an implementation of Oracle that delegates to another implementation, adding caching of all results
type CachingOracle struct {
oracle Oracle
blocks *simplelru.LRU[common.Hash, eth.BlockInfo]
txs *simplelru.LRU[common.Hash, types.Transactions]
rcpts *simplelru.LRU[common.Hash, types.Receipts]
blobs *simplelru.LRU[common.Hash, *eth.Blob]
ptEvals *simplelru.LRU[common.Hash, bool]
oracle Oracle
blocks *simplelru.LRU[common.Hash, eth.BlockInfo]
txs *simplelru.LRU[common.Hash, types.Transactions]
rcpts *simplelru.LRU[common.Hash, types.Receipts]
blobs *simplelru.LRU[common.Hash, *eth.Blob]
pcmps *simplelru.LRU[common.Hash, precompileResult]
}
type precompileResult struct {
result []byte
ok bool
}
func NewCachingOracle(oracle Oracle) *CachingOracle {
......@@ -30,14 +35,14 @@ func NewCachingOracle(oracle Oracle) *CachingOracle {
txsLRU, _ := simplelru.NewLRU[common.Hash, types.Transactions](cacheSize, nil)
rcptsLRU, _ := simplelru.NewLRU[common.Hash, types.Receipts](cacheSize, nil)
blobsLRU, _ := simplelru.NewLRU[common.Hash, *eth.Blob](cacheSize, nil)
ptEvals, _ := simplelru.NewLRU[common.Hash, bool](cacheSize, nil)
pcmps, _ := simplelru.NewLRU[common.Hash, precompileResult](cacheSize, nil)
return &CachingOracle{
oracle: oracle,
blocks: blockLRU,
txs: txsLRU,
rcpts: rcptsLRU,
blobs: blobsLRU,
ptEvals: ptEvals,
oracle: oracle,
blocks: blockLRU,
txs: txsLRU,
rcpts: rcptsLRU,
blobs: blobsLRU,
pcmps: pcmps,
}
}
......@@ -90,13 +95,12 @@ func (o *CachingOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash
return blob
}
func (o *CachingOracle) KZGPointEvaluation(input []byte) bool {
cacheKey := crypto.Keccak256Hash(input)
ptEval, ok := o.ptEvals.Get(cacheKey)
if ok {
return ptEval
func (o *CachingOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
cacheKey := crypto.Keccak256Hash(append(address.Bytes(), input...))
if val, ok := o.pcmps.Get(cacheKey); ok {
return val.result, val.ok
}
ptEval = o.oracle.KZGPointEvaluation(input)
o.ptEvals.Add(cacheKey, ptEval)
return ptEval
res, ok := o.oracle.Precompile(address, input)
o.pcmps.Add(cacheKey, precompileResult{res, ok})
return res, ok
}
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-program/client/l1/test"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
......@@ -93,19 +94,23 @@ func TestCachingOracle_GetBlobs(t *testing.T) {
require.Equal(t, &blob, actualBlob)
}
func TestCachingOracle_KZGPointEvaluation(t *testing.T) {
func TestCachingOracle_Precompile(t *testing.T) {
stub := test.NewStubOracle(t)
oracle := NewCachingOracle(stub)
input := []byte{0x01, 0x02, 0x03, 0x04}
output := []byte{0x0a, 0x0b, 0x0c, 0x0d}
addr := common.Address{0x1}
// Initial call retrieves from the stub
stub.PtEvals[crypto.Keccak256Hash(input)] = true
actualPtEval := oracle.KZGPointEvaluation(input)
require.True(t, actualPtEval)
stub.PcmpResults[crypto.Keccak256Hash(append(addr.Bytes(), input...))] = output
actualResult, actualStatus := oracle.Precompile(addr, input)
require.True(t, actualStatus)
require.EqualValues(t, output, actualResult)
// Later calls should retrieve from cache
delete(stub.PtEvals, crypto.Keccak256Hash(input))
actualPtEval = oracle.KZGPointEvaluation(input)
require.True(t, actualPtEval)
delete(stub.PcmpResults, crypto.Keccak256Hash(append(addr.Bytes(), input...)))
actualResult, actualStatus = oracle.Precompile(addr, input)
require.True(t, actualStatus)
require.EqualValues(t, output, actualResult)
}
......@@ -8,11 +8,11 @@ import (
)
const (
HintL1BlockHeader = "l1-block-header"
HintL1Transactions = "l1-transactions"
HintL1Receipts = "l1-receipts"
HintL1Blob = "l1-blob"
HintL1KZGPointEvaluation = "l1-kzg-point-evaluation"
HintL1BlockHeader = "l1-block-header"
HintL1Transactions = "l1-transactions"
HintL1Receipts = "l1-receipts"
HintL1Blob = "l1-blob"
HintL1Precompile = "l1-precompile"
)
type BlockHeaderHint common.Hash
......@@ -47,10 +47,10 @@ func (l BlobHint) Hint() string {
return HintL1Blob + " " + hexutil.Encode(l)
}
type KZGPointEvaluationHint []byte
type PrecompileHint []byte
var _ preimage.Hint = KZGPointEvaluationHint{}
var _ preimage.Hint = PrecompileHint{}
func (l KZGPointEvaluationHint) Hint() string {
return HintL1KZGPointEvaluation + " " + hexutil.Encode(l)
func (l PrecompileHint) Hint() string {
return HintL1Precompile + " " + hexutil.Encode(l)
}
......@@ -25,11 +25,11 @@ type Oracle interface {
// ReceiptsByBlockHash retrieves the receipts from the block with the given hash.
ReceiptsByBlockHash(blockHash common.Hash) (eth.BlockInfo, types.Receipts)
// GetBlobField retrieves the field element at the given index from the blob with the given hash.
// GetBlob retrieves the blob with the given hash.
GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *eth.Blob
// KZGPointEvaluation retriees the result of the Cancun KZG point evaluation precompile for the given input.
KZGPointEvaluation(input []byte) bool
// Precompile retrieves the result and success indicator of a precompile call for the given input.
Precompile(precompileAddress common.Address, input []byte) ([]byte, bool)
}
// PreimageOracle implements Oracle using by interfacing with the pure preimage.Oracle
......@@ -119,12 +119,13 @@ func (p *PreimageOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHas
return &blob
}
func (p *PreimageOracle) KZGPointEvaluation(input []byte) bool {
p.hint.Hint(KZGPointEvaluationHint(input))
key := preimage.KZGPointEvaluationKey(crypto.Keccak256Hash(input[:]))
func (p *PreimageOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
hintBytes := append(address.Bytes(), input...)
p.hint.Hint(PrecompileHint(hintBytes))
key := preimage.PrecompileKey(crypto.Keccak256Hash(hintBytes))
result := p.oracle.Get(key)
if len(result) != 1 || result[0] > 1 {
panic(fmt.Errorf("unexpected preimage oracle KZGPointEvaluation behavior, got result: %x", result))
if len(result) == 0 { // must contain at least the status code
panic(fmt.Errorf("unexpected precompile oracle behavior, got result: %x", result))
}
return result[0] == 1
return result[1:], result[0] == 1
}
......@@ -24,18 +24,18 @@ type StubOracle struct {
// Blobs maps indexed blob hash to l1 block ref to blob
Blobs map[eth.L1BlockRef]map[eth.IndexedBlobHash]*eth.Blob
// PtEvals maps hashed input to whether the KZG point evaluation was successful
PtEvals map[common.Hash]bool
// PcmpResults maps hashed input to the results of precompile calls
PcmpResults map[common.Hash][]byte
}
func NewStubOracle(t *testing.T) *StubOracle {
return &StubOracle{
t: t,
Blocks: make(map[common.Hash]eth.BlockInfo),
Txs: make(map[common.Hash]types.Transactions),
Rcpts: make(map[common.Hash]types.Receipts),
Blobs: make(map[eth.L1BlockRef]map[eth.IndexedBlobHash]*eth.Blob),
PtEvals: make(map[common.Hash]bool),
t: t,
Blocks: make(map[common.Hash]eth.BlockInfo),
Txs: make(map[common.Hash]types.Transactions),
Rcpts: make(map[common.Hash]types.Receipts),
Blobs: make(map[eth.L1BlockRef]map[eth.IndexedBlobHash]*eth.Blob),
PcmpResults: make(map[common.Hash][]byte),
}
}
......@@ -75,10 +75,10 @@ func (o StubOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *e
return blob
}
func (o StubOracle) KZGPointEvaluation(input []byte) bool {
result, ok := o.PtEvals[crypto.Keccak256Hash(input)]
func (o StubOracle) Precompile(addr common.Address, input []byte) ([]byte, bool) {
result, ok := o.PcmpResults[crypto.Keccak256Hash(append(addr.Bytes(), input...))]
if !ok {
o.t.Fatalf("unknown kzg point evaluation %x", input)
}
return result
return result, true
}
......@@ -40,7 +40,7 @@ type OracleBackedL2Chain struct {
var _ engineapi.EngineBackend = (*OracleBackedL2Chain)(nil)
func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, kzgOracle engineapi.KZGPointEvaluationOracle, chainCfg *params.ChainConfig, l2OutputRoot common.Hash) (*OracleBackedL2Chain, error) {
func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, precompileOracle engineapi.PrecompileOracle, chainCfg *params.ChainConfig, l2OutputRoot common.Hash) (*OracleBackedL2Chain, error) {
output := oracle.OutputByRoot(l2OutputRoot)
outputV0, ok := output.(*eth.OutputV0)
if !ok {
......@@ -67,7 +67,7 @@ func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, kzgOracle engineap
blocks: make(map[common.Hash]*types.Block),
db: NewOracleBackedDB(oracle),
vmCfg: vm.Config{
OptimismPrecompileOverrides: precompileOverrides(kzgOracle),
OptimismPrecompileOverrides: engineapi.CreatePrecompileOverrides(precompileOracle),
},
}, nil
}
......@@ -232,15 +232,3 @@ func (o *OracleBackedL2Chain) SetFinalized(header *types.Header) {
func (o *OracleBackedL2Chain) SetSafe(header *types.Header) {
o.safe = header
}
var kzgPointEvaluationPrecompileAddress = common.BytesToAddress([]byte{0xa})
func precompileOverrides(kzgOracle engineapi.KZGPointEvaluationOracle) vm.PrecompileOverrides {
return func(rules params.Rules, address common.Address) (vm.PrecompiledContract, bool) {
// NOTE: Ignoring chain rules for now. We assume that precompile behavior won't change for the foreseeable future
if address != kzgPointEvaluationPrecompileAddress {
return nil, false
}
return &engineapi.OracleKZGPointEvaluation{Oracle: kzgOracle}, true
}
}
......@@ -28,8 +28,17 @@ var fundedKey, _ = crypto.GenerateKey()
var fundedAddress = crypto.PubkeyToAddress(fundedKey.PublicKey)
var targetAddress = common.HexToAddress("0x001122334455")
// Also a valid input to the kzg point evaluation precompile
var inputData = common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
var (
kzgInputData = common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
ecRecoverInputData = common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549")
bn256PairingInputData = common.FromHex("1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa")
)
var (
ecRecoverReturnValue = []byte{0x1, 0x2}
bn256PairingReturnValue = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
blobPrecompileReturnValue = common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")
)
func TestInitialState(t *testing.T) {
blocks, chain := setupOracleBackedChain(t, 5)
......@@ -189,23 +198,54 @@ func TestGetHeaderByNumber(t *testing.T) {
})
}
func TestKZGPointEvaluationPrecompile(t *testing.T) {
blockCount := 3
headBlockNumber := 3
logger := testlog.Logger(t, log.LevelDebug)
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber, true)
head := blocks[headBlockNumber].Hash()
stubOutput := eth.OutputV0{BlockHash: head}
kzgOracle := new(l2test.StubKZGOracle)
kzgOracle.PtEvals = map[common.Hash]bool{
crypto.Keccak256Hash(inputData): true,
func TestPrecompileOracle(t *testing.T) {
tests := []struct {
name string
input []byte
target common.Address
result []byte
}{
{
name: "EcRecover",
input: ecRecoverInputData,
target: common.BytesToAddress([]byte{0x1}),
result: ecRecoverReturnValue,
},
{
name: "Bn256Pairing",
input: bn256PairingInputData,
target: common.BytesToAddress([]byte{0x8}),
result: bn256PairingReturnValue,
},
{
name: "KZGPointEvaluation",
input: kzgInputData,
target: common.BytesToAddress([]byte{0xa}),
result: blobPrecompileReturnValue,
},
}
chain, err := NewOracleBackedL2Chain(logger, oracle, kzgOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err)
newBlock := createKZGBlock(t, chain)
require.NoError(t, chain.InsertBlockWithoutSetHead(newBlock))
require.Equal(t, 1, kzgOracle.Calls)
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
blockCount := 3
headBlockNumber := 3
logger := testlog.Logger(t, log.LevelDebug)
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber, true)
head := blocks[headBlockNumber].Hash()
stubOutput := eth.OutputV0{BlockHash: head}
precompileOracle := new(l2test.StubPrecompileOracle)
precompileOracle.Results = map[common.Hash]l2test.PrecompileResult{
crypto.Keccak256Hash(append(test.target.Bytes(), test.input...)): {Result: test.result, Ok: true},
}
chain, err := NewOracleBackedL2Chain(logger, oracle, precompileOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err)
newBlock := createBlock(t, chain, WithInput(test.input), WithTargetAddress(test.target))
require.NoError(t, chain.InsertBlockWithoutSetHead(newBlock))
require.Equal(t, 1, precompileOracle.Calls)
})
}
}
func assertBlockDataAvailable(t *testing.T, chain *OracleBackedL2Chain, block *types.Block, blockNumber uint64) {
......@@ -225,8 +265,8 @@ func setupOracleBackedChainWithLowerHead(t *testing.T, blockCount int, headBlock
chainCfg, blocks, oracle := setupOracle(t, blockCount, headBlockNumber, false)
head := blocks[headBlockNumber].Hash()
stubOutput := eth.OutputV0{BlockHash: head}
kzgOracle := new(l2test.StubKZGOracle)
chain, err := NewOracleBackedL2Chain(logger, oracle, kzgOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
precompileOracle := new(l2test.StubPrecompileOracle)
chain, err := NewOracleBackedL2Chain(logger, oracle, precompileOracle, chainCfg, common.Hash(eth.OutputRoot(&stubOutput)))
require.NoError(t, err)
return blocks, chain
}
......@@ -276,7 +316,33 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int, enableEcoton
return chainCfg, blocks, oracle
}
func createBlockWithTargetAddress(t *testing.T, chain *OracleBackedL2Chain, target *common.Address) *types.Block {
type blockCreateConfig struct {
input []byte
target *common.Address
}
type blockCreateOption func(*blockCreateConfig)
func WithInput(input []byte) blockCreateOption {
return func(opts *blockCreateConfig) {
opts.input = input
}
}
func WithTargetAddress(target common.Address) blockCreateOption {
return func(opts *blockCreateConfig) {
opts.target = &target
}
}
func createBlock(t *testing.T, chain *OracleBackedL2Chain, opts ...blockCreateOption) *types.Block {
cfg := blockCreateConfig{}
for _, o := range opts {
o(&cfg)
}
if cfg.target == nil {
cfg.target = &targetAddress
}
parent := chain.GetBlockByHash(chain.CurrentHeader().Hash())
parentDB, err := chain.StateAt(parent.Root())
require.NoError(t, err)
......@@ -287,11 +353,11 @@ func createBlockWithTargetAddress(t *testing.T, chain *OracleBackedL2Chain, targ
rawTx := &types.DynamicFeeTx{
ChainID: config.ChainID,
Nonce: nonce,
To: target,
To: cfg.target,
GasTipCap: big.NewInt(0),
GasFeeCap: parent.BaseFee(),
Gas: 210_000,
Data: inputData,
Gas: 2_000_000,
Data: cfg.input,
Value: big.NewInt(1),
}
tx := types.MustSignNewTx(fundedKey, types.NewLondonSigner(config.ChainID), rawTx)
......@@ -300,14 +366,6 @@ func createBlockWithTargetAddress(t *testing.T, chain *OracleBackedL2Chain, targ
return blocks[0]
}
func createBlock(t *testing.T, chain *OracleBackedL2Chain) *types.Block {
return createBlockWithTargetAddress(t, chain, &targetAddress)
}
func createKZGBlock(t *testing.T, chain *OracleBackedL2Chain) *types.Block {
return createBlockWithTargetAddress(t, chain, &kzgPointEvaluationPrecompileAddress)
}
func TestEngineAPITests(t *testing.T) {
test.RunEngineAPITests(t, func(t *testing.T) engineapi.EngineBackend {
_, chain := setupOracleBackedChain(t, 0)
......
// This file contains code of the upstream go-ethereum kzgPointEvaluation implementation.
// Modifications have been made, primarily to substitute kzg4844.VerifyProof with a preimage oracle call.
// Modifications have been made, primarily to substitute kzgPointEvaluation, ecrecover, and runBn256Pairing
// functions to interact with the preimage oracle.
//
// Original copyright disclaimer, applicable only to this file:
// -------------------------------------------------------------------
......@@ -22,29 +23,150 @@
package engineapi
import (
"bytes"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
// OracleKZGPointEvaluation implements the EIP-4844 point evaluation precompile,
// using the preimage-oracle to perform the evaluation.
type OracleKZGPointEvaluation struct {
Oracle KZGPointEvaluationOracle
var (
ecrecoverPrecompileAddress = common.BytesToAddress([]byte{0x1})
bn256PairingPrecompileAddress = common.BytesToAddress([]byte{0x8})
kzgPointEvaluationPrecompileAddress = common.BytesToAddress([]byte{0xa})
)
// PrecompileOracle defines the high-level API used to retrieve the result of a precompile call
// The caller is expected to validate the input to the precompile call
type PrecompileOracle interface {
Precompile(address common.Address, input []byte) ([]byte, bool)
}
func CreatePrecompileOverrides(precompileOracle PrecompileOracle) vm.PrecompileOverrides {
return func(rules params.Rules, address common.Address) (vm.PrecompiledContract, bool) {
// NOTE: Ignoring chain rules for now. We assume that precompile behavior won't change for the foreseeable future
switch address {
case ecrecoverPrecompileAddress:
return &ecrecoverOracle{Oracle: precompileOracle}, true
case bn256PairingPrecompileAddress:
return &bn256PairingOracle{Oracle: precompileOracle, rules: rules}, true
case kzgPointEvaluationPrecompileAddress:
return &kzgPointEvaluationOracle{Oracle: precompileOracle}, true
default:
return nil, false
}
}
}
type ecrecoverOracle struct {
Oracle PrecompileOracle
}
// KZGPointEvaluationOracle defines the high-level API used to retrieve the result of the KZG point evaluation precompile
type KZGPointEvaluationOracle interface {
KZGPointEvaluation(input []byte) bool
func (c *ecrecoverOracle) RequiredGas(input []byte) uint64 {
return params.EcrecoverGas
}
func (c *ecrecoverOracle) Run(input []byte) ([]byte, error) {
// Modification note: the L1 precompile behavior may change, but not in incompatible ways.
// We want to enforce the subset that represents the EVM behavior activated in L2.
// Below is a copy of the Cancun behavior. L1 might expand on that at a later point.
const ecRecoverInputLength = 128
input = common.RightPadBytes(input, ecRecoverInputLength)
// "input" is (hash, v, r, s), each 32 bytes
// but for ecrecover we want (r, s, v)
r := new(big.Int).SetBytes(input[64:96])
s := new(big.Int).SetBytes(input[96:128])
v := input[63] - 27
// tighter sig s values input homestead only apply to tx sigs
if !allZero(input[32:63]) || !crypto.ValidateSignatureValues(v, r, s, false) {
return nil, nil
}
// We must make sure not to modify the 'input', so placing the 'v' along with
// the signature needs to be done on a new allocation
sig := make([]byte, 65)
copy(sig, input[64:128])
sig[64] = v
// v needs to be at the end for libsecp256k1
// Modification note: below replaces the crypto.Ecrecover call
result, ok := c.Oracle.Precompile(ecrecoverPrecompileAddress, input)
if !ok {
return nil, errors.New("invalid ecrecover input")
}
return result, nil
}
func allZero(b []byte) bool {
for _, byte := range b {
if byte != 0 {
return false
}
}
return true
}
type bn256PairingOracle struct {
rules params.Rules
Oracle PrecompileOracle
}
func (b *bn256PairingOracle) RequiredGas(input []byte) uint64 {
// Required gas changed in Istanbul. Note that IsIstanbul is true for Istanbul and all forks after it
// TODO(client-pod#628): Investigate delegating this to the original precompile implementation to avoid reimplementing.
if b.rules.IsIstanbul {
return params.Bn256PairingBaseGasIstanbul + uint64(len(input)/192)*params.Bn256PairingPerPointGasIstanbul
} else {
return params.Bn256PairingBaseGasByzantium + uint64(len(input)/192)*params.Bn256PairingPerPointGasByzantium
}
}
var (
// true32Byte is returned if the bn256 pairing check succeeds.
true32Byte = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
// false32Byte is returned if the bn256 pairing check fails.
false32Byte = make([]byte, 32)
// errBadPairingInput is returned if the bn256 pairing input is invalid.
errBadPairingInput = errors.New("bad elliptic curve pairing size")
)
func (b *bn256PairingOracle) Run(input []byte) ([]byte, error) {
// Handle some corner cases cheaply
if len(input)%192 > 0 {
return nil, errBadPairingInput
}
// Modification note: below replaces point verification and pairing checks
// Assumes both L2 and the L1 oracle have an identical range of valid points
result, ok := b.Oracle.Precompile(bn256PairingPrecompileAddress, input)
if !ok {
return nil, errors.New("invalid bn256Pairing check")
}
if !bytes.Equal(result, true32Byte) && !bytes.Equal(result, false32Byte) {
panic("unexpected result from bn256Pairing check")
}
return result, nil
}
// kzgPointEvaluationOracle implements the EIP-4844 point evaluation precompile,
// using the preimage-oracle to perform the evaluation.
type kzgPointEvaluationOracle struct {
Oracle PrecompileOracle
}
// RequiredGas estimates the gas required for running the point evaluation precompile.
func (b *OracleKZGPointEvaluation) RequiredGas(input []byte) uint64 {
func (b *kzgPointEvaluationOracle) RequiredGas(_ []byte) uint64 {
return params.BlobTxPointEvaluationPrecompileGas
}
......@@ -60,7 +182,7 @@ var (
)
// Run executes the point evaluation precompile.
func (b *OracleKZGPointEvaluation) Run(input []byte) ([]byte, error) {
func (b *kzgPointEvaluationOracle) Run(input []byte) ([]byte, error) {
// Modification note: the L1 precompile behavior may change, but not in incompatible ways.
// We want to enforce the subset that represents the EVM behavior activated in L2.
// Below is a copy of the Cancun behavior. L1 might expand on that at a later point.
......@@ -93,9 +215,12 @@ func (b *OracleKZGPointEvaluation) Run(input []byte) ([]byte, error) {
copy(proof[:], input[144:])
// Modification note: below replaces the kzg4844.VerifyProof call
ok := b.Oracle.KZGPointEvaluation(input)
result, ok := b.Oracle.Precompile(kzgPointEvaluationPrecompileAddress, input)
if !ok {
return nil, fmt.Errorf("%w: invalid KZG point evaluation", errBlobVerifyKZGProof)
}
return common.FromHex(blobPrecompileReturnValue), nil
if !bytes.Equal(result, common.FromHex(blobPrecompileReturnValue)) {
panic("unexpected result from KZG point evaluation check")
}
return result, nil
}
......@@ -124,17 +124,22 @@ func (o *StubStateOracle) CodeByHash(hash common.Hash) []byte {
return data
}
type StubKZGOracle struct {
type StubPrecompileOracle struct {
t *testing.T
PtEvals map[common.Hash]bool
Results map[common.Hash]PrecompileResult
Calls int
}
func (o *StubKZGOracle) KZGPointEvaluation(input []byte) bool {
result, ok := o.PtEvals[crypto.Keccak256Hash(input)]
type PrecompileResult struct {
Result []byte
Ok bool
}
func (o *StubPrecompileOracle) Precompile(address common.Address, input []byte) ([]byte, bool) {
result, ok := o.Results[crypto.Keccak256Hash(append(address.Bytes(), input...))]
if !ok {
o.t.Fatalf("no value for point evaluation %v", input)
}
o.Calls++
return result
return result.Result, result.Ok
}
......@@ -5,6 +5,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"slices"
"strings"
preimage "github.com/ethereum-optimism/optimism/op-preimage"
......@@ -23,10 +24,16 @@ import (
)
var (
kzgPointEvaluationSuccess = [1]byte{1}
kzgPointEvaluationFailure = [1]byte{0}
precompileSuccess = [1]byte{1}
precompileFailure = [1]byte{0}
)
var acceleratedPrecompiles = []common.Address{
common.BytesToAddress([]byte{0x1}), // ecrecover
common.BytesToAddress([]byte{0x8}), // bn256Pairing
common.BytesToAddress([]byte{0x0a}), // KZG Point Evaluation
}
type L1Source interface {
InfoByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, error)
InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error)
......@@ -38,10 +45,6 @@ type L1BlobSource interface {
GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error)
}
type L1PrecompileSource interface {
KZGPointEvaluation(input []byte) ([]byte, error)
}
type L2Source interface {
InfoAndTxsByHash(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Transactions, error)
NodeByHash(ctx context.Context, hash common.Hash) ([]byte, error)
......@@ -175,22 +178,33 @@ func (p *Prefetcher) prefetch(ctx context.Context, hint string) error {
}
}
return nil
case l1.HintL1KZGPointEvaluation:
precompile := vm.PrecompiledContractsCancun[common.BytesToAddress([]byte{0x0a})]
// KZG Point Evaluation precompile also verifies hintBytes length
_, err := precompile.Run(hintBytes)
var result [1]byte
case l1.HintL1Precompile:
if len(hintBytes) < 20 {
return fmt.Errorf("invalid precompile hint: %x", hint)
}
precompileAddress := common.BytesToAddress(hintBytes[:20])
// For extra safety, avoid accelerating unexpected precompiles
if !slices.Contains(acceleratedPrecompiles, precompileAddress) {
return fmt.Errorf("unsupported precompile address: %s", precompileAddress)
}
// NOTE: We use the precompiled contracts from Cancun because it's the only set that contains the addresses of all accelerated precompiles
// We assume the precompile Run function behavior does not change across EVM upgrades.
// As such, we must not rely on upgrade-specific behavior such as precompile.RequiredGas.
precompile := getPrecompiledContract(precompileAddress)
// KZG Point Evaluation precompile also verifies its input
result, err := precompile.Run(hintBytes[20:])
if err == nil {
result = kzgPointEvaluationSuccess
result = append(precompileSuccess[:], result...)
} else {
result = kzgPointEvaluationFailure
result = append(precompileFailure[:], result...)
}
inputHash := crypto.Keccak256Hash(hintBytes)
// Put the input preimage so it can be loaded later
if err := p.kvStore.Put(preimage.Keccak256Key(inputHash).PreimageKey(), hintBytes); err != nil {
return err
}
return p.kvStore.Put(preimage.KZGPointEvaluationKey(inputHash).PreimageKey(), result[:])
return p.kvStore.Put(preimage.PrecompileKey(inputHash).PreimageKey(), result)
case l2.HintL2BlockHeader, l2.HintL2Transactions:
if len(hintBytes) != 32 {
return fmt.Errorf("invalid L2 header/tx hint: %x", hint)
......@@ -283,3 +297,7 @@ func parseHint(hint string) (string, []byte, error) {
}
return hintType, hintBytes, nil
}
func getPrecompiledContract(address common.Address) vm.PrecompiledContract {
return vm.PrecompiledContractsCancun[address]
}
......@@ -231,45 +231,92 @@ func TestFetchL1Blob(t *testing.T) {
})
}
func TestFetchKZGPointEvaluation(t *testing.T) {
runTest := func(name string, input []byte, expected bool) {
t.Run(name, func(t *testing.T) {
func TestFetchPrecompileResult(t *testing.T) {
ecRecoverInput := common.FromHex("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549")
kzgPointEvalInput := common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
failure := []byte{0}
success := []byte{1}
tests := []struct {
name string
addr common.Address
input []byte
result []byte
}{
{
name: "EcRecover-Valid",
addr: common.BytesToAddress([]byte{0x1}),
input: ecRecoverInput,
result: append(success, common.FromHex("000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b")...),
},
{
name: "Bn256Pairing-Valid",
addr: common.BytesToAddress([]byte{0x8}),
input: []byte{}, // empty is valid
result: append(success, common.FromHex("0000000000000000000000000000000000000000000000000000000000000001")...),
},
{
name: "Bn256Pairing-Invalid",
addr: common.BytesToAddress([]byte{0x8}),
input: []byte{0x1},
result: failure,
},
{
name: "KzgPointEvaluation-Valid",
addr: common.BytesToAddress([]byte{0xa}),
input: kzgPointEvalInput,
result: append(success, common.FromHex("000000000000000000000000000000000000000000000000000000000000100073eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")...),
},
{
name: "KzgPointEvaluation-Invalid",
addr: common.BytesToAddress([]byte{0xa}),
input: []byte{0x0},
result: failure,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
prefetcher, _, _, _, _ := createPrefetcher(t)
oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
result := oracle.KZGPointEvaluation(input)
require.Equal(t, expected, result)
result, ok := oracle.Precompile(test.addr, test.input)
require.Equal(t, test.result[0] == 1, ok)
require.EqualValues(t, test.result[1:], result)
val, err := prefetcher.kvStore.Get(preimage.Keccak256Key(crypto.Keccak256Hash(input)).PreimageKey())
key := crypto.Keccak256Hash(append(test.addr.Bytes(), test.input...))
val, err := prefetcher.kvStore.Get(preimage.Keccak256Key(key).PreimageKey())
require.NoError(t, err)
require.EqualValues(t, input, val)
require.NotEmpty(t, val)
key := preimage.KZGPointEvaluationKey(crypto.Keccak256Hash(input)).PreimageKey()
val, err = prefetcher.kvStore.Get(key)
val, err = prefetcher.kvStore.Get(preimage.PrecompileKey(key).PreimageKey())
require.NoError(t, err)
if expected {
require.EqualValues(t, kzgPointEvaluationSuccess[:], val)
} else {
require.EqualValues(t, kzgPointEvaluationFailure[:], val)
}
require.EqualValues(t, test.result, val)
})
}
validInput := common.FromHex("01e798154708fe7789429634053cbf9f99b619f9f084048927333fce637f549b564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d3630624d25032e67a7e6a4910df5834b8fe70e6bcfeeac0352434196bdf4b2485d5a18f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7873033e038326e87ed3e1276fd140253fa08e9fc25fb2d9a98527fc22a2c9612fbeafdad446cbc7bcdbdcd780af2c16a")
runTest("Valid input", validInput, true)
runTest("Invalid input", []byte{0x00}, false)
t.Run("Already Known", func(t *testing.T) {
input := []byte("test input")
addr := common.BytesToAddress([]byte{0x1})
result := []byte{0x1}
prefetcher, _, _, _, kv := createPrefetcher(t)
err := kv.Put(preimage.KZGPointEvaluationKey(crypto.Keccak256Hash(input)).PreimageKey(), kzgPointEvaluationSuccess[:])
err := kv.Put(preimage.PrecompileKey(crypto.Keccak256Hash(append(addr.Bytes(), input...))).PreimageKey(), append([]byte{1}, result...))
require.NoError(t, err)
oracle := l1.NewPreimageOracle(asOracleFn(t, prefetcher), asHinter(t, prefetcher))
result := oracle.KZGPointEvaluation(input)
require.True(t, result)
actualResult, status := oracle.Precompile(addr, input)
require.EqualValues(t, actualResult, result)
require.True(t, status)
})
}
func TestRestrictedPrecompileContracts(t *testing.T) {
for _, addr := range acceleratedPrecompiles {
require.NotNil(t, getPrecompiledContract(addr))
}
}
func TestFetchL2Block(t *testing.T) {
rng := rand.New(rand.NewSource(123))
block, rcpts := testutils.RandomBlock(rng, 10)
......
......@@ -314,24 +314,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_partOffset",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_input",
"type": "bytes"
}
],
"name": "loadKZGPointEvaluationPreimagePart",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......@@ -389,6 +371,29 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_partOffset",
"type": "uint256"
},
{
"internalType": "address",
"name": "_precompile",
"type": "address"
},
{
"internalType": "bytes",
"name": "_input",
"type": "bytes"
}
],
"name": "loadPrecompilePreimagePart",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
......
......@@ -334,51 +334,63 @@ contract PreimageOracle is IPreimageOracle {
}
/// @inheritdoc IPreimageOracle
function loadKZGPointEvaluationPreimagePart(uint256 _partOffset, bytes calldata _input) external {
function loadPrecompilePreimagePart(uint256 _partOffset, address _precompile, bytes calldata _input) external {
// Prior to Cancun activation, the blob preimage precompile is not available.
if (block.timestamp < CANCUN_ACTIVATION) revert CancunNotActive();
bytes32 res;
bytes32 key;
bytes32 part;
uint256 size;
assembly {
// we leave solidity slots 0x40 and 0x60 untouched, and everything after as scratch-memory.
let ptr := 0x80
// copy input into memory
calldatacopy(ptr, _input.offset, _input.length)
// copy precompile address and input into memory
// len(sig) + len(_partOffset) + address-offset-in-slot
calldatacopy(ptr, 48, 20)
calldatacopy(add(20, ptr), _input.offset, _input.length)
// compute the hash
let h := keccak256(ptr, _input.length)
let h := keccak256(ptr, add(20, _input.length))
// mask out prefix byte, replace with type 6 byte
key := or(and(h, not(shl(248, 0xFF))), shl(248, 0x06))
// Verify the KZG proof by calling the point evaluation precompile.
// Capture the verification result
// Call the precompile to get the result.
res :=
staticcall(
gas(), // forward all gas
0x0A, // point evaluation precompile address
ptr, // input ptr
_input.length, // we may want to load differently sized point-evaluation calls in the future
0x00, // output ptr
0x00 // output size
_precompile,
add(20, ptr), // input ptr
_input.length,
0x0, // Unused as we don't copy anything
0x00 // don't copy anything
)
// "res" will be 0 on error, and 1 on success, of the KZG Point-evaluation precompile call
// We do have to shift it to the left-most byte of the bytes32 however, since we only read that byte.
res := shl(248, res)
// Reuse the `ptr` to store the preimage part including size prefix.
// put size as big-endian uint64 at the start of pre-image
mstore(ptr, shl(192, 1))
size := add(1, returndatasize())
// revert if part offset >= size+8 (i.e. parts must be within bounds)
if iszero(lt(_partOffset, add(size, 8))) {
// Store "PartOffsetOOB()"
mstore(0, 0xfe254987)
// Revert with "PartOffsetOOB()"
revert(0x1c, 4)
}
// Reuse the `ptr` to store the preimage part: <sizePrefix ++ precompileStatus ++ returrnData>
// put size as big-endian uint64 at start of pre-image
mstore(ptr, shl(192, size))
ptr := add(ptr, 0x08)
mstore(ptr, res)
// write precompile result status to the first byte of `ptr`
mstore8(ptr, res)
// write precompile return data to the rest of `ptr`
returndatacopy(add(ptr, 0x01), 0x0, returndatasize())
// compute part given ofset
part := mload(add(sub(ptr, 0x08), _partOffset))
}
preimagePartOk[key][_partOffset] = true;
preimageParts[key][_partOffset] = part;
// size is always 1
preimageLengths[key] = 1;
preimageLengths[key] = size;
}
////////////////////////////////////////////////////////////////
......
......@@ -70,7 +70,11 @@ interface IPreimageOracle {
)
external;
/// @notice Prepares a point evaluation precompile result to be read by the keccak256 of its input.
/// @param _input The point evaluation precompile input.
function loadKZGPointEvaluationPreimagePart(uint256 _partOffset, bytes calldata _input) external;
/// @notice Prepares a precompile result to be read by a precompile key for the specified offset.
/// The precompile result data is a concatenation of the precompile call status byte and its return data.
/// The preimage key is `6 ++ keccak256(precompile ++ input)[1:]`.
/// @param _partOffset The offset of the precompile result being loaded.
/// @param _precompile The precompile address
/// @param _input The input to the precompile call.
function loadPrecompilePreimagePart(uint256 _partOffset, address _precompile, bytes calldata _input) external;
}
......@@ -171,6 +171,95 @@ contract PreimageOracle_Test is Test {
vm.expectRevert("pre-image must exist");
oracle.readPreimage(key, offset);
}
/// @notice Tests that a precompile pre-image result is correctly set.
function test_loadPrecompilePreimagePart_succeeds() public {
bytes memory input = hex"deadbeef";
uint256 offset = 0;
address precompile = address(bytes20(uint160(0x02))); // sha256
bytes32 key = precompilePreimageKey(precompile, input);
oracle.loadPrecompilePreimagePart(offset, precompile, input);
bytes32 part = oracle.preimageParts(key, offset);
// size prefix - 1-byte result + 32-byte sha return data
assertEq(hex"0000000000000021", bytes8(part));
// precompile result
assertEq(bytes1(0x01), bytes1(part << 64));
// precompile call return data
assertEq(bytes23(sha256(input)), bytes23(part << 72));
// Validate the local data length
uint256 length = oracle.preimageLengths(key);
assertEq(length, 1 + 32);
// Validate that the first local data part is set
bool ok = oracle.preimagePartOk(key, offset);
assertTrue(ok);
}
/// @notice Tests that a precompile pre-image result is correctly set at its return data offset.
function test_loadPrecompilePreimagePart_atReturnOffset_succeeds() public {
bytes memory input = hex"deadbeef";
uint256 offset = 9;
address precompile = address(bytes20(uint160(0x02))); // sha256
bytes32 key = precompilePreimageKey(precompile, input);
oracle.loadPrecompilePreimagePart(offset, precompile, input);
bytes32 part = oracle.preimageParts(key, offset);
// 32-byte sha return data
assertEq(sha256(input), part);
// Validate the local data length
uint256 length = oracle.preimageLengths(key);
assertEq(length, 1 + 32);
// Validate that the first local data part is set
bool ok = oracle.preimagePartOk(key, offset);
assertTrue(ok);
}
/// @notice Tests that a failed precompile call has a zero status byte in preimage
function test_loadPrecompilePreimagePart_failedCall_succeeds() public {
bytes memory input = new bytes(193); // invalid input to induce a failed precompile call
uint256 offset = 0;
address precompile = address(bytes20(uint160(0x08))); // bn256Pairing
bytes32 key = precompilePreimageKey(precompile, input);
oracle.loadPrecompilePreimagePart(offset, precompile, input);
bytes32 part = oracle.preimageParts(key, offset);
// size prefix - 1-byte result + 0-byte sha return data
assertEq(hex"0000000000000001", bytes8(part));
// precompile result
assertEq(bytes1(0x00), bytes1(part << 64));
// precompile call return data
assertEq(bytes23(0), bytes23(part << 72));
// Validate the local data length
uint256 length = oracle.preimageLengths(key);
assertEq(length, 1);
// Validate that the first local data part is set
bool ok = oracle.preimagePartOk(key, offset);
assertTrue(ok);
}
/// @notice Tests that adding a global precompile result at the part boundary reverts.
function test_loadPrecompilePreimagePart_partBoundary_reverts() public {
bytes memory input = hex"deadbeef";
uint256 offset = 41; // 8-byte prefix + 1-byte result + 32-byte sha return data
address precompile = address(bytes20(uint160(0x02))); // sha256
vm.expectRevert(PartOffsetOOB.selector);
oracle.loadPrecompilePreimagePart(offset, precompile, input);
}
/// @notice Tests that a global precompile result cannot be set with an out-of-bounds offset.
function test_loadPrecompilePreimagePart_outOfBoundsOffset_reverts() public {
bytes memory input = hex"deadbeef";
uint256 offset = 42;
address precompile = address(bytes20(uint160(0x02))); // sha256
vm.expectRevert(PartOffsetOOB.selector);
oracle.loadPrecompilePreimagePart(offset, precompile, input);
}
}
contract PreimageOracle_LargePreimageProposals_Test is Test {
......@@ -1268,3 +1357,14 @@ function _setStatusByte(bytes32 _hash, uint8 _status) pure returns (bytes32 out_
out_ := or(and(not(shl(248, 0xFF)), _hash), shl(248, _status))
}
}
/// @notice Computes a precompile key for a given precompile address and input.
function precompilePreimageKey(address _precompile, bytes memory _input) pure returns (bytes32 key_) {
bytes memory p = abi.encodePacked(_precompile, _input);
uint256 sz = 20 + _input.length;
assembly {
let h := keccak256(add(0x20, p), sz)
// Mask out prefix byte, replace with type 6 byte
key_ := or(and(h, not(shl(248, 0xFF))), shl(248, 6))
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment