Skip to content

Commit

Permalink
Plasma: Abstract CommitmentData to Interface (#10458)
Browse files Browse the repository at this point in the history
* op-node: Generic Commitment

This is a generic commitment to be used by the op-node & op-batcher.

* abstract commitments to CommitmentData interface

* correct byte-stripping ; add tests ; finish wiring

* make GenericCommitment always verify

* correct action tests

* PR comments

* fix unit test

* remove fmt.Println

---------

Co-authored-by: Joshua Gutow <jgutow@oplabs.co>
  • Loading branch information
axelKingsley and trianglesphere authored May 10, 2024
1 parent 0b4077e commit 1a20fd9
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 47 deletions.
2 changes: 1 addition & 1 deletion op-e2e/actions/l2_batcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions op-e2e/actions/plasma_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion op-node/rollup/derive/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions op-node/rollup/derive/plasma_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 6 additions & 2 deletions op-node/rollup/derive/plasma_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
115 changes: 99 additions & 16 deletions op-plasma/commitment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand All @@ -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
}
69 changes: 69 additions & 0 deletions op-plasma/commitment_test.go
Original file line number Diff line number Diff line change
@@ -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")))
}
}
})
}
}
7 changes: 4 additions & 3 deletions op-plasma/daclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions op-plasma/daclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -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)
}
10 changes: 5 additions & 5 deletions op-plasma/damgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 1a20fd9

Please sign in to comment.