diff --git a/op-e2e/actions/l2_batcher.go b/op-e2e/actions/l2_batcher.go index 46216d56c6af..5eb96ee110ac 100644 --- a/op-e2e/actions/l2_batcher.go +++ b/op-e2e/actions/l2_batcher.go @@ -45,7 +45,7 @@ type L1TxAPI interface { } type PlasmaInputSetter interface { - SetInput(ctx context.Context, img []byte) (plasma.Keccak256Commitment, error) + SetInput(ctx context.Context, img []byte) (plasma.CommitmentData, error) } type BatcherCfg struct { diff --git a/op-e2e/actions/plasma_test.go b/op-e2e/actions/plasma_test.go index 61abdc6cdaa5..0a39495fb510 100644 --- a/op-e2e/actions/plasma_test.go +++ b/op-e2e/actions/plasma_test.go @@ -227,7 +227,7 @@ func (a *L2PlasmaDA) ActResolveInput(t Testing, comm []byte, input []byte, bn ui func (a *L2PlasmaDA) ActResolveLastChallenge(t Testing) { // remove derivation byte prefix - input, err := a.storage.GetInput(t.Ctx(), a.lastComm[1:]) + input, err := a.storage.GetInput(t.Ctx(), plasma.Keccak256Commitment(a.lastComm[1:])) require.NoError(t, err) a.ActResolveInput(t, a.lastComm, input, a.lastCommBn) @@ -458,7 +458,7 @@ func TestPlasma_SequencerStalledMultiChallenges(gt *testing.T) { // keep track of the related commitment comm1 := a.lastComm - input1, err := a.storage.GetInput(t.Ctx(), comm1[1:]) + input1, err := a.storage.GetInput(t.Ctx(), plasma.Keccak256Commitment(comm1[1:])) bn1 := a.lastCommBn require.NoError(t, err) @@ -503,7 +503,7 @@ func TestPlasma_SequencerStalledMultiChallenges(gt *testing.T) { // keep track of the second commitment comm2 := a.lastComm - _, err = a.storage.GetInput(t.Ctx(), comm2[1:]) + _, err = a.storage.GetInput(t.Ctx(), plasma.Keccak256Commitment(comm2[1:])) require.NoError(t, err) a.lastCommBn = a.miner.l1Chain.CurrentBlock().Number.Uint64() diff --git a/op-node/rollup/derive/data_source.go b/op-node/rollup/derive/data_source.go index 2a1dbd7c00de..12ac6e6f1440 100644 --- a/op-node/rollup/derive/data_source.go +++ b/op-node/rollup/derive/data_source.go @@ -28,7 +28,7 @@ type L1BlobsFetcher interface { type PlasmaInputFetcher interface { // GetInput fetches the input for the given commitment at the given block number from the DA storage service. - GetInput(ctx context.Context, l1 plasma.L1Fetcher, c plasma.Keccak256Commitment, blockId eth.BlockID) (eth.Data, error) + GetInput(ctx context.Context, l1 plasma.L1Fetcher, c plasma.CommitmentData, blockId eth.BlockID) (eth.Data, error) // AdvanceL1Origin advances the L1 origin to the given block number, syncing the DA challenge events. AdvanceL1Origin(ctx context.Context, l1 plasma.L1Fetcher, blockId eth.BlockID) error // Reset the challenge origin in case of L1 reorg diff --git a/op-node/rollup/derive/plasma_data_source.go b/op-node/rollup/derive/plasma_data_source.go index e6b9ad7dbfe1..1d00902ca9c4 100644 --- a/op-node/rollup/derive/plasma_data_source.go +++ b/op-node/rollup/derive/plasma_data_source.go @@ -19,7 +19,7 @@ type PlasmaDataSource struct { l1 L1Fetcher id eth.BlockID // keep track of a pending commitment so we can keep trying to fetch the input. - comm plasma.Keccak256Commitment + comm plasma.CommitmentData } func NewPlasmaDataSource(log log.Logger, src DataIter, l1 L1Fetcher, fetcher PlasmaInputFetcher, id eth.BlockID) *PlasmaDataSource { @@ -61,10 +61,17 @@ func (s *PlasmaDataSource) Next(ctx context.Context) (eth.Data, error) { } // validate batcher inbox data is a commitment. - comm, err := plasma.DecodeKeccak256(data[1:]) + // strip the transaction data version byte from the data before decoding. + comm, err := plasma.DecodeCommitmentData(data[1:]) if err != nil { s.log.Warn("invalid commitment", "commitment", data, "err", err) - return s.Next(ctx) + return nil, NotEnoughData + } + // only support keccak256 commitments for now. + // TODO: support other commitment types via flag + if comm.CommitmentType() != plasma.Keccak256CommitmentType { + s.log.Warn("wrong commitment type", "commitmentType", comm.CommitmentType()) + return nil, NotEnoughData } s.comm = comm } diff --git a/op-node/rollup/derive/plasma_data_source_test.go b/op-node/rollup/derive/plasma_data_source_test.go index 0769eb504a11..1e3f4d4375a8 100644 --- a/op-node/rollup/derive/plasma_data_source_test.go +++ b/op-node/rollup/derive/plasma_data_source_test.go @@ -123,8 +123,10 @@ func TestPlasmaDataSource(t *testing.T) { // mock input commitments in l1 transactions input := testutils.RandomData(rng, 2000) comm, _ := storage.SetInput(ctx, input) + // plasma da tests are designed for keccak256 commitments, so we type assert here + kComm := comm.(plasma.Keccak256Commitment) inputs = append(inputs, input) - comms = append(comms, comm) + comms = append(comms, kComm) tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ ChainID: signer.ChainID(), @@ -223,8 +225,10 @@ func TestPlasmaDataSource(t *testing.T) { // mock input commitments in l1 transactions input := testutils.RandomData(rng, 2000) comm, _ := storage.SetInput(ctx, input) + // plasma da tests are designed for keccak256 commitments, so we type assert here + kComm := comm.(plasma.Keccak256Commitment) inputs = append(inputs, input) - comms = append(comms, comm) + comms = append(comms, kComm) tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ ChainID: signer.ChainID(), diff --git a/op-plasma/commitment.go b/op-plasma/commitment.go index 6f45bd4f8429..7d1e310d4d35 100644 --- a/op-plasma/commitment.go +++ b/op-plasma/commitment.go @@ -16,12 +16,82 @@ var ErrCommitmentMismatch = errors.New("commitment mismatch") // CommitmentType is the commitment type prefix. type CommitmentType byte -// KeccakCommitmentType is the default commitment type for the DA storage. -const Keccak256CommitmentType CommitmentType = 0 +// CommitmentType describes the binary format of the commitment. +// KeccakCommitmentType is the default commitment type for the centralized DA storage. +// GenericCommitmentType indicates an opaque bytestring that the op-node never opens. +const ( + Keccak256CommitmentType CommitmentType = 0 + GenericCommitmentType CommitmentType = 1 +) + +// CommitmentData is the binary representation of a commitment. +type CommitmentData interface { + CommitmentType() CommitmentType + Encode() []byte + TxData() []byte + Verify(input []byte) error +} -// Keccak256Commitment is the default commitment type for op-plasma. +// Keccak256Commitment is an implementation of CommitmentData that uses Keccak256 as the commitment function. type Keccak256Commitment []byte +// GenericCommitment is an implementation of CommitmentData that treats the commitment as an opaque bytestring. +type GenericCommitment []byte + +// NewCommitmentData creates a new commitment from the given input and desired type. +func NewCommitmentData(t CommitmentType, input []byte) CommitmentData { + switch t { + case Keccak256CommitmentType: + return NewKeccak256Commitment(input) + case GenericCommitmentType: + return NewGenericCommitment(input) + default: + return nil + } +} + +// DecodeCommitmentData parses the commitment into a known commitment type. +// The input type is determined by the first byte of the raw data. +// The input type is discarded and the commitment is passed to the appropriate constructor. +func DecodeCommitmentData(input []byte) (CommitmentData, error) { + if len(input) == 0 { + return nil, ErrInvalidCommitment + } + t := CommitmentType(input[0]) + data := input[1:] + switch t { + case Keccak256CommitmentType: + return DecodeKeccak256(data) + case GenericCommitmentType: + return DecodeGenericCommitment(data) + default: + return nil, ErrInvalidCommitment + } +} + +// NewKeccak256Commitment creates a new commitment from the given input. +func NewKeccak256Commitment(input []byte) Keccak256Commitment { + return Keccak256Commitment(crypto.Keccak256(input)) +} + +// DecodeKeccak256 validates and casts the commitment into a Keccak256Commitment. +func DecodeKeccak256(commitment []byte) (Keccak256Commitment, error) { + // guard against empty commitments + if len(commitment) == 0 { + return nil, ErrInvalidCommitment + } + // keccak commitments are always 32 bytes + if len(commitment) != 32 { + return nil, ErrInvalidCommitment + } + return commitment, nil +} + +// CommitmentType returns the commitment type of Keccak256. +func (c Keccak256Commitment) CommitmentType() CommitmentType { + return Keccak256CommitmentType +} + // Encode adds a commitment type prefix self describing the commitment. func (c Keccak256Commitment) Encode() []byte { return append([]byte{byte(Keccak256CommitmentType)}, c...) @@ -40,22 +110,35 @@ func (c Keccak256Commitment) Verify(input []byte) error { return nil } -// Keccak256 creates a new commitment from the given input. -func Keccak256(input []byte) Keccak256Commitment { - return Keccak256Commitment(crypto.Keccak256(input)) +// NewGenericCommitment creates a new commitment from the given input. +func NewGenericCommitment(input []byte) GenericCommitment { + return GenericCommitment(input) } -// DecodeKeccak256 validates and casts the commitment into a Keccak256Commitment. -func DecodeKeccak256(commitment []byte) (Keccak256Commitment, error) { +// DecodeGenericCommitment validates and casts the commitment into a GenericCommitment. +func DecodeGenericCommitment(commitment []byte) (GenericCommitment, error) { if len(commitment) == 0 { return nil, ErrInvalidCommitment } - if commitment[0] != byte(Keccak256CommitmentType) { - return nil, ErrInvalidCommitment - } - c := commitment[1:] - if len(c) != 32 { - return nil, ErrInvalidCommitment - } - return c, nil + return commitment[:], nil +} + +// CommitmentType returns the commitment type of Generic Commitment. +func (c GenericCommitment) CommitmentType() CommitmentType { + return GenericCommitmentType +} + +// Encode adds a commitment type prefix self describing the commitment. +func (c GenericCommitment) Encode() []byte { + return append([]byte{byte(GenericCommitmentType)}, c...) +} + +// TxData adds an extra version byte to signal it's a commitment. +func (c GenericCommitment) TxData() []byte { + return append([]byte{TxDataVersion1}, c.Encode()...) +} + +// Verify always returns true for GenericCommitment because the DA Server must validate the data before returning it to the op-node. +func (c GenericCommitment) Verify(input []byte) error { + return nil } diff --git a/op-plasma/commitment_test.go b/op-plasma/commitment_test.go new file mode 100644 index 000000000000..96d845f8c63a --- /dev/null +++ b/op-plasma/commitment_test.go @@ -0,0 +1,69 @@ +package plasma + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestCommitmentData tests the CommitmentData type and its implementations, +// by encoding and decoding the commitment data and verifying the input data. +func TestCommitmentData(t *testing.T) { + + type tcase struct { + name string + commType CommitmentType + commData []byte + expectedErr error + } + + testCases := []tcase{ + { + name: "valid keccak256 commitment", + commType: Keccak256CommitmentType, + commData: []byte("abcdefghijklmnopqrstuvwxyz012345"), + expectedErr: ErrInvalidCommitment, + }, + { + name: "invalid keccak256 commitment", + commType: Keccak256CommitmentType, + commData: []byte("ab_baddata_yz012345"), + expectedErr: ErrInvalidCommitment, + }, + { + name: "valid generic commitment", + commType: GenericCommitmentType, + commData: []byte("any length of data! wow, that's so generic!"), + expectedErr: ErrInvalidCommitment, + }, + { + name: "invalid commitment type", + commType: 9, + commData: []byte("abcdefghijklmnopqrstuvwxyz012345"), + expectedErr: ErrInvalidCommitment, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + comm, err := DecodeCommitmentData(tc.commData) + require.ErrorIs(t, err, tc.expectedErr) + if err == nil { + // Test that the commitment type is correct + require.Equal(t, tc.commType, comm.CommitmentType()) + // Test that reencoding the commitment returns the same data + require.Equal(t, tc.commData, comm.Encode()) + // Test that TxData() returns the same data as the original, prepended with a version byte + require.Equal(t, append([]byte{TxDataVersion1}, tc.commData...), comm.TxData()) + + // Test that Verify() returns no error for the correct data + require.NoError(t, comm.Verify(tc.commData)) + // Test that Verify() returns error for the incorrect data + // don't do this for GenericCommitmentType, which does not do any verification + if tc.commType != GenericCommitmentType { + require.ErrorIs(t, ErrCommitmentMismatch, comm.Verify([]byte("wrong data"))) + } + } + }) + } +} diff --git a/op-plasma/daclient.go b/op-plasma/daclient.go index 2ba58d8a2f08..eae7d2708221 100644 --- a/op-plasma/daclient.go +++ b/op-plasma/daclient.go @@ -30,7 +30,7 @@ func NewDAClient(url string, verify bool) *DAClient { } // GetInput returns the input data for the given encoded commitment bytes. -func (c *DAClient) GetInput(ctx context.Context, comm Keccak256Commitment) ([]byte, error) { +func (c *DAClient) GetInput(ctx context.Context, comm CommitmentData) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/get/0x%x", c.url, comm.Encode()), nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) @@ -60,11 +60,12 @@ func (c *DAClient) GetInput(ctx context.Context, comm Keccak256Commitment) ([]by } // SetInput sets the input data and returns the keccak256 hash commitment. -func (c *DAClient) SetInput(ctx context.Context, img []byte) (Keccak256Commitment, error) { +func (c *DAClient) SetInput(ctx context.Context, img []byte) (CommitmentData, error) { if len(img) == 0 { return nil, ErrInvalidInput } - comm := Keccak256(img) + // TODO(#10312): this is hard-coded to produce Keccak256 commitments + comm := NewCommitmentData(Keccak256CommitmentType, img) // encode with commitment type prefix key := comm.Encode() body := bytes.NewReader(img) diff --git a/op-plasma/daclient_test.go b/op-plasma/daclient_test.go index bc2bd0ee3763..636756ef38a9 100644 --- a/op-plasma/daclient_test.go +++ b/op-plasma/daclient_test.go @@ -70,7 +70,7 @@ func TestDAClient(t *testing.T) { comm, err := client.SetInput(ctx, input) require.NoError(t, err) - require.Equal(t, comm, Keccak256(input)) + require.Equal(t, comm, NewKeccak256Commitment(input)) stored, err := client.GetInput(ctx, comm) require.NoError(t, err) @@ -84,7 +84,7 @@ func TestDAClient(t *testing.T) { require.ErrorIs(t, err, ErrCommitmentMismatch) // test not found error - comm = Keccak256(RandomData(rng, 32)) + comm = NewKeccak256Commitment(RandomData(rng, 32)) _, err = client.GetInput(ctx, comm) require.ErrorIs(t, err, ErrNotFound) @@ -97,6 +97,6 @@ func TestDAClient(t *testing.T) { _, err = client.SetInput(ctx, input) require.Error(t, err) - _, err = client.GetInput(ctx, Keccak256(input)) + _, err = client.GetInput(ctx, NewKeccak256Commitment(input)) require.Error(t, err) } diff --git a/op-plasma/damgr.go b/op-plasma/damgr.go index 533532b76b17..703229ef2a18 100644 --- a/op-plasma/damgr.go +++ b/op-plasma/damgr.go @@ -40,8 +40,8 @@ type L1Fetcher interface { // DAStorage interface for calling the DA storage server. type DAStorage interface { - GetInput(ctx context.Context, key Keccak256Commitment) ([]byte, error) - SetInput(ctx context.Context, img []byte) (Keccak256Commitment, error) + GetInput(ctx context.Context, key CommitmentData) ([]byte, error) + SetInput(ctx context.Context, img []byte) (CommitmentData, error) } // HeadSignalFn is the callback function to accept head-signals without a context. @@ -165,7 +165,7 @@ func (d *DA) Reset(ctx context.Context, base eth.L1BlockRef, baseCfg eth.SystemC // GetInput returns the input data for the given commitment bytes. blockNumber is required to lookup // the challenge status in the DataAvailabilityChallenge L1 contract. -func (d *DA) GetInput(ctx context.Context, l1 L1Fetcher, comm Keccak256Commitment, blockId eth.BlockID) (eth.Data, error) { +func (d *DA) GetInput(ctx context.Context, l1 L1Fetcher, comm CommitmentData, blockId eth.BlockID) (eth.Data, error) { // If the challenge head is ahead in the case of a pipeline reset or stall, we might have synced a // challenge event for this commitment. Otherwise we mark the commitment as part of the canonical // chain so potential future challenge events can be selected. @@ -356,12 +356,12 @@ func (d *DA) fetchChallengeLogs(ctx context.Context, l1 L1Fetcher, block eth.Blo } // decodeChallengeStatus decodes and validates a challenge event from a transaction log, returning the associated commitment bytes. -func (d *DA) decodeChallengeStatus(log *types.Log) (ChallengeStatus, Keccak256Commitment, error) { +func (d *DA) decodeChallengeStatus(log *types.Log) (ChallengeStatus, CommitmentData, error) { event, err := DecodeChallengeStatusEvent(log) if err != nil { return 0, nil, err } - comm, err := DecodeKeccak256(event.ChallengedCommitment) + comm, err := DecodeCommitmentData(event.ChallengedCommitment) if err != nil { return 0, nil, err } diff --git a/op-plasma/damock.go b/op-plasma/damock.go index f21e80df25e0..c43bdbc53e92 100644 --- a/op-plasma/damock.go +++ b/op-plasma/damock.go @@ -14,18 +14,20 @@ import ( // MockDAClient mocks a DA storage provider to avoid running an HTTP DA server // in unit tests. type MockDAClient struct { - store ethdb.KeyValueStore - log log.Logger + CommitmentType CommitmentType + store ethdb.KeyValueStore + log log.Logger } func NewMockDAClient(log log.Logger) *MockDAClient { return &MockDAClient{ - store: memorydb.New(), - log: log, + CommitmentType: Keccak256CommitmentType, + store: memorydb.New(), + log: log, } } -func (c *MockDAClient) GetInput(ctx context.Context, key Keccak256Commitment) ([]byte, error) { +func (c *MockDAClient) GetInput(ctx context.Context, key CommitmentData) ([]byte, error) { bytes, err := c.store.Get(key.Encode()) if err != nil { return nil, ErrNotFound @@ -33,8 +35,8 @@ func (c *MockDAClient) GetInput(ctx context.Context, key Keccak256Commitment) ([ return bytes, nil } -func (c *MockDAClient) SetInput(ctx context.Context, data []byte) (Keccak256Commitment, error) { - key := Keccak256(data) +func (c *MockDAClient) SetInput(ctx context.Context, data []byte) (CommitmentData, error) { + key := NewCommitmentData(c.CommitmentType, data) return key, c.store.Put(key.Encode(), data) } @@ -49,7 +51,7 @@ type DAErrFaker struct { setInputErr error } -func (f *DAErrFaker) GetInput(ctx context.Context, key Keccak256Commitment) ([]byte, error) { +func (f *DAErrFaker) GetInput(ctx context.Context, key CommitmentData) ([]byte, error) { if err := f.getInputErr; err != nil { f.getInputErr = nil return nil, err @@ -57,7 +59,7 @@ func (f *DAErrFaker) GetInput(ctx context.Context, key Keccak256Commitment) ([]b return f.Client.GetInput(ctx, key) } -func (f *DAErrFaker) SetInput(ctx context.Context, data []byte) (Keccak256Commitment, error) { +func (f *DAErrFaker) SetInput(ctx context.Context, data []byte) (CommitmentData, error) { if err := f.setInputErr; err != nil { f.setInputErr = nil return nil, err @@ -80,7 +82,7 @@ var ErrNotEnabled = errors.New("plasma not enabled") // PlasmaDisabled is a noop plasma DA implementation for stubbing. type PlasmaDisabled struct{} -func (d *PlasmaDisabled) GetInput(ctx context.Context, l1 L1Fetcher, commitment Keccak256Commitment, blockId eth.BlockID) (eth.Data, error) { +func (d *PlasmaDisabled) GetInput(ctx context.Context, l1 L1Fetcher, commitment CommitmentData, blockId eth.BlockID) (eth.Data, error) { return nil, ErrNotEnabled }