diff --git a/test/dummy.go b/test/dummy.go index 83adbbf..f064cb2 100644 --- a/test/dummy.go +++ b/test/dummy.go @@ -5,7 +5,9 @@ import ( "context" "encoding/hex" "errors" + "fmt" "math" + "reflect" "sync" "time" @@ -105,18 +107,14 @@ func (d *DummySequencer) GetNextBatch(ctx context.Context, req sequencing.GetNex lastBatchHash := d.lastBatchHash d.lastBatchHashMutex.RUnlock() - if lastBatchHash == nil && req.LastBatchHash != nil { - return nil, errors.New("lastBatch is supposed to be nil") - } else if lastBatchHash != nil && req.LastBatchHash == nil { - return nil, errors.New("lastBatch is not supposed to be nil") - } else if !bytes.Equal(lastBatchHash, req.LastBatchHash) { - return nil, errors.New("supplied lastBatch does not match with sequencer last batch") + if !reflect.DeepEqual(lastBatchHash, req.LastBatchHash) { + return nil, fmt.Errorf("batch hash mismatch: lastBatchHash = %x, req.LastBatchHash = %x", lastBatchHash, req.LastBatchHash) } batch := d.tq.GetNextBatch(req.MaxBytes) batchRes := &sequencing.GetNextBatchResponse{Batch: batch, Timestamp: now} // If there are no transactions, return empty batch without updating the last batch hash - if batch.Transactions == nil { + if len(batch.Transactions) == 0 { return batchRes, nil } diff --git a/test/dummy_test.go b/test/dummy_test.go index 7ef7175..4d072cb 100644 --- a/test/dummy_test.go +++ b/test/dummy_test.go @@ -2,6 +2,9 @@ package test import ( "context" + "crypto/rand" + "fmt" + "io" "math" "testing" "time" @@ -14,7 +17,8 @@ import ( func TestTransactionQueue_AddTransaction(t *testing.T) { queue := NewTransactionQueue() - tx1 := []byte("transaction_1") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) queue.AddTransaction(tx1) // Check that the transaction was added @@ -27,8 +31,10 @@ func TestTransactionQueue_GetNextBatch(t *testing.T) { queue := NewTransactionQueue() // Add multiple transactions - tx1 := []byte("transaction_1") - tx2 := []byte("transaction_2") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx2, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) queue.AddTransaction(tx1) queue.AddTransaction(tx2) @@ -46,7 +52,8 @@ func TestTransactionQueue_GetNextBatch(t *testing.T) { func TestDummySequencer_SubmitRollupTransaction(t *testing.T) { // Define a test rollup ID and transaction rollupId := []byte("test_rollup_id") - tx := []byte("test_transaction") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) sequencer := NewDummySequencer(rollupId) // Submit a transaction @@ -92,9 +99,12 @@ func TestDummySequencer_SubmitEmptyTransaction(t *testing.T) { func TestDummySequencer_SubmitMultipleTransactions(t *testing.T) { // Define a test rollup ID and multiple transactions rollupId := []byte("test_rollup_id") - tx1 := []byte("transaction_1") - tx2 := []byte("transaction_2") - tx3 := []byte("transaction_3") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx2, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx3, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) sequencer := NewDummySequencer(rollupId) // Submit multiple transactions @@ -111,7 +121,7 @@ func TestDummySequencer_SubmitMultipleTransactions(t *testing.T) { Tx: tx3, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req1) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req1) assert.NoError(t, err) _, err = sequencer.SubmitRollupTransaction(context.Background(), req2) assert.NoError(t, err) @@ -129,13 +139,14 @@ func TestDummySequencer_SubmitMultipleTransactions(t *testing.T) { func TestDummySequencer_GetNextBatch(t *testing.T) { // Add a transaction to the queue rollupId := []byte("test_rollup_id") - tx := []byte("test_transaction") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) sequencer := NewDummySequencer(rollupId) req := sequencing.SubmitRollupTransactionRequest{ RollupId: rollupId, Tx: tx, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) assert.NoError(t, err) // Retrieve the next batch @@ -178,12 +189,13 @@ func TestDummySequencer_GetNextBatch_LastBatchHashMismatch(t *testing.T) { // Submit a transaction rollupId := []byte("test_rollup_id") sequencer := NewDummySequencer(rollupId) - tx := []byte("test_transaction") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) req := sequencing.SubmitRollupTransactionRequest{ RollupId: rollupId, Tx: tx, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) assert.NoError(t, err) // Retrieve the next batch @@ -195,7 +207,7 @@ func TestDummySequencer_GetNextBatch_LastBatchHashMismatch(t *testing.T) { // Assert that the batch hash mismatch error is returned assert.Error(t, err) - assert.Equal(t, "lastBatch is supposed to be nil", err.Error()) + assert.ErrorContains(t, err, "batch hash mismatch", "unexpected error message") } // Test retrieving a batch with maxBytes limit @@ -203,9 +215,12 @@ func TestDummySequencer_GetNextBatch_MaxBytesLimit(t *testing.T) { // Define a test rollup ID and multiple transactions rollupId := []byte("test_rollup_id") sequencer := NewDummySequencer(rollupId) - tx1 := []byte("transaction_1") - tx2 := []byte("transaction_2") - tx3 := []byte("transaction_3") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx2, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx3, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) // Submit multiple transactions req1 := sequencing.SubmitRollupTransactionRequest{ @@ -221,7 +236,7 @@ func TestDummySequencer_GetNextBatch_MaxBytesLimit(t *testing.T) { Tx: tx3, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req1) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req1) assert.NoError(t, err) _, err = sequencer.SubmitRollupTransaction(context.Background(), req2) assert.NoError(t, err) @@ -267,12 +282,13 @@ func TestDummySequencer_VerifyBatch(t *testing.T) { // Add and retrieve a batch rollupId := []byte("test_rollup_id") sequencer := NewDummySequencer(rollupId) - tx := []byte("test_transaction") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) req := sequencing.SubmitRollupTransactionRequest{ RollupId: rollupId, Tx: tx, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) assert.NoError(t, err) // Get the next batch to generate batch hash @@ -320,8 +336,10 @@ func TestDummySequencer_VerifyBatchWithMultipleTransactions(t *testing.T) { // Define a test rollup ID and multiple transactions rollupId := []byte("test_rollup_id") sequencer := NewDummySequencer(rollupId) - tx1 := []byte("transaction_1") - tx2 := []byte("transaction_2") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx2, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) // Submit multiple transactions req1 := sequencing.SubmitRollupTransactionRequest{ @@ -333,7 +351,7 @@ func TestDummySequencer_VerifyBatchWithMultipleTransactions(t *testing.T) { Tx: tx2, } - _, err := sequencer.SubmitRollupTransaction(context.Background(), req1) + _, err = sequencer.SubmitRollupTransaction(context.Background(), req1) assert.NoError(t, err) _, err = sequencer.SubmitRollupTransaction(context.Background(), req2) assert.NoError(t, err) @@ -375,3 +393,16 @@ func TestDummySequencer_VerifyBatch_NotFound(t *testing.T) { assert.NoError(t, err) assert.False(t, verifyResp.Status) } + +// GenerateSecureRandomBytes generates cryptographically secure random bytes of the given length. +func GenerateSecureRandomBytes(length int) ([]byte, error) { + if length <= 0 { + return nil, fmt.Errorf("invalid length: %d, must be greater than 0", length) + } + + buf := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, buf); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + return buf, nil +} diff --git a/test/multi_rollup_sequencer.go b/test/multi_rollup_sequencer.go new file mode 100644 index 0000000..c3897c3 --- /dev/null +++ b/test/multi_rollup_sequencer.go @@ -0,0 +1,126 @@ +package test + +import ( + "context" + "encoding/hex" + "fmt" + "reflect" + "sync" + "time" + + "github.com/rollkit/go-sequencing" +) + +// MultiRollupSequencer is a sequencer for testing that serves multiple rollups +type MultiRollupSequencer struct { + rollups map[string]*RollupData + rollupsMutex sync.RWMutex +} + +// RollupData holds the data for a specific rollup, including its transaction queue, last batch hash, and seen batches. +type RollupData struct { + tq *TransactionQueue + lastBatchHash []byte + lastBatchHashMutex sync.RWMutex + + seenBatches map[string]struct{} + seenBatchesMutex sync.Mutex +} + +// SubmitRollupTransaction implements sequencing.Sequencer. +func (d *MultiRollupSequencer) SubmitRollupTransaction(ctx context.Context, req sequencing.SubmitRollupTransactionRequest) (*sequencing.SubmitRollupTransactionResponse, error) { + rollup, err := d.getOrCreateRollup(req.RollupId) + if err != nil { + return nil, err + } + rollup.tq.AddTransaction(req.Tx) + return &sequencing.SubmitRollupTransactionResponse{}, nil +} + +// GetNextBatch implements sequencing.Sequencer. +func (d *MultiRollupSequencer) GetNextBatch(ctx context.Context, req sequencing.GetNextBatchRequest) (*sequencing.GetNextBatchResponse, error) { + rollup, err := d.getOrCreateRollup(req.RollupId) + if err != nil { + return nil, err + } + + now := time.Now() + rollup.lastBatchHashMutex.RLock() + lastBatchHash := rollup.lastBatchHash + rollup.lastBatchHashMutex.RUnlock() + + if !reflect.DeepEqual(lastBatchHash, req.LastBatchHash) { + return nil, fmt.Errorf("batch hash mismatch: lastBatchHash = %x, req.LastBatchHash = %x", lastBatchHash, req.LastBatchHash) + } + + batch := rollup.tq.GetNextBatch(req.MaxBytes) + batchRes := &sequencing.GetNextBatchResponse{Batch: batch, Timestamp: now} + // If there are no transactions, return empty batch without updating the last batch hash + if len(batch.Transactions) == 0 { + return batchRes, nil + } + + h, err := batch.Hash() + if err != nil { + return nil, err + } + + rollup.lastBatchHashMutex.Lock() + rollup.lastBatchHash = h + rollup.lastBatchHashMutex.Unlock() + + rollup.seenBatchesMutex.Lock() + rollup.seenBatches[hex.EncodeToString(h)] = struct{}{} + rollup.seenBatchesMutex.Unlock() + return batchRes, nil +} + +// VerifyBatch implements sequencing.Sequencer. +func (d *MultiRollupSequencer) VerifyBatch(ctx context.Context, req sequencing.VerifyBatchRequest) (*sequencing.VerifyBatchResponse, error) { + rollup, err := d.getOrCreateRollup(req.RollupId) + if err != nil { + return nil, err + } + + rollup.seenBatchesMutex.Lock() + defer rollup.seenBatchesMutex.Unlock() + key := hex.EncodeToString(req.BatchHash) + if _, exists := rollup.seenBatches[key]; exists { + return &sequencing.VerifyBatchResponse{Status: true}, nil + } + return &sequencing.VerifyBatchResponse{Status: false}, nil +} + +// getOrCreateRollup returns the RollupData for a given rollupId, creating it if necessary. +func (d *MultiRollupSequencer) getOrCreateRollup(rollupId []byte) (*RollupData, error) { + rollupKey := hex.EncodeToString(rollupId) + + d.rollupsMutex.Lock() + defer d.rollupsMutex.Unlock() + rollup, exists := d.rollups[rollupKey] + if exists { + return rollup, nil + } + + // Double-check existence after acquiring write lock + if rollup, exists := d.rollups[rollupKey]; exists { + return rollup, nil + } + + // Create a new RollupData if it doesn't exist + rollup = &RollupData{ + tq: NewTransactionQueue(), + seenBatches: make(map[string]struct{}, 0), + } + d.rollups[rollupKey] = rollup + return rollup, nil +} + +// NewMultiRollupSequencer creates a new MultiRollupSequencer +func NewMultiRollupSequencer() *MultiRollupSequencer { + return &MultiRollupSequencer{ + rollups: make(map[string]*RollupData), + } +} + +var _ sequencing.Sequencer = &MultiRollupSequencer{} diff --git a/test/multi_rollup_sequencer_test.go b/test/multi_rollup_sequencer_test.go new file mode 100644 index 0000000..a5f4345 --- /dev/null +++ b/test/multi_rollup_sequencer_test.go @@ -0,0 +1,212 @@ +package test + +import ( + "context" + "crypto/rand" + "math" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/rollkit/go-sequencing" +) + +func TestMultiRollupSequencer_SubmitRollupTransaction(t *testing.T) { + sequencer := NewMultiRollupSequencer() + + rollupId := []byte("test-rollup") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + + // Submit the transaction + req := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId, + Tx: tx, + } + + res, err := sequencer.SubmitRollupTransaction(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, res) + + // Ensure that the transaction was added to the transaction queue for the rollup + rollup, _ := sequencer.getOrCreateRollup(rollupId) + nextBatch := rollup.tq.GetNextBatch(math.MaxInt32) + + assert.Equal(t, 1, len(nextBatch.Transactions)) + assert.Equal(t, tx, nextBatch.Transactions[0]) +} + +func TestMultiRollupSequencer_GetNextBatch(t *testing.T) { + sequencer := NewMultiRollupSequencer() + + rollupId := []byte("test-rollup") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + + // Submit the transaction + req := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId, + Tx: tx, + } + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) + assert.NoError(t, err) + + // Get next batch + getBatchReq := sequencing.GetNextBatchRequest{ + RollupId: rollupId, + LastBatchHash: nil, + MaxBytes: math.MaxInt32, + } + batchRes, err := sequencer.GetNextBatch(context.Background(), getBatchReq) + assert.NoError(t, err) + + // Verify that the batch contains the transaction + assert.NotNil(t, batchRes.Batch) + assert.Equal(t, 1, len(batchRes.Batch.Transactions)) + assert.Equal(t, tx, batchRes.Batch.Transactions[0]) +} + +func TestMultiRollupSequencer_VerifyBatch(t *testing.T) { + sequencer := NewMultiRollupSequencer() + + rollupId := []byte("test-rollup") + tx, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + + // Submit the transaction + req := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId, + Tx: tx, + } + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) + assert.NoError(t, err) + + // Get the next batch to update the last batch hash + getBatchReq := sequencing.GetNextBatchRequest{ + RollupId: rollupId, + LastBatchHash: nil, + MaxBytes: math.MaxInt32, + } + batchRes, err := sequencer.GetNextBatch(context.Background(), getBatchReq) + assert.NoError(t, err) + + bHash, err := batchRes.Batch.Hash() + assert.NoError(t, err) + + // Verify the batch + verifyReq := sequencing.VerifyBatchRequest{ + RollupId: rollupId, + BatchHash: bHash, + } + + verifyRes, err := sequencer.VerifyBatch(context.Background(), verifyReq) + assert.NoError(t, err) + assert.True(t, verifyRes.Status) +} + +func TestMultiRollupSequencer_MultipleRollups(t *testing.T) { + sequencer := NewMultiRollupSequencer() + + rollupId1 := []byte("rollup-1") + rollupId2 := []byte("rollup-2") + tx1, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + tx2, err := GenerateSecureRandomBytes(32) + assert.NoError(t, err) + + // Submit transactions for two different rollups + req1 := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId1, + Tx: tx1, + } + req2 := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId2, + Tx: tx2, + } + + _, err = sequencer.SubmitRollupTransaction(context.Background(), req1) + assert.NoError(t, err) + + _, err = sequencer.SubmitRollupTransaction(context.Background(), req2) + assert.NoError(t, err) + + // Get next batch for rollup 1 + getBatchReq1 := sequencing.GetNextBatchRequest{ + RollupId: rollupId1, + LastBatchHash: nil, + MaxBytes: math.MaxInt32, + } + batchRes1, err := sequencer.GetNextBatch(context.Background(), getBatchReq1) + assert.NoError(t, err) + + assert.Equal(t, 1, len(batchRes1.Batch.Transactions)) + assert.Equal(t, tx1, batchRes1.Batch.Transactions[0]) + + // Get next batch for rollup 2 + getBatchReq2 := sequencing.GetNextBatchRequest{ + RollupId: rollupId2, + LastBatchHash: nil, + MaxBytes: math.MaxInt32, + } + batchRes2, err := sequencer.GetNextBatch(context.Background(), getBatchReq2) + assert.NoError(t, err) + + assert.Equal(t, 1, len(batchRes2.Batch.Transactions)) + assert.Equal(t, tx2, batchRes2.Batch.Transactions[0]) +} + +// TestMultiRollupSequencer tests the MultiRollupSequencer core functionality of +// submitting transactions, getting the next batch, and verifying a batch +func TestMultiRollupSequencer(t *testing.T) { + // Test with 1 rollup + testMultiRollupSequencer(t, 1) + + // Test with 2 to 5 rollups + r, err := rand.Int(rand.Reader, big.NewInt(4)) // Generates a number between 0 and 3 + assert.NoError(t, err) + + numRollups := int(r.Int64() + 2) // Adjust range to be between 2 and 5, cast to int + testMultiRollupSequencer(t, numRollups) +} + +func testMultiRollupSequencer(t *testing.T, numRollups int) { + sequencer := NewMultiRollupSequencer() + + for i := 0; i < numRollups; i++ { + // Submit a transaction + rollupId, err := GenerateSecureRandomBytes(10) + assert.NoError(t, err) + tx, err := GenerateSecureRandomBytes(30) + assert.NoError(t, err) + req := sequencing.SubmitRollupTransactionRequest{ + RollupId: rollupId, + Tx: tx, + } + _, err = sequencer.SubmitRollupTransaction(context.Background(), req) + assert.NoError(t, err) + + // Get Next Batch + getBatchReq := sequencing.GetNextBatchRequest{ + RollupId: rollupId, + LastBatchHash: nil, + MaxBytes: math.MaxInt32, + } + batchRes, err := sequencer.GetNextBatch(context.Background(), getBatchReq) + assert.NoError(t, err) + assert.Equal(t, 1, len(batchRes.Batch.Transactions)) + assert.Equal(t, tx, batchRes.Batch.Transactions[0]) + + // Verify the batch + bHash, err := batchRes.Batch.Hash() + assert.NoError(t, err) + verifyReq := sequencing.VerifyBatchRequest{ + RollupId: rollupId, + BatchHash: bHash, + } + verifyRes, err := sequencer.VerifyBatch(context.Background(), verifyReq) + assert.NoError(t, err) + assert.True(t, verifyRes.Status) + } +}