From 82ad76783c19ee6da8ad0803a04eeb810a5509ce Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Sun, 7 Dec 2025 00:17:51 +0530 Subject: [PATCH 1/7] Use updated simplified SDK flow --- .gitignore | 8 +- Makefile | 18 +- ...nsition.go => external_block_processor.go} | 39 ++- cmd/config.go | 76 ++++++ cmd/main.go | 240 +++++------------ cmd/main_test.go | 116 ++++----- config.example.yaml | 36 +++ config/chain_data.json | 4 - config/consensus_chains.json | 14 - config/ext_networks.json | 15 -- docker-compose.yml | 50 ++-- go.mod | 7 +- go.sum | 6 +- pelacli.example.yaml | 121 +++++++++ readme.md | 246 +++++++++--------- 15 files changed, 539 insertions(+), 457 deletions(-) rename application/{state_transition.go => external_block_processor.go} (92%) create mode 100644 cmd/config.go create mode 100644 config.example.yaml delete mode 100644 config/chain_data.json delete mode 100644 config/consensus_chains.json delete mode 100644 config/ext_networks.json create mode 100644 pelacli.example.yaml diff --git a/.gitignore b/.gitignore index e374580..1205c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ .vscode appchain main -app_data/ -pelacli_data/ -test_consensus/ -test_consensus_app/ -multichain/ +data/ +config.yaml +pelacli.yaml diff --git a/Makefile b/Makefile index 45fb0aa..d30c875 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,16 @@ VERSION=v2.4.0 +# Run appchain with default config (zero-config) run: - go run cmd/main.go \ - -emitter-port=:50051 \ - -db-path=/tmp/example/db/ \ - -tx-dir=/tmp/consensus/fetcher/snapshots/42 \ - -local-db-path=/tmp/example/test_tmp \ - -stream-dir=/tmp/consensus/events/ \ - -multichain-config=./debug/multichain.json \ - -rpc-port=:8080 + go run ./cmd/... + +# Run appchain with custom config file +run-config: + go run ./cmd/... -config=config.yaml + +# Build appchain binary +build-bin: + go build -o bin/appchain ./cmd/... dockerbuild: DOCKER_BUILDKIT=1 docker build --ssh default -t appchain:latest . diff --git a/application/state_transition.go b/application/external_block_processor.go similarity index 92% rename from application/state_transition.go rename to application/external_block_processor.go index 1693422..8c34756 100644 --- a/application/state_transition.go +++ b/application/external_block_processor.go @@ -7,6 +7,7 @@ import ( "github.com/0xAtelerix/sdk/gosdk" "github.com/0xAtelerix/sdk/gosdk/apptypes" + "github.com/0xAtelerix/sdk/gosdk/evmtypes" "github.com/0xAtelerix/sdk/gosdk/external" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -56,31 +57,29 @@ const ( `"name":"WithdrawToSolana","type":"event"}]` ) -var ( - _ gosdk.StateTransitionSimplified = &StateTransition{} - _ gosdk.StateTransitionInterface[Transaction[Receipt], Receipt] = gosdk.BatchProcesser[Transaction[Receipt], Receipt]{} -) +// Verify ExtBlockProcessor implements ExternalBlockProcessor interface. +var _ gosdk.ExternalBlockProcessor = &ExtBlockProcessor{} -type StateTransition struct { - msa *gosdk.MultichainStateAccess +type ExtBlockProcessor struct { + msa gosdk.MultichainStateAccessor } -func NewStateTransition(msa *gosdk.MultichainStateAccess) *StateTransition { - return &StateTransition{ +func NewExtBlockProcessor(msa gosdk.MultichainStateAccessor) *ExtBlockProcessor { + return &ExtBlockProcessor{ msa: msa, } } -// how to external chains blocks -func (st *StateTransition) ProcessBlock( +// ProcessBlock handles external chain blocks (EVM, Solana). +func (p *ExtBlockProcessor) ProcessBlock( b apptypes.ExternalBlock, tx kv.RwTx, ) ([]apptypes.ExternalTransaction, error) { switch { case gosdk.IsEvmChain(apptypes.ChainType(b.ChainID)): - return st.processEVMBlock(b, tx) + return p.processEVMBlock(b, tx) case gosdk.IsSolanaChain(apptypes.ChainType(b.ChainID)): - return st.processSolanaBlock(b, tx) + return p.processSolanaBlock(b, tx) default: log.Warn().Uint64("chainID", b.ChainID).Msg("Unsupported external chain, skipping...") } @@ -88,13 +87,13 @@ func (st *StateTransition) ProcessBlock( return nil, nil } -func (st *StateTransition) processSolanaBlock( +func (p *ExtBlockProcessor) processSolanaBlock( b apptypes.ExternalBlock, _ kv.RwTx, ) ([]apptypes.ExternalTransaction, error) { var externalTxs []apptypes.ExternalTransaction - solBlock, err := st.msa.SolanaBlock(context.Background(), b) + solBlock, err := p.msa.SolanaBlock(context.Background(), b) if err != nil { return nil, err } @@ -108,24 +107,24 @@ func (st *StateTransition) processSolanaBlock( return externalTxs, nil } -func (st *StateTransition) processEVMBlock( +func (p *ExtBlockProcessor) processEVMBlock( b apptypes.ExternalBlock, dbtx kv.RwTx, ) ([]apptypes.ExternalTransaction, error) { var externalTxs []apptypes.ExternalTransaction - block, err := st.msa.EthBlock(context.Background(), b) + block, err := p.msa.EVMBlock(context.Background(), b) if err != nil { return nil, err } - receipts, err := st.msa.EthReceipts(context.Background(), b) + receipts, err := p.msa.EVMReceipts(context.Background(), b) if err != nil { return nil, err } for _, r := range receipts { - extTxs := st.processReceipt(dbtx, r, b.ChainID) + extTxs := p.processReceipt(dbtx, r, b.ChainID) if len(extTxs) > 0 { externalTxs = append(externalTxs, extTxs...) } @@ -143,9 +142,9 @@ func (st *StateTransition) processEVMBlock( // processReceipt handles Deposit events from the external chain // Just for example, In real use-case, handle according to your logic -func (*StateTransition) processReceipt( +func (*ExtBlockProcessor) processReceipt( tx kv.RwTx, - r types.Receipt, + r evmtypes.Receipt, chainID uint64, ) []apptypes.ExternalTransaction { var externalTxs []apptypes.ExternalTransaction diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..a2a32a8 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "os" + + "github.com/0xAtelerix/sdk/gosdk" + "github.com/rs/zerolog" + "gopkg.in/yaml.v3" +) + +// Config holds the appchain configuration. +type Config struct { + // ChainID is the unique identifier for this appchain. + ChainID uint64 `yaml:"chain_id"` + + // DataDir is the root data directory (shared with pelacli). + DataDir string `yaml:"data_dir"` + + // EmitterPort is the gRPC port for the emitter API. + EmitterPort string `yaml:"emitter_port"` + + // RPCPort is the HTTP port for JSON-RPC server. + RPCPort string `yaml:"rpc_port"` + + // LogLevel is the zerolog log level (0=debug, 1=info, 2=warn, 3=error). + LogLevel int `yaml:"log_level"` +} + +// DefaultConfig returns a config with all defaults applied. +func DefaultConfig() *Config { + cfg := &Config{} + cfg.applyDefaults() + + return cfg +} + +// LoadConfig loads configuration from a YAML file. +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read config: %w", err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + + cfg.applyDefaults() + + return &cfg, nil +} + +// applyDefaults sets default values for unset fields. +func (c *Config) applyDefaults() { + if c.ChainID == 0 { + c.ChainID = gosdk.DefaultAppchainID + } + + if c.DataDir == "" { + c.DataDir = gosdk.DefaultDataDir + } + + if c.EmitterPort == "" { + c.EmitterPort = gosdk.DefaultEmitterPort + } + + if c.RPCPort == "" { + c.RPCPort = gosdk.DefaultRPCPort + } + + if c.LogLevel == 0 { + c.LogLevel = int(zerolog.InfoLevel) + } +} diff --git a/cmd/main.go b/cmd/main.go index 25d1e19..9f7781d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,9 +2,8 @@ package main import ( "context" - "encoding/binary" - "encoding/json" "flag" + "fmt" "os" "os/signal" "syscall" @@ -12,10 +11,6 @@ import ( "github.com/0xAtelerix/sdk/gosdk" "github.com/0xAtelerix/sdk/gosdk/rpc" "github.com/0xAtelerix/sdk/gosdk/txpool" - "github.com/fxamacker/cbor/v2" - "github.com/ledgerwatch/erigon-lib/kv" - "github.com/ledgerwatch/erigon-lib/kv/mdbx" - mdbxlog "github.com/ledgerwatch/log/v3" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -23,225 +18,106 @@ import ( "github.com/0xAtelerix/example/application/api" ) -const ChainID = 42 - -type RuntimeArgs struct { - EmitterPort string - AppchainDBPath string - EventStreamDir string - TxStreamDir string - LocalDBPath string - RPCPort string - MutlichainConfig gosdk.MultichainConfig - LogLevel zerolog.Level -} - func main() { - // Context with cancel for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel() - RunCLI(ctx) -} - -func RunCLI(ctx context.Context) { - config := gosdk.MakeAppchainConfig(ChainID, nil) - - // Use a local FlagSet (no globals). + // Parse command line flags fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) - - emitterPort := fs.String("emitter-port", config.EmitterPort, "Emitter gRPC port") - appchainDBPath := fs.String("db-path", config.AppchainDBPath, "Path to appchain DB") - streamDir := fs.String("stream-dir", config.EventStreamDir, "Event stream directory") - txDir := fs.String("tx-dir", config.TxStreamDir, "Transaction stream directory") - - localDBPath := fs.String("local-db-path", "./localdb", "Path to local DB") - rpcPort := fs.String("rpc-port", ":8080", "Port for the JSON-RPC server") - multichainConfigJSON := fs.String("multichain-config", "", "Multichain config JSON path") - logLevel := fs.Int("log-level", int(zerolog.InfoLevel), "Logging level") - - if *logLevel > int(zerolog.Disabled) { - *logLevel = int(zerolog.DebugLevel) - } else if *logLevel < int(zerolog.TraceLevel) { - *logLevel = int(zerolog.TraceLevel) - } - + configPath := fs.String("config", "", "Path to config.yaml (optional)") _ = fs.Parse(os.Args[1:]) - var mcDbs gosdk.MultichainConfig + // Load config from file or use defaults + var ( + cfg *Config + err error + ) - if multichainConfigJSON != nil && *multichainConfigJSON != "" { - f, err := os.ReadFile(*multichainConfigJSON) + if *configPath != "" { + cfg, err = LoadConfig(*configPath) if err != nil { - log.Panic().Err(err).Msg("Error reading multichain config") + log.Fatal().Err(err).Msg("Failed to load config") } - err = json.Unmarshal(f, &mcDbs) - if err != nil { - log.Warn().Err(err).Msg("Error unmarshalling multichain config") - } - } + log.Info().Str("config", *configPath).Msg("Loaded config") + } else { + cfg = DefaultConfig() - args := RuntimeArgs{ - EmitterPort: *emitterPort, - AppchainDBPath: *appchainDBPath, - EventStreamDir: *streamDir, - TxStreamDir: *txDir, - LocalDBPath: *localDBPath, - RPCPort: *rpcPort, - LogLevel: zerolog.Level(*logLevel), - MutlichainConfig: mcDbs, + log.Info().Msg("Using default config") } - Run(ctx, args, nil) -} - -func Run(ctx context.Context, args RuntimeArgs, _ chan<- int) { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(args.LogLevel) - + // Setup logging + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}). + Level(zerolog.Level(cfg.LogLevel)) ctx = log.With().Logger().WithContext(ctx) - // Cancel on SIGINT/SIGTERM too (centralized; no per-runner signal goroutines needed) + // Handle shutdown signals ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() - config := gosdk.MakeAppchainConfig(ChainID, args.MutlichainConfig) - - config.EmitterPort = args.EmitterPort - config.AppchainDBPath = args.AppchainDBPath - config.EventStreamDir = args.EventStreamDir - config.TxStreamDir = args.TxStreamDir - config.Logger = &log.Logger - - chainDBs, err := gosdk.NewMultichainStateAccessDB(args.MutlichainConfig) - if err != nil { - log.Fatal().Err(err).Msg("Failed to create multichain db") - } - - msa := gosdk.NewMultichainStateAccess(chainDBs) - - // инициализируем базу на нашей стороне - appchainDB, err := mdbx.NewMDBX(mdbxlog.New()). - Path(config.AppchainDBPath). - WithTableCfg(func(_ kv.TableCfg) kv.TableCfg { - return gosdk.MergeTables( - gosdk.DefaultTables(), - application.Tables(), - ) - }).Open() - if err != nil { - log.Fatal().Err(err).Msg("Failed to appchain mdbx database") - } - - defer appchainDB.Close() - - subs, err := gosdk.NewSubscriber(ctx, appchainDB) - if err != nil { - log.Fatal().Err(err).Msg("Failed to create subscriber") - } - - stateTransition := gosdk.NewBatchProcesser[application.Transaction[application.Receipt]]( - application.NewStateTransition(msa), - msa, - subs, - ) - - localDB, err := mdbx.NewMDBX(mdbxlog.New()). - Path(args.LocalDBPath). - WithTableCfg(func(_ kv.TableCfg) kv.TableCfg { - return txpool.Tables() - }). - Open() - if err != nil { - log.Fatal().Err(err).Msg("Failed to local mdbx database") - } - - defer localDB.Close() - - // fixme dynamic val set. Right now it is especially for local development with pelacli - valset := &gosdk.ValidatorSet{Set: map[gosdk.ValidatorID]gosdk.Stake{0: 100}} - - var epochKey [4]byte - binary.BigEndian.PutUint32(epochKey[:], 1) - - valsetData, err := cbor.Marshal(valset) - if err != nil { - log.Fatal().Err(err).Msg("Failed to marshal validator set data") + if err := Run(ctx, cfg); err != nil { + log.Fatal().Err(err).Msg("Failed to run appchain") } +} - err = appchainDB.Update(ctx, func(tx kv.RwTx) error { - return tx.Put(gosdk.ValsetBucket, epochKey[:], valsetData) +// Run starts the appchain with the given config. Exported for testing. +func Run(ctx context.Context, cfg *Config) error { + // Initialize all common components (SDK Defaults if not provided) + appInit, err := gosdk.Init(ctx, gosdk.InitConfig{ + ChainID: cfg.ChainID, + DataDir: cfg.DataDir, + EmitterPort: cfg.EmitterPort, + CustomTables: application.Tables(), + Logger: &log.Logger, }) if err != nil { - log.Fatal().Err(err).Msg("Failed to appchain mdbx database") + return fmt.Errorf("init appchain: %w", err) } - - txPool := txpool.NewTxPool[application.Transaction[application.Receipt]]( - localDB, - ) - - txBatchDB, err := mdbx.NewMDBX(mdbxlog.New()). - Path(config.TxStreamDir). - WithTableCfg(func(_ kv.TableCfg) kv.TableCfg { - return gosdk.TxBucketsTables() - }). - Readonly().Open() - if err != nil { - log.Fatal().Str("path", config.TxStreamDir).Err(err).Msg("Failed to tx batch mdbx database") - } - - log.Info().Msg("Starting appchain...") - - appchainExample := gosdk.NewAppchain( - stateTransition, + defer appInit.Close() + + // Create transaction pool + txPool := txpool.NewTxPool[application.Transaction[application.Receipt]](appInit.LocalDB) + + // Create appchain with app-specific logic + appchain := gosdk.NewAppchain( + appInit, + gosdk.NewDefaultBatchProcessor[application.Transaction[application.Receipt]]( + application.NewExtBlockProcessor(appInit.Multichain), + appInit.Multichain, + appInit.Subscriber, + ), application.BlockConstructor, txPool, - config, - appchainDB, - subs, - msa, - txBatchDB, ) - if err != nil { - log.Fatal().Err(err).Msg("Failed to start appchain") + // Initialize dev validator set (local development only) + if err := gosdk.InitDevValidatorSet(ctx, appInit.AppchainDB); err != nil { + return fmt.Errorf("init dev validator set: %w", err) } - // Initialize genesis accounts and trading pairs after all databases are ready - log.Info().Msg("Initializing genesis state...") - - if err := application.InitializeGenesis(ctx, appchainDB); err != nil { - log.Fatal().Err(err).Msg("Failed to initialize genesis state") + // Initialize app-specific genesis state + if err := application.InitializeGenesis(ctx, appInit.AppchainDB); err != nil { + return fmt.Errorf("init genesis state: %w", err) } - // Run appchain in goroutine - runErr := make(chan error, 1) - + // Run appchain in background go func() { - select { - case <-ctx.Done(): - // nothing to do - case runErr <- appchainExample.Run(ctx): - // nothing to do + if err := appchain.Run(ctx); err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Appchain error") } }() + // Setup and start JSON-RPC server rpcServer := rpc.NewStandardRPCServer(nil) - - // Optional: add middleware for logging rpcServer.AddMiddleware(api.NewExampleMiddleware(log.Logger)) - // Add standard RPC methods - Refer RPC readme in sdk for details rpc.AddStandardMethods[ application.Transaction[application.Receipt], application.Receipt, application.Block, - ](rpcServer, appchainDB, txPool, ChainID) + ](rpcServer, appInit.AppchainDB, txPool, cfg.ChainID) - // Add custom RPC methods - Optional - api.NewCustomRPC(rpcServer, appchainDB).AddRPCMethods() + api.NewCustomRPC(rpcServer, appInit.AppchainDB).AddRPCMethods() - if err := rpcServer.StartHTTPServer(ctx, args.RPCPort); err != nil { - log.Fatal().Err(err).Msg("Failed to start RPC server") - } + return rpcServer.StartHTTPServer(ctx, cfg.RPCPort) } diff --git a/cmd/main_test.go b/cmd/main_test.go index fe0aa9c..6c3b062 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "path/filepath" - "syscall" "testing" "time" @@ -38,66 +37,72 @@ func waitUntil(ctx context.Context, f func() bool) error { } } -// TestEndToEnd spins up main(), posts a transaction to the /rpc endpoint and +// TestEndToEnd spins up the appchain, posts a transaction to the /rpc endpoint and // verifies we get a 2xx response. func TestEndToEnd(t *testing.T) { - var err error - port := getFreePort(t) + dataDir := t.TempDir() + chainID := uint64(1001) - // temp dirs for clean DB state - tmp := t.TempDir() - dbPath := filepath.Join(tmp, "appchain.mdbx") - localDB := filepath.Join(tmp, "local.mdbx") - streamDir := filepath.Join(tmp, "stream") - txDir := filepath.Join(tmp, "tx") - - // Create an empty MDBX database that can be opened in readonly mode - err = createEmptyMDBXDatabase(txDir, gosdk.TxBucketsTables()) - require.NoError(t, err, "create empty txBatch database") + // Create required directories and databases + // SDK's Init() expects these paths to exist + txBatchPath := gosdk.TxBatchPath(dataDir, chainID) + eventsPath := gosdk.EventsPath(dataDir) - // craft os.Args for main() - oldArgs := os.Args + require.NoError(t, os.MkdirAll(txBatchPath, 0o755)) + require.NoError(t, os.MkdirAll(eventsPath, 0o755)) - defer func() { os.Args = oldArgs }() + // Create TxBatchDB (normally created by pelacli's fetcher) + err := createEmptyMDBXDatabase(txBatchPath, gosdk.TxBucketsTables()) + require.NoError(t, err, "create empty txBatch database") - os.Args = []string{ - "appchain-test-binary", - "-rpc-port", fmt.Sprintf(":%d", port), - "-emitter-port", ":0", // 0 → let OS choose, we don’t care in the test - "-db-path", dbPath, - "-local-db-path", localDB, - "-stream-dir", streamDir, - "-tx-dir", txDir, + // Create events file (normally created by pelacli) + eventsFile := filepath.Join(eventsPath, "epoch_1.data") + f, err := os.Create(eventsFile) + require.NoError(t, err) + require.NoError(t, f.Close()) + + // Create config with test values + cfg := &Config{ + ChainID: chainID, + DataDir: dataDir, + EmitterPort: ":0", // Let OS choose + RPCPort: fmt.Sprintf(":%d", port), } - go RunCLI(t.Context()) + // Create cancellable context for the test + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + // Run appchain in background + go func() { + _ = Run(ctx, cfg) + }() - // wait until HTTP service is up + // Wait until HTTP service is up rpcURL := fmt.Sprintf("http://127.0.0.1:%d/rpc", port) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + waitCtx, waitCancel := context.WithTimeout(ctx, 5*time.Second) + defer waitCancel() - if err = waitUntil(ctx, func() bool { - // GET is fine; we only care the port is bound. - var req *http.Request - req, err = http.NewRequestWithContext(ctx, http.MethodGet, rpcURL, nil) - require.NoError(t, err, "GET req /rpc") + err = waitUntil(waitCtx, func() bool { + req, reqErr := http.NewRequestWithContext(waitCtx, http.MethodGet, rpcURL, nil) + if reqErr != nil { + return false + } - var resp *http.Response - resp, err = http.DefaultClient.Do(req) - require.NoError(t, err, "GET res /rpc") + resp, respErr := http.DefaultClient.Do(req) + if respErr != nil { + return false + } - err = resp.Body.Close() - require.NoError(t, err) + _ = resp.Body.Close() return true - }); err != nil { - t.Fatalf("JSON-RPC service never became ready: %v", err) - } + }) + require.NoError(t, err, "JSON-RPC service never became ready") - // build & send a transaction + // Build & send a transaction tx := application.Transaction[application.Receipt]{ Sender: "Vasya", Value: 42, @@ -105,12 +110,12 @@ func TestEndToEnd(t *testing.T) { } var buf bytes.Buffer - if err = json.NewEncoder(&buf).Encode(tx); err != nil { - t.Fatalf("encode tx: %v", err) - } + + err = json.NewEncoder(&buf).Encode(tx) + require.NoError(t, err, "encode tx") req, err := http.NewRequestWithContext( - ctx, + waitCtx, http.MethodPost, rpcURL, bytes.NewReader(buf.Bytes()), @@ -126,18 +131,13 @@ func TestEndToEnd(t *testing.T) { require.NoError(t, resp.Body.Close()) }() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - t.Fatalf("unexpected HTTP status: %s", resp.Status) - } + require.True(t, resp.StatusCode >= 200 && resp.StatusCode < 300, + "unexpected HTTP status: %s", resp.Status) - // graceful shutdown - // The real program listens for SIGINT/SIGTERM, - // so use the same mechanism to drain goroutines. - proc, _ := os.FindProcess(os.Getpid()) - _ = proc.Signal(syscall.SIGINT) + // Cancel context to trigger graceful shutdown + cancel() - // Give main() a moment to tear down so the test runner’s - // goroutine leak detector stays quiet. + // Give Run() a moment to tear down time.Sleep(500 * time.Millisecond) t.Log("Success!") @@ -162,7 +162,7 @@ func getFreePort(t *testing.T) int { return port } -// createEmptyMDBXDatabase creates an empty MDBX database that can be opened in readonly mode +// createEmptyMDBXDatabase creates an empty MDBX database that can be opened in readonly mode. func createEmptyMDBXDatabase(dbPath string, tableCfg kv.TableCfg) error { tempDB, err := mdbx.NewMDBX(mdbxlog.New()). Path(dbPath). diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..206332b --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,36 @@ +# ============================================================================= +# APPCHAIN CONFIGURATION +# ============================================================================= +# Copy this file to config.yaml and modify as needed. +# All fields are optional - defaults will be used if not specified. +# +# Run with: ./appchain --config=config.yaml +# ============================================================================= + +# Unique identifier for this appchain +# Must match the chain_id in pelacli's appchains config +# Default: 42 +chain_id: 42 + +# Root data directory shared with pelacli +# Both appchain and pelacli must use the same data_dir for communication +# Default: ./data +data_dir: "./data" + +# gRPC port for emitter API +# Pelacli's fetcher connects to this port to fetch tx batches and checkpoints +# Default: :9090 +emitter_port: ":9090" + +# HTTP port for JSON-RPC server +# Clients connect to this port to submit transactions and query state +# Default: :8080 +rpc_port: ":8080" + +# Log level for application logging +# 0 = debug (most verbose) +# 1 = info (default) +# 2 = warn +# 3 = error (least verbose) +# Default: 1 +log_level: 1 diff --git a/config/chain_data.json b/config/chain_data.json deleted file mode 100644 index 1f894d9..0000000 --- a/config/chain_data.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "80002": "/multichain/amoy", - "123231": "/multichain/solana-devnet" -} diff --git a/config/consensus_chains.json b/config/consensus_chains.json deleted file mode 100644 index 0339a04..0000000 --- a/config/consensus_chains.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "ChainID": 80002, - "DBPath": "/multichain/amoy", - "APIKey": "{YOUR AMOY WSS URL}", - "StartBlock": 27701526 - }, - { - "ChainID": 123231, - "DBPath": "/multichain/solana-devnet", - "APIKey": "https://api.devnet.solana.com", - "StartBlock": 414579298 - } -] diff --git a/config/ext_networks.json b/config/ext_networks.json deleted file mode 100644 index e0448fe..0000000 --- a/config/ext_networks.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "chainId": 11155111, - "rpcUrl": "", - "contractAddress": "", - "privateKey": "" - }, - { - "chainId": 123231, - "rpcUrl": "https://api.devnet.solana.com", - "contractAddress": "", - "privateKey": "" - } -] - diff --git a/docker-compose.yml b/docker-compose.yml index e4e39f6..0b64ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,33 @@ services: + pelacli: + container_name: pelacli + image: pelagosnetwork/pelacli:latest + working_dir: /app + volumes: + - ./data:/app/data + # Uncomment to use custom config: + # - ./pelacli.yaml:/app/pelacli.yaml + command: + - consensus + # Uncomment to use custom config: + # - --config=/app/pelacli.yaml + appchain: build: context: . dockerfile: Dockerfile + working_dir: /app pid: "service:pelacli" image: appchain:latest volumes: - - ./pelacli_data:/consensus_data - - ./app_data:/data - - ./config/chain_data.json:/data/chain_data.json:ro - - ./multichain:/multichain + - ./data:/app/data + # Uncomment to use custom config: + # - ./config.yaml:/app/config.yaml ports: - "9090:9090" - "8080:8080" depends_on: - pelacli - command: - - --emitter-port=:9090 - - --db-path=/data/appchain-db - - --local-db-path=/data/local-db - - --stream-dir=/consensus_data/events - - --tx-dir=/consensus_data/fetcher/snapshots/42 - - --rpc-port=:8080 - - --multichain-config=/data/chain_data.json - - pelacli: - container_name: pelacli - image: pelagosnetwork/pelacli:latest - volumes: - - ./pelacli_data:/consensus_data - - ./config/consensus_chains.json:/consensus_chains.json:ro - - ./config/ext_networks.json:/ext_networks.json:ro - - ./multichain:/multichain - command: - - consensus - - --snapshot-dir=/consensus_data - - --appchain=42=appchain:9090 - - --ask-period=1s - - --multichain-dir=/consensus_data/multichain_db - - --chains-json=/consensus_chains.json - - --ext-txn-config-json=/ext_networks.json - + # Uncomment to use custom config: + # command: + # - -config=/app/config.yaml diff --git a/go.mod b/go.mod index 532d499..d89d4ab 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/0xAtelerix/example go 1.25.0 require ( - github.com/0xAtelerix/sdk v0.1.6 + github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2 github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.16.3 - github.com/fxamacker/cbor/v2 v2.9.0 github.com/holiman/uint256 v1.3.2 github.com/ledgerwatch/erigon-lib v1.0.0 github.com/ledgerwatch/log/v3 v3.9.0 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -32,6 +32,7 @@ require ( github.com/ethereum/c-kzg-4844/v2 v2.1.4 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -39,6 +40,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect @@ -62,7 +64,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/grpc v1.75.1 // indirect google.golang.org/protobuf v1.36.9 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) replace google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 => google.golang.org/genproto v0.0.0-20250707201910-8d1bb00bc6a7 diff --git a/go.sum b/go.sum index 735fda3..c848e80 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xAtelerix/sdk v0.1.6 h1:v1S/KJKlGmi6wyKK+jxqS2SgMe7g0TnxR+3OopZVguk= -github.com/0xAtelerix/sdk v0.1.6/go.mod h1:N6ZEyq1hmfDhFYnQnqSrKOOiLWEfxEv0xb2EG4i/YYU= +github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2 h1:lyW4x1FeRQ+Kyl0OLzsM4wnSJNCKOi4Aoth8jM6Ptkk= +github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -108,6 +108,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= diff --git a/pelacli.example.yaml b/pelacli.example.yaml new file mode 100644 index 0000000..183ae7c --- /dev/null +++ b/pelacli.example.yaml @@ -0,0 +1,121 @@ +# ============================================================================= +# PELACLI CONFIGURATION +# ============================================================================= +# Copy this file to pelacli.yaml and modify as needed. +# All fields are optional - defaults will be used if not specified. +# +# Run with: pelacli consensus --config=pelacli.yaml +# ============================================================================= + +# Root data directory for all pelacli data (events, fetcher, multichain) +# Must match appchain's data_dir for communication +# Default: ./data +data_dir: "./data" + +# ============================================================================= +# CONSENSUS SETTINGS +# ============================================================================= +consensus: + # How often to poll appchain for new transactions + # Default: 100ms + ask_period: 100ms + + # Port for fetcher gRPC server (internal communication) + # Default: 8088 + fetcher_port: 8088 + +# ============================================================================= +# API SERVER SETTINGS +# ============================================================================= +api: + # Port for external transactions REST API (for explorer, etc.) + # Set to 0 to disable the API server + # Default: 8081 + port: 8081 + +# ============================================================================= +# AVAIL DA SETTINGS (Data Availability Layer) +# ============================================================================= +# Leave api_key empty to disable Avail DA integration +avail_da: + # Avail Turbo API key (empty = disabled) + # Get your API key from: https://turbo.availproject.org + api_key: "" + + # Avail Turbo API URL + # Default: https://staging.turbo-api.availproject.org + url: "https://staging.turbo-api.availproject.org" + +# ============================================================================= +# APPCHAIN CONNECTIONS +# ============================================================================= +# Configure which appchains pelacli should connect to for fetching transactions. +# If not specified, defaults to chain_id=42 at :9090 (SDK's DefaultAppchainID) +# +# appchains: +# - chain_id: 42 +# address: ":9090" # gRPC address of appchain's emitter + +# ============================================================================= +# READ CHAINS - WSS connections for reading external chain blocks (oracle) +# ============================================================================= +# If not specified, defaults to Sepolia + Amoy with built-in WSS endpoints. +# DBPath is auto-computed from data_dir (e.g., ./data/multichain/11155111/). +# You can add any EVM or SVM compatible chain here. +# +# Block offset values: +# -4 = safe (2 blocks behind latest) +# -3 = finalized (most secure, ~15 min delay on Ethereum) +# -2 = latest (no delay, default for testnets) +# -1 = pending +# 0 = not set (uses default: -3 finalized for core, -2 latest for pelacli) +# 1+ = fixed offset from latest (e.g., 2 means if latest is block X, you get X-2) +# +# read_chains: +# - chain_id: 11155111 # Ethereum Sepolia +# api_key: "wss://my-custom-sepolia-endpoint.com" +# start_block: 0 # 0 = start from latest block or give specific block number +# block_offset: -2 # -3=finalized, -2=latest, -4=safe +# blocks_to_keep: 0 # 0 = no pruning (keep all blocks) +# prune_interval_seconds: 0 # 0 = default (5 minutes) +# +# - chain_id: 80002 # Polygon Amoy +# api_key: "wss://my-custom-amoy-endpoint.com" +# start_block: 0 +# block_offset: -2 +# blocks_to_keep: 1000 # Keep last 1000 blocks +# prune_interval_seconds: 300 # Prune every 5 minutes + +# ============================================================================= +# WRITE CHAINS - RPC connections for sending transactions to external chains +# ============================================================================= +# If not specified, defaults to Sepolia + Amoy with built-in RPC endpoints. +# Configure to send cross-chain transactions to external blockchains. +# +# write_chains: +# # Override Sepolia with your own RPC and key +# - chain_id: 11155111 +# rpc_url: "https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY" +# private_key: "your-private-key-hex-without-0x-prefix" +# pelagos_contract: "0x922e02fFbDe8ABbF3058ccC3f9433018A2ff8C1d" +# +# # Override Polygon Amoy with your own RPC and key +# - chain_id: 80002 +# rpc_url: "https://polygon-amoy.g.alchemy.com/v2/YOUR_API_KEY" +# private_key: "your-private-key-hex-without-0x-prefix" +# pelagos_contract: "0x98D34a83c45FEae289f6FA72ba009Ad49F3D26ED" +# +# # Add a custom chain not in defaults +# - chain_id: 421614 # Arbitrum Sepolia +# rpc_url: "https://arb-sepolia.g.alchemy.com/v2/YOUR_API_KEY" +# private_key: "your-private-key-hex" +# pelagos_contract: "0xYourPelagosContractAddress" + +# ============================================================================= +# DEFAULT PELAGOS CONTRACT ADDRESSES (for reference, They might change) +# ============================================================================= +# Ethereum Sepolia (11155111): 0x922e02fFbDe8ABbF3058ccC3f9433018A2ff8C1d +# Polygon Amoy (80002): 0x98D34a83c45FEae289f6FA72ba009Ad49F3D26ED +# +# Deploy your own Pelagos and AppChain contracts using: +# https://github.com/0xAtelerix/sdk/tree/main/contracts diff --git a/readme.md b/readme.md index a31120c..6168029 100644 --- a/readme.md +++ b/readme.md @@ -9,10 +9,8 @@ - [Key concepts & execution model](#key-concepts--execution-model) - [Project layout](#project-layout) - [Docker — compose stack](#docker--compose-stack) -- [Configuration files](#configuration-files) - - [consensus_chains.json](#configconsensus_chainsjson-used-by-pelacli-for-reading-from-external-chains) - - [chain_data.json](#configchain_datajson-used-by-appchain-for-reading-external-chain-data) - - [ext_networks.json](#configext_networksjson-used-by-pelacli-for-writing-to-external-chains) +- [Zero-Config Setup](#zero-config-setup) +- [Custom Configuration (Optional)](#custom-configuration-optional) - [Build & Run](#build--run) - [JSON-RPC quickstart](#json-rpc-quickstart) - [Code walkthrough (where to extend)](#code-walkthrough-where-to-extend) @@ -29,14 +27,14 @@ * **Buckets** (tables) for app state (`appaccounts`), receipts, blocks, checkpoints, etc. * A runnable `main.go` that wires the SDK, DBs, tx-pool, validator set, the appchain loop, and default **JSON-RPC**. * One **custom JSON-RPC** (`getBalance`) + **standard** ones (`sendTransaction`, `getTransactionStatus`, `getTransactionReceipt`, …). -* A **docker-compose** that runs the node together with `pelacli` so your txs actually progress. +* A **docker-compose** that runs the node together with `pelacli` so your txs actually progress. ## Key concepts & execution model 1. **Clients** submit transactions via JSON-RPC (`sendTransaction`) into the **tx pool**. 2. The **consensus** (`pelacli`) periodically: - * pulls txs via the appchain’s **gRPC emitter API** (`CreateInternalTransactionsBatch`), + * pulls txs via the appchain's **gRPC emitter API** (`CreateInternalTransactionsBatch`), * writes **tx-batches** to an MDBX database (`txbatch`), * appends **events** (referencing those batches + external blocks) to the event stream file. 3. The appchain run loop consumes **events + tx-batches**, executes your `Transaction.Process` inside a DB write transaction, persists **receipts**, builds a **block**, and writes a **checkpoint**. @@ -44,8 +42,8 @@ > Status lifecycle: **Pending** (in tx pool) → **Batched** (pulled by pelacli) → **Processed/Failed** (after your txn processing logic runs in a block). -* **Transactions don’t auto-finalize.** - Without the **consensus** you’ll only ever see `Pending`. This compose includes `pelacli` to move them forward. +* **Transactions don't auto-finalize.** + Without the **consensus** you'll only ever see `Pending`. This compose includes `pelacli` to move them forward. * **The appchain waits for real data sources.** It blocks until both exist: @@ -54,11 +52,11 @@ * the **tx-batch MDBX**: `` with the `txbatch` table. `pelacli` creates and updates both. -* **Multichain access uses local MDBX** - The SDK reads external chain state from **MDBX databases** on disk. `pelacli` populates and updates them using your RPC **API key**. +* **Multichain access uses local SQLite** + The SDK reads external chain state from **SQLite databases** on disk. `pelacli` populates and updates them automatically. By default, Sepolia and Polygon Amoy are enabled. Add additional networks via `read_chains` in `pelacli.yaml` (see [`pelacli.example.yaml`](./pelacli.example.yaml)). * **Cross-chain transaction flow** - - **Read**: `pelacli` fetches data from external chains → stores in MDBX → appchain reads via `MultichainStateAccess` + - **Read**: `pelacli` fetches data from external chains → stores in SQLite → appchain reads via `MultichainStateAccess` - **Write**: appchain generates `ExternalTransaction` → `pelacli` sends to Pelagos contract → Pelagos routes to specific AppChain contract based on appchainID - **Custom contracts**: Deploy your own AppChain contracts on external chains using the contracts in the [SDK contracts folder](https://github.com/0xAtelerix/sdk/tree/main/contracts) for more advanced cross-chain interactions @@ -79,175 +77,190 @@ │ ├─ api.go # Custom JSON-RPC methods (getBalance) │ └─ middleware.go # CORS and other middleware ├─ cmd/ -│ └─ main.go # Wiring & run loop (the app binary) -├─ config/ -│ ├─ chain_data.json # Chain ID → MDBX path mapping (appchain reads) -│ ├─ consensus_chains.json # External chains to fetch data from (pelacli writes) -│ └─ ext_networks.json # External chains to send txns to (pelacli sends txns) +│ ├─ config.go # Config struct and YAML loading +│ ├─ main.go # Wiring & run loop (the app binary) +│ └─ main_test.go # End-to-end integration test +├─ data/ # Shared volume (created at runtime) +├─ config.example.yaml # Example appchain config +├─ pelacli.example.yaml # Example pelacli config ├─ Dockerfile # Dockerfile for the appchain node ├─ docker-compose.yml # Compose for appchain + pelacli -└─ test_txns.sh # Test script for sending transactions and checking things are working +└─ test_txns.sh # Test script for sending transactions ``` ## Docker — compose stack -This compose runs **both** your appchain and the **pelacli** streamer. +This compose runs **both** your appchain and the **pelacli** streamer with **zero configuration required**. ### `docker-compose.yml` ```yaml services: + pelacli: + container_name: pelacli + image: pelagosnetwork/pelacli:latest + working_dir: /app + volumes: + - ./data:/app/data + command: + - consensus + # Zero-config: uses SDK defaults + # - appchain: 42 @ :9090 + # - chains: Sepolia, Polygon Amoy + # - paths: ./data/events, ./data/multichain + appchain: build: context: . dockerfile: Dockerfile + working_dir: /app pid: "service:pelacli" image: appchain:latest volumes: - - ./pelacli_data:/consensus_data - - ./app_data:/data - - ./config/chain_data.json:/data/chain_data.json:ro - - ./multichain:/multichain + - ./data:/app/data ports: - "9090:9090" - "8080:8080" depends_on: - pelacli - command: - - --emitter-port=:9090 - - --db-path=/data/appchain-db - - --local-db-path=/data/local-db - - --stream-dir=/consensus_data/events - - --tx-dir=/consensus_data/fetcher/snapshots/42 - - --rpc-port=:8080 - - --multichain-config=/data/chain_data.json - - pelacli: - container_name: pelacli - image: pelagosnetwork/pelacli:latest - volumes: - - ./pelacli_data:/consensus_data - - ./config/consensus_chains.json:/consensus_chains.json:ro - - ./config/ext_networks.json:/ext_networks.json:ro - - ./multichain:/multichain - command: - - consensus - - --snapshot-dir=/consensus_data - - --appchain=42=appchain:9090 - - --ask-period=1s - - --multichain-dir=/consensus_data/multichain_db - - --chains-json=/consensus_chains.json - - --ext-txn-config-json=/ext_networks.json + # Zero-config: uses SDK defaults + # - chain_id: 42 + # - emitter: :9090 + # - rpc: :8080 + # - data: ./data ``` -**What the paths mean** +**Data directory structure** -* Appchain: +Both containers share the same `./data` volume: - * `--stream-dir=/consensus_data/events` → pelacli writes `epoch_1.data` here. - * `--tx-dir=/consensus_data/fetcher/snapshots/42` → pelacli writes the read-only MDBX with `txbatch` table here. - * `--multichain-config=/data/chain_data.json` → maps chain IDs to MDBX DBs for external access. +``` +./data/ +├── multichain/ # External chain data (written by pelacli, read by appchain) +│ ├── 11155111/ # Ethereum Sepolia +│ │ └── sqlite +│ └── 80002/ # Polygon Amoy +│ └── sqlite +├── events/ # Consensus events (written by pelacli, read by appchain) +│ └── epoch_1.data +├── fetcher/ # Transaction batches (written by pelacli, read by appchain) +│ └── 42/ # Per-appchain (chainID=42) +│ └── mdbx.dat +├── appchain/ # Appchain state (written by appchain) +│ └── 42/ +│ └── mdbx.dat +└── local/ # Local node data like txpool (written by appchain) + └── 42/ + └── mdbx.dat +``` -* pelacli: +* **pelacli** writes: `events/`, `fetcher/`, `multichain/` +* **appchain** writes: `appchain/`, `local/` +* **appchain** reads: `events/`, `fetcher/`, `multichain/` - * `--chains-json=/consensus_chains.json` → tells pelacli which L1/L2s to fetch (Sepolia in the example). - * `--ext-txn-config-json=/ext_networks.json` → configures external chains that pelacli can send transactions to. - * `--appchain=42=appchain:9090` → your **ChainID** is `42` and the appchain gRPC emitter is at `appchain:9090`. - * `--snapshot-dir` and `--multichain-dir` live under `/consensus_data` (shared with appchain as `./pelacli_data`). +> Keep **ChainID=42** consistent across your code and pelacli config (both default to 42). -> Keep **ChainID=42** consistent across your code (`const ChainID = 42`), pelacli mapping, and the `--tx-dir` path that includes `/snapshots/42`. +## Zero-Config Setup -## Configuration files +The default setup requires **no configuration files**. Pelacli comes with built-in testnet defaults: -### `config/consensus_chains.json` (used by **pelacli** for **reading** from external chains) +| Network | Chain ID | Pelagos Contract | +|---------|----------|------------------| +| Sepolia | 11155111 | `0x922e02fFbDe8ABbF3058ccC3f9433018A2ff8C1d` | +| Polygon Amoy | 80002 | `0x98D34a83c45FEae289f6FA72ba009Ad49F3D26ED` | -> **Put your API key** (Alchemy/Infura/etc.) so pelacli can fetch external chain data and materialize it in MDBX. +Just clone and run: -```json -[ - { - "ChainID": 11155111, - "DBPath": "/multichain/sepolia", - "APIKey": "YOUR_API_KEY_HERE", - "StartBlock": 9214937 - } -] +```bash +git clone https://github.com/0xAtelerix/example +cd example +docker compose up -d ``` -* `DBPath` must live under the mounted `/multichain` volume (shared with appchain). -* `StartBlock` controls the initial sync point. -### `config/chain_data.json` (used by **appchain** for **reading** external chain data) +## Custom Configuration (Optional) -> Maps **chain IDs** to **MDBX paths** (the same ones pelacli maintains). Your appchain uses this to access external blockchain state. +For production or custom setups, copy the example config files and modify as needed. -```json -{ - "11155111": "/multichain/sepolia" -} +### Appchain Configuration + +```bash +cp config.example.yaml config.yaml +# Edit config.yaml with your settings +./appchain -config=config.yaml ``` -### `config/ext_networks.json` (used by **pelacli** for **writing** to external chains) +Available settings in `config.yaml`: + +| Field | Default | Description | +|-------|---------|-------------| +| `chain_id` | 42 | Appchain identifier (must match pelacli) | +| `data_dir` | `./data` | Root data directory shared with pelacli | +| `emitter_port` | `:9090` | gRPC port for emitter API | +| `rpc_port` | `:8080` | HTTP port for JSON-RPC server | +| `log_level` | 1 | Log verbosity: 0=debug, 1=info, 2=warn, 3=error | -> Configures external chains that pelacli can send transactions to. Your appchain generates `ExternalTransaction` items that pelacli processes and submits using these credentials. +See [`config.example.yaml`](./config.example.yaml) for the complete example. -```json -[ - { - "chainId": 137, - "rpcUrl": "https://polygon-rpc.com", - "contractAddress": "0x1234567890123456789012345678901234567890", - "privateKey": "YOUR_PRIVATE_KEY_HERE" - } -] +### Pelacli Configuration + +```bash +cp pelacli.example.yaml pelacli.yaml +# Edit pelacli.yaml with your settings +pelacli consensus --config=pelacli.yaml ``` -* `chainId` → The chain ID of the external chain (e.g., 137 for Polygon mainnet, 80002 for Polygon Amoy testnet). -* `rpcUrl` → RPC endpoint for the external chain. -* `contractAddress` → Address of the Pelagos contract on this chain that will route transactions to appchain contracts. -* `privateKey` → Private key that pelacli will use to sign and send transactions to this chain. +Key configuration sections: -> ⚠️ **Security Note**: Keep your private keys secure. Never commit `ext_networks.json` with real private keys to version control. The private key account must have sufficient native tokens for gas fees and appropriate permissions to interact with the Pelagos contract. +| Section | Default | Description | +|---------|---------|-------------| +| `data_dir` | `./data` | Root data directory (must match appchain's `data_dir`) | +| `consensus` | - | Polling interval and fetcher settings | +| `api` | - | External transactions REST API settings | +| `avail_da` | - | Avail DA integration (optional) | +| `appchains` | `42@:9090` | Which appchains to connect to | +| `read_chains` | Sepolia, Amoy | WSS endpoints for reading external blocks | +| `write_chains` | Sepolia, Amoy | RPC endpoints for sending cross-chain transactions | +**Block offset values** (for `read_chains`): +- `-4` = safe (2 blocks behind latest) +- `-3` = finalized (most secure, ~15 min delay on Ethereum) +- `-2` = latest (no delay, default for testnets) +- `1`, `2`, etc. = fixed offset from latest (e.g., `2` means if latest is block X, you get block X-2) -## Build & Run +See [`pelacli.example.yaml`](./pelacli.example.yaml) for all available options with detailed comments. -1. **Fill configs:** - * Put your API key into `config/consensus_chains.json` (required for reading external chain data). - * Ensure `config/chain_data.json` points to the same MDBX path(s). - * Configure external transaction networks in `config/ext_networks.json` (optional, only needed for writing to external chains). +## Build & Run -2. **Start:** - * Make sure local docker daemon is working +1. **Start (zero-config):** ```bash docker compose up -d ``` -3. **Check health:** +2. **Check health:** ```bash curl -s http://localhost:8080/health | jq . ``` -4. **Tail logs:** +3. **Tail logs:** ```bash docker compose logs -f pelacli docker compose logs -f appchain ``` -5. **Test:** - * If you are running the skeleton app without changes, you can use the provided script to send test transactions. - +4. **Test:** + ```bash ./test_txns.sh ``` -> On the first run, pelacli will populate MDBX and start producing events/tx-batches. Your appchain waits until the event file and tx-batch DB exist, then begins processing. +> On the first run, pelacli will populate SQLite databases and start producing events/tx-batches. Your appchain waits until the event file and tx-batch DB exist, then begins processing. --- @@ -319,15 +332,16 @@ curl -s http://localhost:8080/rpc \ ## Flags (quick reference) -These are wired in `main.go` and already set in `docker-compose.yml`: +The appchain uses SDK defaults. Override with config file: + +* `-config=config.yaml` — Path to YAML config file (optional) -* `--emitter-port=:9090` — gRPC emitter (pelacli pulls txs here) -* `--db-path=/data/appchain-db` — appchain MDBX -* `--local-db-path=/data/local-db` — tx pool MDBX -* `--stream-dir=/consensus_data/events` — event file directory (pelacli writes) -* `--tx-dir=/consensus_data/fetcher/snapshots/42` — **read-only** tx-batch MDBX (pelacli writes) -* `--rpc-port=:8080` — JSON-RPC server -* `--multichain-config=/data/chain_data.json` — external chain MDBX mapping +All settings can be configured in the YAML file: +* `chain_id` — Appchain identifier (default: 42) +* `data_dir` — Root data directory (default: `./data`) +* `emitter_port` — gRPC emitter port (default: `:9090`) +* `rpc_port` — JSON-RPC server port (default: `:8080`) +* `log_level` — Log verbosity 0-3 (default: 1=info) ## Additional Resources @@ -335,4 +349,4 @@ These are wired in `main.go` and already set in `docker-compose.yml`: - [SDK Contracts](https://github.com/0xAtelerix/sdk/tree/main/contracts) — Deploy your own AppChain contracts on external chains for custom cross-chain functionality --- -**Happy building! 🚀** For questions or issues, check the [issues](https://github.com/0xAtelerix/example/issues) or reach out to the Pelagos community. +**Happy building!** For questions or issues, check the [issues](https://github.com/0xAtelerix/example/issues) or reach out to the Pelagos community. From 150ff302e5daa5647522956ca37d0fdd3532fefc Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Sun, 14 Dec 2025 23:40:23 +0530 Subject: [PATCH 2/7] Update SDK --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d89d4ab..0495fda 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/0xAtelerix/example go 1.25.0 require ( - github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2 + github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.16.3 github.com/holiman/uint256 v1.3.2 diff --git a/go.sum b/go.sum index c848e80..09f5794 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2 h1:lyW4x1FeRQ+Kyl0OLzsM4wnSJNCKOi4Aoth8jM6Ptkk= -github.com/0xAtelerix/sdk v0.1.7-0.20251206184030-4cf47f27c5d2/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc h1:zQZiAZGuqOEGr4IUEMz3jSF7DL6i58vBiGQnuX4HUMM= +github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= From 1250bac4df468f97d6fe242552c2ef59d353de66 Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Tue, 30 Dec 2025 20:05:40 +0530 Subject: [PATCH 3/7] Updated with latest SDK code --- application/external_block_processor.go | 2 +- cmd/config.go | 76 ------------------------- cmd/main.go | 63 ++++++++------------ cmd/main_test.go | 22 +++---- config.example.yaml | 48 ++++++++++------ docker-compose.yml | 18 +++--- go.mod | 4 +- go.sum | 8 ++- pelacli.example.yaml | 39 +++++++++++-- 9 files changed, 113 insertions(+), 167 deletions(-) delete mode 100644 cmd/config.go diff --git a/application/external_block_processor.go b/application/external_block_processor.go index 8c34756..55dfd10 100644 --- a/application/external_block_processor.go +++ b/application/external_block_processor.go @@ -64,7 +64,7 @@ type ExtBlockProcessor struct { msa gosdk.MultichainStateAccessor } -func NewExtBlockProcessor(msa gosdk.MultichainStateAccessor) *ExtBlockProcessor { +func NewExtBlockProcessor(msa gosdk.MultichainStateAccessor) gosdk.ExternalBlockProcessor { return &ExtBlockProcessor{ msa: msa, } diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index a2a32a8..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/0xAtelerix/sdk/gosdk" - "github.com/rs/zerolog" - "gopkg.in/yaml.v3" -) - -// Config holds the appchain configuration. -type Config struct { - // ChainID is the unique identifier for this appchain. - ChainID uint64 `yaml:"chain_id"` - - // DataDir is the root data directory (shared with pelacli). - DataDir string `yaml:"data_dir"` - - // EmitterPort is the gRPC port for the emitter API. - EmitterPort string `yaml:"emitter_port"` - - // RPCPort is the HTTP port for JSON-RPC server. - RPCPort string `yaml:"rpc_port"` - - // LogLevel is the zerolog log level (0=debug, 1=info, 2=warn, 3=error). - LogLevel int `yaml:"log_level"` -} - -// DefaultConfig returns a config with all defaults applied. -func DefaultConfig() *Config { - cfg := &Config{} - cfg.applyDefaults() - - return cfg -} - -// LoadConfig loads configuration from a YAML file. -func LoadConfig(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read config: %w", err) - } - - var cfg Config - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("parse config: %w", err) - } - - cfg.applyDefaults() - - return &cfg, nil -} - -// applyDefaults sets default values for unset fields. -func (c *Config) applyDefaults() { - if c.ChainID == 0 { - c.ChainID = gosdk.DefaultAppchainID - } - - if c.DataDir == "" { - c.DataDir = gosdk.DefaultDataDir - } - - if c.EmitterPort == "" { - c.EmitterPort = gosdk.DefaultEmitterPort - } - - if c.RPCPort == "" { - c.RPCPort = gosdk.DefaultRPCPort - } - - if c.LogLevel == 0 { - c.LogLevel = int(zerolog.InfoLevel) - } -} diff --git a/cmd/main.go b/cmd/main.go index 9f7781d..e4d614a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,8 +10,6 @@ import ( "github.com/0xAtelerix/sdk/gosdk" "github.com/0xAtelerix/sdk/gosdk/rpc" - "github.com/0xAtelerix/sdk/gosdk/txpool" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/0xAtelerix/example/application" @@ -19,9 +17,6 @@ import ( ) func main() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // Parse command line flags fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) configPath := fs.String("config", "", "Path to config.yaml (optional)") @@ -29,29 +24,27 @@ func main() { // Load config from file or use defaults var ( - cfg *Config + cfg *gosdk.InitConfig err error ) if *configPath != "" { - cfg, err = LoadConfig(*configPath) + cfg, err = gosdk.LoadConfig(*configPath) if err != nil { log.Fatal().Err(err).Msg("Failed to load config") } log.Info().Str("config", *configPath).Msg("Loaded config") } else { - cfg = DefaultConfig() + cfg = &gosdk.InitConfig{} log.Info().Msg("Using default config") } // Setup logging - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}). - Level(zerolog.Level(cfg.LogLevel)) - ctx = log.With().Logger().WithContext(ctx) + ctx := gosdk.SetupLogger(context.Background(), cfg.LogLevel) - // Handle shutdown signals + // signal.NotifyContext provides cancellation on SIGINT/SIGTERM ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() @@ -61,48 +54,42 @@ func main() { } // Run starts the appchain with the given config. Exported for testing. -func Run(ctx context.Context, cfg *Config) error { - // Initialize all common components (SDK Defaults if not provided) - appInit, err := gosdk.Init(ctx, gosdk.InitConfig{ - ChainID: cfg.ChainID, - DataDir: cfg.DataDir, - EmitterPort: cfg.EmitterPort, - CustomTables: application.Tables(), - Logger: &log.Logger, - }) +func Run(ctx context.Context, cfg *gosdk.InitConfig) error { + // Add custom tables to config + cfg.CustomTables = application.Tables() + + // Stage 1: Initialize storage and config (logger comes from context) + appInit, err := gosdk.InitApp[application.Transaction[application.Receipt]](ctx, *cfg) if err != nil { - return fmt.Errorf("init appchain: %w", err) + return fmt.Errorf("init storage: %w", err) } defer appInit.Close() - // Create transaction pool - txPool := txpool.NewTxPool[application.Transaction[application.Receipt]](appInit.LocalDB) - - // Create appchain with app-specific logic - appchain := gosdk.NewAppchain( - appInit, + // Stage 2: Create appchain with batch processor + app := gosdk.NewAppchain( + appInit.Storage, + appInit.Config, gosdk.NewDefaultBatchProcessor[application.Transaction[application.Receipt]]( - application.NewExtBlockProcessor(appInit.Multichain), - appInit.Multichain, - appInit.Subscriber, + application.NewExtBlockProcessor(appInit.Storage.Multichain()), + appInit.Storage.Multichain(), + appInit.Storage.Subscriber(), ), application.BlockConstructor, - txPool, ) // Initialize dev validator set (local development only) - if err := gosdk.InitDevValidatorSet(ctx, appInit.AppchainDB); err != nil { + if err := gosdk.InitDevValidatorSet(ctx, appInit.Storage.AppchainDB()); err != nil { return fmt.Errorf("init dev validator set: %w", err) } // Initialize app-specific genesis state - if err := application.InitializeGenesis(ctx, appInit.AppchainDB); err != nil { + if err := application.InitializeGenesis(ctx, appInit.Storage.AppchainDB()); err != nil { return fmt.Errorf("init genesis state: %w", err) } // Run appchain in background go func() { - if err := appchain.Run(ctx); err != nil { + if err := app.Run(ctx); err != nil { log.Ctx(ctx).Error().Err(err).Msg("Appchain error") } }() @@ -115,9 +102,9 @@ func Run(ctx context.Context, cfg *Config) error { application.Transaction[application.Receipt], application.Receipt, application.Block, - ](rpcServer, appInit.AppchainDB, txPool, cfg.ChainID) + ](rpcServer, appInit.Storage.AppchainDB(), appInit.Storage.TxPool(), appInit.Config.ChainID) - api.NewCustomRPC(rpcServer, appInit.AppchainDB).AddRPCMethods() + api.NewCustomRPC(rpcServer, appInit.Storage.AppchainDB()).AddRPCMethods() - return rpcServer.StartHTTPServer(ctx, cfg.RPCPort) + return rpcServer.StartHTTPServer(ctx, appInit.Config.RPCPort) } diff --git a/cmd/main_test.go b/cmd/main_test.go index 6c3b062..b36c8ed 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "os" - "path/filepath" "testing" "time" @@ -45,8 +44,8 @@ func TestEndToEnd(t *testing.T) { chainID := uint64(1001) // Create required directories and databases - // SDK's Init() expects these paths to exist - txBatchPath := gosdk.TxBatchPath(dataDir, chainID) + // SDK's InitApp() expects these paths to exist + txBatchPath := gosdk.TxBatchPathForChain(dataDir, chainID) eventsPath := gosdk.EventsPath(dataDir) require.NoError(t, os.MkdirAll(txBatchPath, 0o755)) @@ -56,18 +55,13 @@ func TestEndToEnd(t *testing.T) { err := createEmptyMDBXDatabase(txBatchPath, gosdk.TxBucketsTables()) require.NoError(t, err, "create empty txBatch database") - // Create events file (normally created by pelacli) - eventsFile := filepath.Join(eventsPath, "epoch_1.data") - f, err := os.Create(eventsFile) - require.NoError(t, err) - require.NoError(t, f.Close()) - // Create config with test values - cfg := &Config{ - ChainID: chainID, - DataDir: dataDir, - EmitterPort: ":0", // Let OS choose - RPCPort: fmt.Sprintf(":%d", port), + cfg := &gosdk.InitConfig{ + ChainID: &chainID, + DataDir: dataDir, + EmitterPort: ":0", // Let OS choose + RPCPort: fmt.Sprintf(":%d", port), + RequiredChains: []uint64{}, } // Create cancellable context for the test diff --git a/config.example.yaml b/config.example.yaml index 206332b..23b191e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,11 +1,7 @@ -# ============================================================================= -# APPCHAIN CONFIGURATION -# ============================================================================= +# Appchain Configuration # Copy this file to config.yaml and modify as needed. # All fields are optional - defaults will be used if not specified. -# # Run with: ./appchain --config=config.yaml -# ============================================================================= # Unique identifier for this appchain # Must match the chain_id in pelacli's appchains config @@ -14,23 +10,41 @@ chain_id: 42 # Root data directory shared with pelacli # Both appchain and pelacli must use the same data_dir for communication -# Default: ./data -data_dir: "./data" +# Default: /data (container path, works with docker-compose default mounts) +data_dir: "/data" -# gRPC port for emitter API -# Pelacli's fetcher connects to this port to fetch tx batches and checkpoints +# gRPC port for emitter API (pelacli fetcher connects here) # Default: :9090 emitter_port: ":9090" -# HTTP port for JSON-RPC server -# Clients connect to this port to submit transactions and query state +# HTTP port for JSON-RPC server (clients connect here) # Default: :8080 rpc_port: ":8080" -# Log level for application logging -# 0 = debug (most verbose) -# 1 = info (default) -# 2 = warn -# 3 = error (least verbose) -# Default: 1 +# Log level: -1=trace, 0=debug, 1=info, 2=warn, 3=error +# Default: 1 (info) log_level: 1 + +# Required external chains that this appchain reads from +# Appchain will wait for these chains' data at startup +# Common chain IDs: 11155111 (Eth Sepolia), 80002 (Polygon Amoy), 97 (BSC testnet) +required_chains: + - 80002 # Polygon Amoy testnet + - 11155111 # Ethereum Sepolia testnet + +# Prometheus metrics port (optional) +# Default: "" (disabled) +# prometheus_port: ":2112" + +# Validator ID for consensus (optional) +# Default: "" (uses default validator) +# validator_id: "validator-1" + +# Custom paths (advanced use only - overrides standard data_dir structure) +# NOTE: Appchain and pelacli must use compatible paths for communication +# custom_paths: +# multichain_dir: "/custom/path/to/multichain" +# appchain_db_dir: "/custom/path/to/appchain/db" +# txpool_dir: "/custom/path/to/txpool" +# events_stream_dir: "/custom/path/to/events" +# txbatch_dir: "/custom/path/to/txbatch" diff --git a/docker-compose.yml b/docker-compose.yml index 7a5b2d6..611e7fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,15 +4,13 @@ services: image: pelagosnetwork/pelacli:latest working_dir: /app volumes: - - ./data:/app/data - # Uncomment to use custom config: - # - ./pelacli.yaml:/app/pelacli.yaml + - ./data:/data + # - ./pelacli.yaml:/config/pelacli.yaml ports: - - "8081:8081" #Default API-port, Use config to change + - "8081:8081" command: - consensus - # Uncomment to use custom config: - # - --config=/app/pelacli.yaml + # - --config=/config/pelacli.yaml appchain: build: @@ -22,17 +20,15 @@ services: pid: "service:pelacli" image: appchain:latest volumes: - - ./data:/app/data - # Uncomment to use custom config: - # - ./config.yaml:/app/config.yaml + - ./data:/data + # - ./config.yaml:/config/config.yaml ports: - "9090:9090" - "8080:8080" depends_on: - pelacli - # Uncomment to use custom config: # command: - # - -config=/app/config.yaml + # - -config=/config/config.yaml explorer: image: pelagosnetwork/explorer:latest diff --git a/go.mod b/go.mod index 0495fda..30d63a4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/0xAtelerix/example go 1.25.0 require ( - github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc + github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5 github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.16.3 github.com/holiman/uint256 v1.3.2 @@ -11,7 +11,6 @@ require ( github.com/ledgerwatch/log/v3 v3.9.0 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -64,6 +63,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect google.golang.org/grpc v1.75.1 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 => google.golang.org/genproto v0.0.0-20250707201910-8d1bb00bc6a7 diff --git a/go.sum b/go.sum index 09f5794..2aaef89 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc h1:zQZiAZGuqOEGr4IUEMz3jSF7DL6i58vBiGQnuX4HUMM= -github.com/0xAtelerix/sdk v0.1.7-0.20251214090843-ebdecda41cdc/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7-0.20251230125616-2c39bfdb98c5 h1:XIxpZCn9Fy38uY9pBUw2nSX/KYr+qrodgPR588/2wSw= +github.com/0xAtelerix/sdk v0.1.7-0.20251230125616-2c39bfdb98c5/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7-0.20251230142116-3b1098726a96 h1:nNSDyH0Ft1aWoGvq6QgMDUVcbg77Uu531adufnrDJs0= +github.com/0xAtelerix/sdk v0.1.7-0.20251230142116-3b1098726a96/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5 h1:rCoE/cyR2fUzX7/ErYnXPcM0pTqREbzRBeSemQ2ucyI= +github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= diff --git a/pelacli.example.yaml b/pelacli.example.yaml index 183ae7c..a71e432 100644 --- a/pelacli.example.yaml +++ b/pelacli.example.yaml @@ -7,10 +7,37 @@ # Run with: pelacli consensus --config=pelacli.yaml # ============================================================================= -# Root data directory for all pelacli data (events, fetcher, multichain) -# Must match appchain's data_dir for communication -# Default: ./data -data_dir: "./data" +# ============================================================================= +# DATA DIR +# ============================================================================= +# Root data directory shared with appchain +# Both appchain and pelacli must use the same data_dir for communication +# Default: /data (container path, works with docker-compose default mounts) +data_dir: "/data" + +# ============================================================================= +# CUSTOM PATHS (OPTIONAL) +# ============================================================================= +# Override specific directory paths. If not set, paths are derived from data_dir. +# Useful for advanced deployments requiring non-standard directory layouts. +# +# custom_paths: +# # Fetcher database directory +# # Default: {data_dir}/consensus/fetcher-db +# fetcher_db_dir: "/custom/path/to/fetcher-db" +# +# # Consensus events directory +# # Default: {data_dir}/consensus/events +# events_dir: "/custom/path/to/events" +# +# # Transaction batch directory +# # Default: {data_dir}/consensus/txbatch +# txbatch_dir: "/custom/path/to/txbatch" +# +# # Multichain root directory +# # Individual chain paths: {multichain_dir}/{chainID} +# # Default: {data_dir}/multichain +# multichain_dir: "/custom/path/to/multichain" # ============================================================================= # CONSENSUS SETTINGS @@ -50,11 +77,11 @@ avail_da: # APPCHAIN CONNECTIONS # ============================================================================= # Configure which appchains pelacli should connect to for fetching transactions. -# If not specified, defaults to chain_id=42 at :9090 (SDK's DefaultAppchainID) +# If not specified, defaults to chain_id=42 at appchain:9090 (Docker service name) # # appchains: # - chain_id: 42 -# address: ":9090" # gRPC address of appchain's emitter +# address: "appchain:9090" # Docker service name for inter-container communication # ============================================================================= # READ CHAINS - WSS connections for reading external chain blocks (oracle) From 8050f0b7049934b006be419bb48fe185d6a193d2 Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Tue, 30 Dec 2025 22:24:21 +0530 Subject: [PATCH 4/7] Update SDK --- application/external_block_processor.go | 11 ++++++----- application/solana_payload.go | 4 ++-- application/transaction.go | 4 ++-- go.mod | 2 +- go.sum | 8 ++------ 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/application/external_block_processor.go b/application/external_block_processor.go index 55dfd10..437cfa3 100644 --- a/application/external_block_processor.go +++ b/application/external_block_processor.go @@ -9,6 +9,7 @@ import ( "github.com/0xAtelerix/sdk/gosdk/apptypes" "github.com/0xAtelerix/sdk/gosdk/evmtypes" "github.com/0xAtelerix/sdk/gosdk/external" + "github.com/0xAtelerix/sdk/gosdk/library" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -76,9 +77,9 @@ func (p *ExtBlockProcessor) ProcessBlock( tx kv.RwTx, ) ([]apptypes.ExternalTransaction, error) { switch { - case gosdk.IsEvmChain(apptypes.ChainType(b.ChainID)): + case library.IsEvmChain(apptypes.ChainType(b.ChainID)): return p.processEVMBlock(b, tx) - case gosdk.IsSolanaChain(apptypes.ChainType(b.ChainID)): + case library.IsSolanaChain(apptypes.ChainType(b.ChainID)): return p.processSolanaBlock(b, tx) default: log.Warn().Uint64("chainID", b.ChainID).Msg("Unsupported external chain, skipping...") @@ -226,7 +227,7 @@ func (*ExtBlockProcessor) processReceipt( // Create an external transaction record for the destination chain (EVM) extTx, err := external.NewExTxBuilder( createTokenMintPayload(userAddr, amountOut, tokenOut), - gosdk.EthereumSepoliaChainID). + library.EthereumSepoliaChainID). Build() if err != nil { log.Error().Err(err).Msg("Failed to create external transaction for swap event") @@ -243,7 +244,7 @@ func (*ExtBlockProcessor) processReceipt( Str("tokenOut", tokenOut). Str("amountIn", amountIn.String()). Str("amountOut", amountOut.String()). - Uint64("target_chainID", uint64(gosdk.EthereumSepoliaChainID)). + Uint64("target_chainID", uint64(library.EthereumSepoliaChainID)). Msg("Processed swap event - EVM to EVM") case WithdrawToSolanaSignature: @@ -268,7 +269,7 @@ func (*ExtBlockProcessor) processReceipt( log.Info(). Uint64("source_chainID", chainID). Str("amount", amount.String()). - Uint64("target_chainID", uint64(gosdk.SolanaDevnetChainID)). + Uint64("target_chainID", uint64(library.SolanaDevnetChainID)). Msg("Processed withdraw to Solana event - EVM to Solana withdraw") default: diff --git a/application/solana_payload.go b/application/solana_payload.go index 392a76f..c24a972 100644 --- a/application/solana_payload.go +++ b/application/solana_payload.go @@ -4,9 +4,9 @@ import ( "encoding/binary" "fmt" - "github.com/0xAtelerix/sdk/gosdk" "github.com/0xAtelerix/sdk/gosdk/apptypes" "github.com/0xAtelerix/sdk/gosdk/external" + "github.com/0xAtelerix/sdk/gosdk/library" "github.com/blocto/solana-go-sdk/common" "github.com/blocto/solana-go-sdk/types" ) @@ -128,7 +128,7 @@ func createSolanaMintPayload(amount uint64) (apptypes.ExternalTransaction, error // Build account list: appchainProg first, then specifics (mint, ata, authority, token) // Total: 5 accounts will be passed to appchain after CPI // In appchain: [0]=program, [1]=mint, [2]=ata, [3]=authority, [4]=token - exTx, err := external.NewExTxBuilder(appchainData, gosdk.SolanaDevnetChainID). + exTx, err := external.NewExTxBuilder(appchainData, library.SolanaDevnetChainID). AddSolanaAccounts([]types.AccountMeta{appchainProg, mintAcc, ataAcc, authorityAcc, tokenProg}). Build() if err != nil { diff --git a/application/transaction.go b/application/transaction.go index b29fdd6..bd6f408 100644 --- a/application/transaction.go +++ b/application/transaction.go @@ -6,9 +6,9 @@ import ( "math/big" "strings" - "github.com/0xAtelerix/sdk/gosdk" "github.com/0xAtelerix/sdk/gosdk/apptypes" "github.com/0xAtelerix/sdk/gosdk/external" + "github.com/0xAtelerix/sdk/gosdk/library" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/holiman/uint256" @@ -173,7 +173,7 @@ func (e *Transaction[R]) createExternalTransaction() (apptypes.ExternalTransacti payload := createTokenMintPayload(recipientAddr, amount, e.Token) // Create external transaction targeting Sepolia - extTx, err := external.NewExTxBuilder(payload, gosdk.EthereumSepoliaChainID).Build() + extTx, err := external.NewExTxBuilder(payload, library.EthereumSepoliaChainID).Build() if err != nil { return apptypes.ExternalTransaction{}, err } diff --git a/go.mod b/go.mod index 30d63a4..40f3915 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/0xAtelerix/example go 1.25.0 require ( - github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5 + github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.16.3 github.com/holiman/uint256 v1.3.2 diff --git a/go.sum b/go.sum index 2aaef89..b12a391 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xAtelerix/sdk v0.1.7-0.20251230125616-2c39bfdb98c5 h1:XIxpZCn9Fy38uY9pBUw2nSX/KYr+qrodgPR588/2wSw= -github.com/0xAtelerix/sdk v0.1.7-0.20251230125616-2c39bfdb98c5/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= -github.com/0xAtelerix/sdk v0.1.7-0.20251230142116-3b1098726a96 h1:nNSDyH0Ft1aWoGvq6QgMDUVcbg77Uu531adufnrDJs0= -github.com/0xAtelerix/sdk v0.1.7-0.20251230142116-3b1098726a96/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= -github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5 h1:rCoE/cyR2fUzX7/ErYnXPcM0pTqREbzRBeSemQ2ucyI= -github.com/0xAtelerix/sdk v0.1.7-0.20251230143106-dbcf0b7b00a5/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb h1:vbZt6Lxlbe7iROkgDmKGvVovaLRXP0NUubCg8UOQekg= +github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= From f37f533eddfb9733b807374c39c2e01137809105 Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Tue, 30 Dec 2025 23:12:10 +0530 Subject: [PATCH 5/7] Handle graceful shutdown --- cmd/main.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e4d614a..537fd7f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -87,14 +87,7 @@ func Run(ctx context.Context, cfg *gosdk.InitConfig) error { return fmt.Errorf("init genesis state: %w", err) } - // Run appchain in background - go func() { - if err := app.Run(ctx); err != nil { - log.Ctx(ctx).Error().Err(err).Msg("Appchain error") - } - }() - - // Setup and start JSON-RPC server + // Setup JSON-RPC server rpcServer := rpc.NewStandardRPCServer(nil) rpcServer.AddMiddleware(api.NewExampleMiddleware(log.Logger)) @@ -106,5 +99,26 @@ func Run(ctx context.Context, cfg *gosdk.InitConfig) error { api.NewCustomRPC(rpcServer, appInit.Storage.AppchainDB()).AddRPCMethods() - return rpcServer.StartHTTPServer(ctx, appInit.Config.RPCPort) + // Error channel for goroutines + errCh := make(chan error, 2) + + // Run appchain in background + go func() { + errCh <- app.Run(ctx) + }() + + // Run RPC server in background + go func() { + errCh <- rpcServer.StartHTTPServer(ctx, appInit.Config.RPCPort) + }() + + // Wait for shutdown signal or error + select { + case <-ctx.Done(): + log.Ctx(ctx).Info().Msg("Shutdown signal received") + + return nil + case err := <-errCh: + return err + } } From 5cb9d029985e67bb24ff5f1453ef85bbdabcc39c Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Wed, 31 Dec 2025 18:43:43 +0530 Subject: [PATCH 6/7] Log error --- cmd/main.go | 2 ++ cmd/main_test.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index 537fd7f..22a1b29 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -119,6 +119,8 @@ func Run(ctx context.Context, cfg *gosdk.InitConfig) error { return nil case err := <-errCh: + log.Ctx(ctx).Error().Err(err).Msg("Appchain error") + return err } } diff --git a/cmd/main_test.go b/cmd/main_test.go index b36c8ed..74cfe68 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -70,7 +70,9 @@ func TestEndToEnd(t *testing.T) { // Run appchain in background go func() { - _ = Run(ctx, cfg) + if runErr := Run(ctx, cfg); runErr != nil { + t.Logf("Run error: %v", runErr) + } }() // Wait until HTTP service is up From 6cc39701763aeb3719e0d4e2304c50ac97b82957 Mon Sep 17 00:00:00 2001 From: Krishna Upadhyaya Date: Tue, 6 Jan 2026 20:13:48 +0530 Subject: [PATCH 7/7] Update SDK --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 40f3915..74985e5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/0xAtelerix/example go 1.25.0 require ( - github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb + github.com/0xAtelerix/sdk v0.1.7 github.com/blocto/solana-go-sdk v1.30.0 github.com/ethereum/go-ethereum v1.16.3 github.com/holiman/uint256 v1.3.2 diff --git a/go.sum b/go.sum index b12a391..27d9637 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb h1:vbZt6Lxlbe7iROkgDmKGvVovaLRXP0NUubCg8UOQekg= -github.com/0xAtelerix/sdk v0.1.7-0.20251230161310-edae7d68bfeb/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= +github.com/0xAtelerix/sdk v0.1.7 h1:8cEVHTs2vL4LYAuNKwSQcxqssztOfDETzYtl9gvGnak= +github.com/0xAtelerix/sdk v0.1.7/go.mod h1:tYa+zDjpx3DrDnTrqU2gASr8BmEoDzz6XrPuPkNcx64= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=