Commit 4309094e authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Implement verification for preimages (#9166)

* op-challenger: Implement verification for preimages

Identifies the leaf to challenge, but doesn't yet produce the merkle proofs required and can't actually send the transaction.

* op-challenger: Review feedback.

* op-challenger: Avoid nil
parent f44446ab
......@@ -2,6 +2,7 @@ package matrix
import (
"errors"
"fmt"
"io"
"math/big"
......@@ -17,10 +18,62 @@ type StateMatrix struct {
}
var (
ErrInvalidMaxLen = errors.New("invalid max length to absorb")
uint256Size = 32
ErrInvalidMaxLen = errors.New("invalid max length to absorb")
ErrIncorrectCommitmentCount = errors.New("incorrect number of commitments for input length")
ErrValid = errors.New("state commitments are valid")
uint256Size = 32
)
// Challenge creates a [types.Challenge] to invalidate the provided preimage data if possible.
// [ErrValid] is returned if the provided inputs are valid and no challenge can be created.
func Challenge(data io.Reader, commitments []common.Hash) (types.Challenge, error) {
s := NewStateMatrix()
m := s.PackState()
var prestate types.Leaf
for i := 0; ; i++ {
unpaddedLeaf, err := s.absorbNextLeafInput(data)
isEOF := errors.Is(err, io.EOF)
if err != nil && !isEOF {
return types.Challenge{}, fmt.Errorf("failed to verify inputs: %w", err)
}
validCommitment := s.StateCommitment()
if i >= len(commitments) {
// There should have been more commitments.
// The contracts should prevent this so it can't be challenged, return an error
return types.Challenge{}, ErrIncorrectCommitmentCount
}
claimedCommitment := commitments[i]
var paddedLeaf [types.BlockSize]byte
copy(paddedLeaf[:], unpaddedLeaf)
// TODO(client-pod#480): Add actual keccak padding to ensure the merkle proofs are correct
poststate := types.Leaf{
Input: paddedLeaf,
Index: big.NewInt(int64(i)),
StateCommitment: claimedCommitment,
}
if validCommitment != claimedCommitment {
return types.Challenge{
StateMatrix: m,
Prestate: prestate,
Poststate: poststate,
}, nil
}
if isEOF {
if i < len(commitments)-1 {
// We got too many commitments
// The contracts should prevent this so it can't be challenged, return an error
return types.Challenge{}, ErrIncorrectCommitmentCount
}
break
}
prestate = poststate
m = s.PackState()
}
return types.Challenge{}, ErrValid
}
// NewStateMatrix creates a new state matrix initialized with the initial, zero keccak block.
func NewStateMatrix() *StateMatrix {
return &StateMatrix{s: newLegacyKeccak256()}
......
......@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"math/big"
"math/rand"
"testing"
......@@ -55,32 +56,6 @@ type testData struct {
Commitments []common.Hash `json:"commitments"`
}
func TestReferenceCommitments(t *testing.T) {
var tests []testData
require.NoError(t, json.Unmarshal(refTests, &tests))
for i, test := range tests {
test := test
t.Run(fmt.Sprintf("Ref-%v", i), func(t *testing.T) {
s := NewStateMatrix()
commitments := []common.Hash{s.StateCommitment()}
for i := 0; i < len(test.Input); i += types.BlockSize {
end := min(i+types.BlockSize, len(test.Input))
s.absorbLeafInput(test.Input[i:end], end == len(test.Input))
commitments = append(commitments, s.StateCommitment())
}
if len(test.Input) == 0 {
s.absorbLeafInput(nil, true)
commitments = append(commitments, s.StateCommitment())
}
actual := s.Hash()
expected := crypto.Keccak256Hash(test.Input)
require.Equal(t, expected, actual)
require.Equal(t, test.Commitments, commitments)
})
}
}
func TestReferenceCommitmentsFromReader(t *testing.T) {
var tests []testData
require.NoError(t, json.Unmarshal(refTests, &tests))
......@@ -220,6 +195,119 @@ func TestMatrix_AbsorbNextLeaf(t *testing.T) {
}
}
func TestVerifyPreimage(t *testing.T) {
preimage := testutils.RandomData(rand.New(rand.NewSource(2323)), 1024)
validCommitments := func() []common.Hash {
valid, err := NewStateMatrix().AbsorbUpTo(bytes.NewReader(preimage), 1000*types.BlockSize)
require.ErrorIs(t, err, io.EOF, "Should read all preimage data")
return valid.Commitments
}
leafData := func(idx int) (out [types.BlockSize]byte) {
copy(out[:], preimage[idx*types.BlockSize:(idx+1)*types.BlockSize])
return
}
challengeLeaf := func(commitments []common.Hash, invalidIdx int) types.Challenge {
invalidLeafStart := invalidIdx * types.BlockSize
s := NewStateMatrix()
_, err := s.AbsorbUpTo(bytes.NewReader(preimage), invalidLeafStart)
require.NoError(t, err)
return types.Challenge{
StateMatrix: s.PackState(),
Prestate: types.Leaf{
Input: leafData(invalidIdx - 1),
Index: big.NewInt(int64(invalidIdx - 1)),
StateCommitment: commitments[invalidIdx-1],
},
Poststate: types.Leaf{
Input: leafData(invalidIdx),
Index: big.NewInt(int64(invalidIdx)),
StateCommitment: commitments[invalidIdx],
},
}
}
type testInputs struct {
name string
commitments func() []common.Hash
expected types.Challenge
expectedErr error
}
tests := []testInputs{
{
name: "Valid",
commitments: validCommitments,
expectedErr: ErrValid,
},
{
name: "IncorrectFirstLeaf",
commitments: func() []common.Hash {
commitments := validCommitments()
commitments[0] = common.Hash{0xaa}
return commitments
},
expected: types.Challenge{
StateMatrix: NewStateMatrix().PackState(),
Prestate: types.Leaf{},
Poststate: types.Leaf{
Input: leafData(0),
Index: big.NewInt(int64(0)),
StateCommitment: common.Hash{0xaa},
},
},
},
}
for i := 1; i < len(preimage)/types.BlockSize; i++ {
commitments := validCommitments()
commitments[i] = common.Hash{0xaa}
tests = append(tests, testInputs{
name: fmt.Sprintf("Incorrect-%v", i),
commitments: func() []common.Hash {
return commitments
},
expected: challengeLeaf(commitments, i),
})
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
challenge, err := Challenge(bytes.NewReader(preimage), test.commitments())
require.ErrorIs(t, err, test.expectedErr)
require.Equal(t, test.expected, challenge)
})
}
}
func TestVerifyPreimage_DataMultipleOfBlockSize(t *testing.T) {
preimage := testutils.RandomData(rand.New(rand.NewSource(2323)), 5*types.BlockSize)
valid, err := NewStateMatrix().AbsorbUpTo(bytes.NewReader(preimage), 1000*types.BlockSize)
require.ErrorIs(t, err, io.EOF, "Should read all preimage data")
_, err = Challenge(bytes.NewReader(preimage), valid.Commitments)
require.ErrorIs(t, err, ErrValid)
}
func TestVerifyPreimage_TooManyCommitments(t *testing.T) {
data := []byte{1}
valid, err := NewStateMatrix().AbsorbUpTo(bytes.NewReader(data[:]), 10*types.BlockSize)
require.ErrorIs(t, err, io.EOF)
commitments := append(valid.Commitments, common.Hash{0xaa})
_, err = Challenge(bytes.NewReader(data), commitments)
require.ErrorIs(t, err, ErrIncorrectCommitmentCount)
}
func TestVerifyPreimage_TooFewCommitments(t *testing.T) {
data := [types.BlockSize * 3]byte{}
valid, err := NewStateMatrix().AbsorbUpTo(bytes.NewReader(data[:]), 10*types.BlockSize)
require.ErrorIs(t, err, io.EOF)
commitments := valid.Commitments[:len(valid.Commitments)-1]
_, err = Challenge(bytes.NewReader(data[:]), commitments)
require.ErrorIs(t, err, ErrIncorrectCommitmentCount)
}
func FuzzKeccak(f *testing.F) {
f.Fuzz(func(t *testing.T, number, time uint64, data []byte) {
s := NewStateMatrix()
......
......@@ -849,5 +849,17 @@
"0xcd4659f1f12afa5949b88198029fc204f63a18d6d4b00c4f8c216019cda7653c",
"0x55b83209b05d78020837a302f30f74151e1c871be2ae0f43d6e3d239a05a6416"
]
},
{
"input": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAX6bu6pD+hu97cVqUkoazqhtXPFvnwPFy/jQjR+LFOXFZvvjCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlRkem0+AIzvEmyqbgj5pbDQsr0gtBBZYN/IwUYeggMVEhM/rDKEiLf1Z5E/xs1a5ukkKJwXBVAioEmG81qwOBn9FqHWIrclhCUpqZRfRIZPp8ZXl64RTSIXhGibMujQipVQuXUsAOd4QE4HjirdI9SfzsLMesYimfC/eedEkhjL0z9+sjhgp5wJY84LBiNI6ZwrzNGke/zdySctbfKbgj2q6ECX3jy6lbeiNThEdLfO1t+IHM7rijOlNw/r8Up3YgH5ExUuuAOUJ5B8OuPXyMfnl484FISrdt0XGpNJpS66b1XHmf7p3TmFLlnO7/23uN57KiNdpe0RaB8R+Y5IzmcWrzR67wcbjbBTbjbAD6w1QIWVx6iUVSfciQIgw7WdYK1C6PXxUMEflpAnV0POZSMu++sbYrxJhlGArYgVbSH4i93eENLOJytOfnKXYzirqsHyCjHa55FqYBqC4ogkXXHBIzE90x9n2bNpc2IHYXIUfDcrkZSQFBnyze2uRsYx8dBXi+Pd2pLSFc3l0WnUCRXc4LCMFKEKWy/6fPHAUTy569sNCQd1E19Fx96UAQOsLRAtiHKp9XwCQYFhnPY2j61MYnsnV1Eh+cBc4J8ofY4mK4r7OBY9ze9+qrP95CCxyDJUIHDzDySR05MlXTJwwmA1a2X4r45shLLgZEF6EWsLwSvibme0tjvzczsfIwNGAmvg2gx4Uajr6kBelBRAobURalVBBx0SLghEjf1pXftmDkmRJipoV7tUVNK4tYEm5VVAZ95AA98=",
"commitments": [
"0xee0a1a26c607ab52c6308165995365f7951a185fccca4b76c847b8860d9fea7a",
"0xbedfbe751c8d3913a76bb030bdcad312ce8c70aeabfaefcf491dafe740946ca5",
"0x74393cbd5f8764be54385a8c7f9442358ae2580f33aebc2597ffa42949172a34",
"0xc881ce64585de627f538376cae9513ca5f4efaa2ab255ce3fa5b15e890223982",
"0xa6ad654807a4f8de3b78d7fa389dc3bd31bbbb5bef24e8e8174f75c6f9ac5c6d",
"0xf6785ff09114b843cccff1e119100ca5ec235725ef2763573e1f3063bf48b886",
"0x20642552f0e7ef28ff28252b3839f0fa323f46792bd103baa9778684f737cda3"
]
}
]
......@@ -4,13 +4,14 @@ import (
"context"
"sync"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type Verifier interface {
Verify(ctx context.Context, blockHash common.Hash, oracle keccakTypes.LargePreimageOracle, preimage keccakTypes.LargePreimageMetaData) error
Verify(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, preimage keccakTypes.LargePreimageMetaData) error
}
type LargePreimageScheduler struct {
......
......@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/testlog"
......@@ -95,7 +96,7 @@ type stubVerifier struct {
verified []keccakTypes.LargePreimageMetaData
}
func (s *stubVerifier) Verify(_ context.Context, _ common.Hash, _ keccakTypes.LargePreimageOracle, image keccakTypes.LargePreimageMetaData) error {
func (s *stubVerifier) Verify(_ context.Context, _ common.Hash, _ fetcher.Oracle, image keccakTypes.LargePreimageMetaData) error {
s.m.Lock()
defer s.m.Unlock()
s.verified = append(s.verified, image)
......
......@@ -68,6 +68,17 @@ func (m LargePreimageMetaData) ShouldVerify() bool {
return m.Timestamp > 0 && !m.Countered
}
type Challenge struct {
// StateMatrix is the packed state matrix preimage of the StateCommitment in Prestate
StateMatrix []byte // TODO(client-pod#480): Need a better representation of this
// Prestate is the valid leaf immediately prior to the first invalid leaf
Prestate Leaf
// Poststate is the first invalid leaf in the preimage. The challenge claims that this leaf is invalid.
Poststate Leaf
}
type LargePreimageOracle interface {
Addr() common.Address
GetActivePreimages(ctx context.Context, blockHash common.Hash) ([]LargePreimageMetaData, error)
......
package keccak
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/matrix"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -14,6 +18,8 @@ type Fetcher interface {
FetchInputs(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, ident keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error)
}
var ErrNotImplemented = errors.New("verify implementation not complete")
type PreimageVerifier struct {
log log.Logger
fetcher Fetcher
......@@ -26,10 +32,23 @@ func NewPreimageVerifier(logger log.Logger, fetcher Fetcher) *PreimageVerifier {
}
}
func (v *PreimageVerifier) Verify(ctx context.Context, blockHash common.Hash, oracle keccakTypes.LargePreimageOracle, preimage keccakTypes.LargePreimageMetaData) error {
_, err := v.fetcher.FetchInputs(ctx, blockHash, oracle, preimage.LargePreimageIdent)
func (v *PreimageVerifier) Verify(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, preimage keccakTypes.LargePreimageMetaData) error {
inputs, err := v.fetcher.FetchInputs(ctx, blockHash, oracle, preimage.LargePreimageIdent)
if err != nil {
return fmt.Errorf("failed to fetch leaves: %w", err)
}
return nil
readers := make([]io.Reader, 0, len(inputs))
var commitments []common.Hash
for _, input := range inputs {
readers = append(readers, bytes.NewReader(input.Input))
commitments = append(commitments, input.Commitments...)
}
_, err = matrix.Challenge(io.MultiReader(readers...), commitments)
if errors.Is(err, matrix.ErrValid) {
return nil
} else if err != nil {
return fmt.Errorf("failed to verify preimage: %w", err)
}
// TODO(client-pod#480): Implement sending the challenge transaction
return ErrNotImplemented
}
package keccak
import (
"bytes"
"context"
"errors"
"io"
"math/rand"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/matrix"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
func TestVerify(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
tests := []struct {
name string
inputs func() []keccakTypes.InputData
expectedErr error
}{
{
name: "Valid-SingleInput",
inputs: func() []keccakTypes.InputData { return validInputs(t, 1) },
},
{
name: "Valid-MultipleInputs",
inputs: func() []keccakTypes.InputData { return validInputs(t, 3) },
},
{
name: "Invalid-FirstCommitment",
inputs: func() []keccakTypes.InputData {
inputs := validInputs(t, 1)
inputs[0].Commitments[0] = common.Hash{0xaa}
return inputs
},
expectedErr: ErrNotImplemented,
},
{
name: "Invalid-MiddleCommitment",
inputs: func() []keccakTypes.InputData {
inputs := validInputs(t, 1)
inputs[0].Commitments[1] = common.Hash{0xaa}
return inputs
},
expectedErr: ErrNotImplemented,
},
{
name: "Invalid-LastCommitment",
inputs: func() []keccakTypes.InputData {
inputs := validInputs(t, 3)
inputs[2].Commitments[len(inputs[2].Commitments)-1] = common.Hash{0xaa}
return inputs
},
expectedErr: ErrNotImplemented,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
fetcher := &stubFetcher{
inputs: test.inputs(),
}
verifier := NewPreimageVerifier(logger, fetcher)
preimage := keccakTypes.LargePreimageMetaData{}
err := verifier.Verify(context.Background(), common.Hash{0xff}, &stubOracle{}, preimage)
require.ErrorIs(t, err, test.expectedErr)
})
}
}
func validInputs(t *testing.T, inputCount int) []keccakTypes.InputData {
chunkSize := 2 * keccakTypes.BlockSize
data := testutils.RandomData(rand.New(rand.NewSource(4444)), inputCount*chunkSize)
var calls []keccakTypes.InputData
in := bytes.NewReader(data)
s := matrix.NewStateMatrix()
for {
call, err := s.AbsorbUpTo(in, chunkSize)
if !errors.Is(err, io.EOF) {
require.NoError(t, err)
}
calls = append(calls, call)
if errors.Is(err, io.EOF) {
break
}
}
return calls
}
type stubFetcher struct {
inputs []keccakTypes.InputData
}
func (s *stubFetcher) FetchInputs(_ context.Context, _ common.Hash, _ fetcher.Oracle, _ keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error) {
return s.inputs, nil
}
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