Skip to content

Commit

Permalink
Merge pull request dymensionxyz#25 from dymensionxyz/omritoptix/settl…
Browse files Browse the repository at this point in the history
…ement

Omritoptix/settlement
  • Loading branch information
omritoptix authored Jul 27, 2022
2 parents 275c6a4 + 810af99 commit 523fb8a
Show file tree
Hide file tree
Showing 8 changed files with 815 additions and 0 deletions.
213 changes: 213 additions & 0 deletions settlement/mock/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package mock

import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"time"

"github.com/celestiaorg/optimint/log"
"github.com/celestiaorg/optimint/settlement"
"github.com/celestiaorg/optimint/store"
"github.com/celestiaorg/optimint/testutil"
"github.com/celestiaorg/optimint/types"
)

const defaultBatchSize = 5

// SettlementLayerClient is intended only for usage in tests.
type SettlementLayerClient struct {
logger log.Logger
settlementKV store.KVStore
latestHeight uint64
config Config
ctx context.Context
cancel context.CancelFunc
}

// Config for the SettlementLayerClient mock
type Config struct {
AutoUpdateBatches bool
AutoUpdateBatchInterval time.Duration
BatchSize uint64
}

var _ settlement.LayerClient = &SettlementLayerClient{}

// Init is called once. it initializes the struct members.
func (s *SettlementLayerClient) Init(config []byte, settlementKV store.KVStore, logger log.Logger) error {
s.logger = logger
s.settlementKV = settlementKV
s.latestHeight = 0
s.ctx, s.cancel = context.WithCancel(context.Background())
if len(config) > 0 {
var err error
s.config, err = s.decodeConfig(config)
if err != nil {
return err
}
if s.config.BatchSize == 0 {
s.config.BatchSize = defaultBatchSize
}
}
return nil
}

func (s *SettlementLayerClient) decodeConfig(config []byte) (Config, error) {
var c Config
err := json.Unmarshal(config, &c)
return c, err
}

// Start is called once, after init. If configured so, it will start producing batches every interval.
func (s *SettlementLayerClient) Start() error {
s.logger.Debug("Mock settlement Layer Client starting")
if s.config.AutoUpdateBatches {
go func() {
timer := time.NewTimer(s.config.AutoUpdateBatchInterval)
for {
select {
case <-s.ctx.Done():
return
case <-timer.C:
s.updateSettlementWithBatch()
}
}
}()
}
return nil
}

// Stop is called once, after Start. it cancels the auto batches created, if such was started.
func (s *SettlementLayerClient) Stop() error {
s.logger.Debug("Mock settlement Layer Client stopping")
s.cancel()
return nil
}

func (s *SettlementLayerClient) updateSettlementWithBatch() {
s.logger.Debug("Mock settlement Layer Client updating with batch")
batch := s.createBatch(s.latestHeight+1, s.latestHeight+1+s.config.BatchSize)
s.SubmitBatch(&batch)
}

func (s *SettlementLayerClient) createBatch(startHeight uint64, endHeight uint64) types.Batch {
s.logger.Debug("Creating batch for settlement layer", "start height", startHeight, "end height", endHeight)
blocks := testutil.GenerateBlocks(int(endHeight - startHeight))
batch := types.Batch{
StartHeight: startHeight,
EndHeight: endHeight,
Blocks: blocks,
// TODO(omritoptix): Change it to be received as func arg
DAPath: fmt.Sprint(endHeight),
}
return batch
}

// saveBatch saves the data to the kvstore
func (s *SettlementLayerClient) saveBatch(resultRetrieveBatch *settlement.ResultRetrieveBatch) error {
s.logger.Debug("Saving batch to settlement layer", "start height",
resultRetrieveBatch.StartHeight, "end height", resultRetrieveBatch.EndHeight)
b, err := json.Marshal(resultRetrieveBatch)
if err != nil {
return err
}
err = s.settlementKV.Set(getKey(resultRetrieveBatch.EndHeight), b)
if err != nil {
return err
}
return nil
}

func (s *SettlementLayerClient) validateBatch(batch *types.Batch) error {
if batch.StartHeight != s.latestHeight+1 {
return errors.New("batch start height must be last height + 1")
}
if batch.EndHeight < batch.StartHeight {
return errors.New("batch end height must be greater or equal to start height")
}
return nil
}

// SubmitBatch submits the batch to the settlement layer. This should create a transaction which (potentially)
// triggers a state transition in the settlement layer.
func (s *SettlementLayerClient) SubmitBatch(batch *types.Batch) settlement.ResultSubmitBatch {
s.logger.Debug("Submitting batch to settlement layer", "start height", batch.StartHeight, "end height", batch.EndHeight)
// validate batch
err := s.validateBatch(batch)
if err != nil {
return settlement.ResultSubmitBatch{
BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()},
}
}
// Build the result to save in the settlement layer.
batchResult := &settlement.ResultRetrieveBatch{
StartHeight: batch.StartHeight,
EndHeight: batch.EndHeight,
}
for _, block := range batch.Blocks {
batchResult.AppHashes = append(batchResult.AppHashes, block.Header.AppHash)
}
// Save to the settlement layer
err = s.saveBatch(batchResult)
if err != nil {
s.logger.Error("Error saving app hash to kv store", "error", err)
return settlement.ResultSubmitBatch{
BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()},
}
}
s.latestHeight = batch.EndHeight
return settlement.ResultSubmitBatch{
BaseResult: settlement.BaseResult{Code: settlement.StatusSuccess},
}
}

func (s *SettlementLayerClient) retrieveBatchAtEndHeight(endHeight uint64) (*settlement.ResultRetrieveBatch, error) {
b, err := s.settlementKV.Get(getKey(endHeight))
if err != nil {
return nil, errors.New("error getting batch from kv store for height " + fmt.Sprint(endHeight))
}
var batchResult settlement.ResultRetrieveBatch
err = json.Unmarshal(b, &batchResult)
if err != nil {
return nil, errors.New("error unmarshalling batch")
}
return &batchResult, nil
}

// RetrieveBatch Gets the batch which contains the given height. Empty height returns the latest batch.
func (s *SettlementLayerClient) RetrieveBatch(height ...uint64) (settlement.ResultRetrieveBatch, error) {
if len(height) == 0 {
s.logger.Debug("Getting latest batch from settlement layer", "latest height", s.latestHeight)
batchResult, err := s.retrieveBatchAtEndHeight(s.latestHeight)
if err != nil {
return settlement.ResultRetrieveBatch{
BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()},
}, errors.New("error getting latest batch")
}
return *batchResult, nil

} else if len(height) == 1 {
s.logger.Debug("Getting batch from settlement layer for height", height)
height := height[0]
endHeight := uint64(float64(height/s.config.BatchSize)) * (s.config.BatchSize + 1)
resultRetrieveBatch, err := s.retrieveBatchAtEndHeight(endHeight)
if err != nil {
return settlement.ResultRetrieveBatch{
BaseResult: settlement.BaseResult{Code: settlement.StatusError, Message: err.Error()},
}, errors.New("error getting batch from kv store at height " + fmt.Sprint(endHeight))
}
return *resultRetrieveBatch, nil
} else {
return settlement.ResultRetrieveBatch{}, errors.New("height len must be 1 or 0")
}

}

func getKey(endHeight uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, endHeight)
return b
}
29 changes: 29 additions & 0 deletions settlement/registry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package registry

import (
"github.com/celestiaorg/optimint/settlement"
"github.com/celestiaorg/optimint/settlement/mock"
)

// A central registry for all Settlement Layer Clients
var clients = map[string]func() settlement.LayerClient{
"mock": func() settlement.LayerClient { return &mock.SettlementLayerClient{} },
}

// GetClient returns client identified by name.
func GetClient(name string) settlement.LayerClient {
f, ok := clients[name]
if !ok {
return nil
}
return f()
}

// RegisteredClients returns names of all settlement clients in registry.
func RegisteredClients() []string {
registered := make([]string, 0, len(clients))
for name := range clients {
registered = append(registered, name)
}
return registered
}
24 changes: 24 additions & 0 deletions settlement/registry/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package registry_test

import (
"testing"

"github.com/celestiaorg/optimint/settlement/registry"
"github.com/stretchr/testify/assert"
)

func TestRegistery(t *testing.T) {
assert := assert.New(t)

expected := []string{"mock"}
actual := registry.RegisteredClients()

assert.ElementsMatch(expected, actual)

for _, e := range expected {
dalc := registry.GetClient(e)
assert.NotNil(dalc)
}

assert.Nil(registry.GetClient("nonexistent"))
}
61 changes: 61 additions & 0 deletions settlement/settlement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package settlement

import (
"github.com/celestiaorg/optimint/log"
"github.com/celestiaorg/optimint/store"
"github.com/celestiaorg/optimint/types"
)

// StatusCode is a type for settlement layer return status.
type StatusCode uint64

// settlement layer return codes.
const (
StatusUnknown StatusCode = iota
StatusSuccess
StatusTimeout
StatusError
)

// BaseResult contains basic information returned by the settlement layer.
type BaseResult struct {
// Code is to determine if the action succeeded.
Code StatusCode
// Message may contain settlement layer specific information (like detailed error message, etc)
Message string
}

// ResultSubmitBatch contains information returned from settlement layer after batch submission.
type ResultSubmitBatch struct {
BaseResult
}

// ResultRetrieveBatch contains information returned from settlement layer after batch retrieval.
type ResultRetrieveBatch struct {
BaseResult
StartHeight uint64
EndHeight uint64
AppHashes [][32]byte
}

// LayerClient defines generic interface for Settlement layer interaction.
type LayerClient interface {

// Init is called once for the client initialization
Init(config []byte, settlementKV store.KVStore, logger log.Logger) error

// Start is called once, after Init. It's implementation should start the client service.
Start() error

// Stop is called once, after Start. It should stop the client service.
Stop() error

// SubmitBatch submits the batch to the settlement layer. This should create a transaction which (potentially)
// triggers a state transition in the settlement layer.
SubmitBatch(batch *types.Batch) ResultSubmitBatch

// RetrieveBatch Gets the batch which contains the given height. Empty height returns the latest batch.
RetrieveBatch(height ...uint64) (ResultRetrieveBatch, error)

// TODO(omritoptix): Support getting multiple batches and pagination
}
Loading

0 comments on commit 523fb8a

Please sign in to comment.