From ae440101f17f47f20da75339d8a7ce9cd6711a06 Mon Sep 17 00:00:00 2001 From: Sledro Date: Sat, 27 Jan 2024 18:17:06 +0000 Subject: [PATCH 1/8] restrict publisher commands --- cli/cmd/rollup_next.go | 6 ++++++ cli/cmd/rollup_start.go | 6 ++++++ node/ethereum.go | 13 ++++++++++++- node/node.go | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cli/cmd/rollup_next.go b/cli/cmd/rollup_next.go index 74a35d3..ce7fe7a 100644 --- a/cli/cmd/rollup_next.go +++ b/cli/cmd/rollup_next.go @@ -33,6 +33,12 @@ var RollupNextCmd = &cobra.Command{ n, err := node.NewFromConfig(cfg, logger, ethKey) utils.NoErr(err) + // Can only run rollup node if the eth key is a publisher + if !n.IsPublisher(ethKey) { + logger.Warn("ETH_KEY is not a publisher, cannot run rollup next command") + return + } + r := rollup.NewRollup(n, &rollup.Opts{ L1PollDelay: time.Duration(cfg.Rollup.L1PollDelay) * time.Millisecond, L2PollDelay: time.Duration(cfg.Rollup.L2PollDelay) * time.Millisecond, diff --git a/cli/cmd/rollup_start.go b/cli/cmd/rollup_start.go index 0eaf37d..9326bd1 100644 --- a/cli/cmd/rollup_start.go +++ b/cli/cmd/rollup_start.go @@ -30,6 +30,12 @@ var RollupStartCmd = &cobra.Command{ n, err := node.NewFromConfig(cfg, logger, ethKey) utils.NoErr(err) + // Can only run rollup node if the eth key is a publisher + if !n.IsPublisher(ethKey) { + logger.Warn("ETH_KEY is not a publisher, cannot run rollup start command") + return + } + r := rollup.NewRollup(n, &rollup.Opts{ L1PollDelay: time.Duration(cfg.Rollup.L1PollDelay) * time.Millisecond, L2PollDelay: time.Duration(cfg.Rollup.L2PollDelay) * time.Millisecond, diff --git a/node/ethereum.go b/node/ethereum.go index ddf3582..8309d60 100644 --- a/node/ethereum.go +++ b/node/ethereum.go @@ -25,16 +25,23 @@ import ( // It Provides access to the Ethereum Network and methods for // interacting with important contracts on the network including: // - CanonicalStateChain.sol With with methods for getting and pushing +// - DAOracle.sol With methods for verifying data availability +// - Challenge.sol With methods for challenging data availability etc d // rollup block headers. type Ethereum interface { + // CanonicalStateChain GetRollupHeight() (uint64, error) // Get the current rollup block height. GetHeight() (uint64, error) // Get the current block height of the Ethereum network. GetRollupHead() (canonicalStateChainContract.CanonicalStateChainHeader, error) // Get the latest rollup block header in the CanonicalStateChain.sol contract. PushRollupHead(header *canonicalStateChainContract.CanonicalStateChainHeader) (*types.Transaction, error) // Push a new rollup block header to the CanonicalStateChain.sol contract. GetRollupHeader(index uint64) (canonicalStateChainContract.CanonicalStateChainHeader, error) // Get the rollup block header at the given index from the CanonicalStateChain.sol contract. GetRollupHeaderByHash(hash common.Hash) (canonicalStateChainContract.CanonicalStateChainHeader, error) // Get the rollup block header with the given hash from the CanonicalStateChain.sol contract. - Wait(txHash common.Hash) (*types.Receipt, error) // Wait for the given transaction to be mined. + Wait(txHash common.Hash) (*types.Receipt, error) // Wait for a transaction to be mined. + GetPublisher() (common.Address, error) // Get the address of the publisher of the CanonicalStateChain.sol contract. + + // DAOracle DAVerify(*CelestiaProof) (bool, error) + // Check if the data availability layer is verified. // Challenges GetChallengeFee() (*big.Int, error) @@ -369,6 +376,10 @@ func (e *EthereumClient) FilterChallengeDAUpdate(opts *bind.FilterOpts, _blockHa return e.ws.challenge.FilterChallengeDAUpdate(opts, _blockHash, _blockIndex, _status) } +func (e *EthereumClient) GetPublisher() (common.Address, error) { + return e.http.canonicalStateChain.Publisher(nil) +} + // MOCK CLIENT FOR TESTING type ethereumMock struct { diff --git a/node/node.go b/node/node.go index 3aa35cd..e8d6709 100644 --- a/node/node.go +++ b/node/node.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/spf13/viper" ) @@ -114,3 +115,20 @@ func (n *Node) GetDAPointer(hash common.Hash) (*CelestiaPointer, error) { return pointer, nil } + +// Returns true if the given ethKey is the publisher set in CanonicalStateChain +func (n *Node) IsPublisher(ethKey *ecdsa.PrivateKey) bool { + if ethKey == nil { + panic("eth key is nil") + } + + p, err := n.Ethereum.GetPublisher() + if err != nil { + panic(err) + } + + // Get address of public key + addr := crypto.PubkeyToAddress(ethKey.PublicKey) + + return p == addr +} From c409710f67a9734a5205d125d88ac587aacf3bfc Mon Sep 17 00:00:00 2001 From: Sledro Date: Sat, 27 Jan 2024 18:19:30 +0000 Subject: [PATCH 2/8] auto-retry on failed submitBlob() with an increased gas price by 20% --- node/celestia.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/node/celestia.go b/node/celestia.go index dcdf7e7..26a88cd 100644 --- a/node/celestia.go +++ b/node/celestia.go @@ -135,8 +135,23 @@ func (c *CelestiaClient) PublishBundle(blocks Bundle) (*CelestiaPointer, error) // fee is gas price * gas limit. State machine does not refund users for unused gas so all of the fee is used fee := int64(gasPrice * float64(gasLimit)) - // post the blob - pointer, err := c.submitBlob(context.Background(), cosmosmath.NewInt(fee), gasLimit, []*blob.Blob{b}) + var pointer *CelestiaPointer + + // Retry up to 5 times + for i := 0; i < 5; i++ { + // post the blob + pointer, err = c.submitBlob(context.Background(), cosmosmath.NewInt(fee), gasLimit, []*blob.Blob{b}) + if err == nil { + break + } + + // Increase gas price by 20% + gasPrice *= 1.2 + fee = int64(gasPrice * float64(gasLimit)) + + c.logger.Warn("Failed to submit blob, retrying", "attempt", i+1, "fee", fee, "gas_limit", gasLimit, "gas_price", gasPrice, "error", err) + } + if err != nil { return nil, err } From 02993ed7a1ca85aef5e1898599f8e959246fdda3 Mon Sep 17 00:00:00 2001 From: Sledro Date: Sat, 27 Jan 2024 23:40:22 +0000 Subject: [PATCH 3/8] add helper to convert challenge status enum to string --- node/contracts/types.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/node/contracts/types.go b/node/contracts/types.go index b61a85f..0e282cf 100644 --- a/node/contracts/types.go +++ b/node/contracts/types.go @@ -9,3 +9,19 @@ type ChallengeDaInfo struct { Expiry *big.Int `pretty:"Expiry"` Status uint8 `pretty:"Status"` } + +// Helper to convert challenge status enum to string +func StatusString(c uint8) string { + switch c { + case 0: + return "None" + case 1: + return "ChallengerInitiated" + case 2: + return "DefenderResponded" + case 3: + return "ChallengerWon" + default: + return "Unknown" + } +} From 0081fc7b64279e2cae54ea96762055e81265c2b7 Mon Sep 17 00:00:00 2001 From: Sledro Date: Sun, 28 Jan 2024 02:39:57 +0000 Subject: [PATCH 4/8] improve defender logic and tidy --- cli/cmd/defender_defendda.go | 1 - cli/cmd/defender_proveda.go | 2 +- cli/cmd/defender_start.go | 10 +- defender/defender.go | 226 ++++++++++++++++++----------------- node/store.go | 103 ---------------- 5 files changed, 121 insertions(+), 221 deletions(-) diff --git a/cli/cmd/defender_defendda.go b/cli/cmd/defender_defendda.go index 2fb54dc..8e6d907 100644 --- a/cli/cmd/defender_defendda.go +++ b/cli/cmd/defender_defendda.go @@ -42,7 +42,6 @@ var DefenderDefendDaCmd = &cobra.Command{ d := defender.NewDefender(n, &defender.Opts{ Logger: logger.With("ctx", "Defender"), - DryRun: dryRun, }) // get block hash and tx hash from args/flags diff --git a/cli/cmd/defender_proveda.go b/cli/cmd/defender_proveda.go index 8530c08..dc0011c 100644 --- a/cli/cmd/defender_proveda.go +++ b/cli/cmd/defender_proveda.go @@ -40,7 +40,7 @@ var DefenderProveDaCmd = &cobra.Command{ }) blockHash := common.HexToHash(args[0]) - proof, err := d.ProveDA(blockHash) + proof, err := d.GetDAProof(blockHash) if err != nil { logger.Error("Failed to prove data availability", "err", err) panic(err) diff --git a/cli/cmd/defender_start.go b/cli/cmd/defender_start.go index 1f97c52..68a5104 100644 --- a/cli/cmd/defender_start.go +++ b/cli/cmd/defender_start.go @@ -26,11 +26,13 @@ var DefenderStartCmd = &cobra.Command{ Logger: logger.With("ctx", "Defender"), WorkerDelay: time.Duration(cfg.Defender.WorkerDelay) * time.Millisecond, }) + for { + err = d.Start() + if err != nil { + logger.Error("Defender.Start failed", "err", err, "retry_in", "5s") + } - err = d.Start() - if err != nil { - logger.Error("Defender.Start failed", "err", err, "retry_in", "5s") + time.Sleep(5 * time.Second) } - }, } diff --git a/defender/defender.go b/defender/defender.go index d168470..7eaa4e7 100644 --- a/defender/defender.go +++ b/defender/defender.go @@ -1,28 +1,26 @@ package defender import ( + "context" "fmt" "hummingbird/node" + "strings" "time" "log/slog" - "os" - "os/signal" "sync" - "syscall" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "hummingbird/node/contracts" challengeContract "hummingbird/node/contracts/Challenge.sol" ) type Opts struct { Logger *slog.Logger WorkerDelay time.Duration - DryRun bool // DryRun indicates whether or not to actually submit the block to the L1 rollup contract. } type Defender struct { @@ -34,18 +32,58 @@ func NewDefender(node *node.Node, opts *Opts) *Defender { return &Defender{Node: node, Opts: opts} } +// Start starts the defender. +// +// It will: +// 1. Start a goroutine to Scan Challenge.sol historic events, find any pending +// DA challenges that were missed and defend them. +// 2. In main thread, watch Challenge.sol for new DA challenges and defend them. func (d *Defender) Start() error { - d.retryMissedDAChallenges() - go d.retryActiveDAChallengesWorker() + errChan := make(chan error, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - if err := d.WatchAndDefendDAChallenges(); err != nil { - return fmt.Errorf("error watching and defending DA challenges: %w", err) + go func() { + errChan <- d.startHistoricWorker(ctx) + }() + + go func() { + errChan <- d.watchAndDefendDAChallenges(ctx) + }() + + select { + case err := <-errChan: + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() } + return nil } -func (d *Defender) WatchAndDefendDAChallenges() error { +func (d *Defender) startHistoricWorker(ctx context.Context) error { + ticker := time.NewTicker(d.Opts.WorkerDelay) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := d.scanAndDefendHistoricChallenges(); err != nil { + return fmt.Errorf("error retrying historic DA challenges: %w", err) + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// Watches the Challenge.sol contract for new DA challenges and defends them. +func (d *Defender) watchAndDefendDAChallenges(ctx context.Context) error { challenges := make(chan *challengeContract.ChallengeChallengeDAUpdate) + errChan := make(chan error, 1) + subscription, err := d.Ethereum.WatchChallengesDA(challenges) if err != nil { return fmt.Errorf("error starting WatchChallengesDA: %w", err) @@ -54,12 +92,8 @@ func (d *Defender) WatchAndDefendDAChallenges() error { d.Opts.Logger.Info("Defender is watching for DA challenges") - // Listen for shutdown signals - shutdown := make(chan os.Signal, 1) - signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) - go func() { - <-shutdown + <-ctx.Done() close(challenges) }() @@ -70,149 +104,116 @@ func (d *Defender) WatchAndDefendDAChallenges() error { go func(challenge *challengeContract.ChallengeChallengeDAUpdate) { defer wg.Done() - header, err := d.Ethereum.GetRollupHeaderByHash(challenge.BlockHash) - if err != nil { - d.Opts.Logger.Error("error getting rollup header by hash:", "error", err) - } - err = d.Store.StoreLastScannedBlockNumber(header.Epoch) - if err != nil { - d.Opts.Logger.Error("error storing last scanned block number:", "error", err) - } - - err = d.handleDAChallenge(challenge) - if err != nil { - d.Opts.Logger.Error("error handling challenge:", "challenge", challenge, "error", err) - err := d.Store.StoreActiveDAChallenge(challenge) - if err != nil { - d.Opts.Logger.Error("error storing active DA challenge:", "challenge", challenge, "error", err) - } + if err := d.handleDAChallenge(challenge); err != nil { + errChan <- err + return } }(challenge) } - // Wait for all challenges to be handled before returning - wg.Wait() + go func() { + wg.Wait() + close(errChan) + }() + + for err := range errChan { + if err != nil { + return err + } + } return nil } +// Handles a DA challenge by attempting to defend it. +// +// If the challenged data root is not yet available, it will be ignored +// and retried later by the scanAndDefendHistoricChallenges() worker function. func (d *Defender) handleDAChallenge(challenge *challengeContract.ChallengeChallengeDAUpdate) error { - blockHash := common.BytesToHash(challenge.BlockHash[:]) - - d.Opts.Logger.Info("DA challenge received", "block", blockHash.Hex(), "block_index", challenge.BlockIndex, "expiry", challenge.Expiry, "status", challenge.Status) - + // we are only interested in challenges that have been initiated by a challenger, ready to be defended if challenge.Status != 1 { return nil } - celestiaTx, err := d.GetDAPointer(challenge.BlockHash) - if err != nil { - return fmt.Errorf("error getting CelestiaTx: %w", err) - } - - if celestiaTx == nil { - d.Opts.Logger.Info("No CelestiaTx found", "block:", blockHash.Hex()) - return nil - } - - d.Opts.Logger.Info("Found CelestiaTx", "tx_hash", celestiaTx.TxHash.Hex(), "block_hash", blockHash.Hex()) - + blockHash := common.BytesToHash(challenge.BlockHash[:]) + statusString := contracts.StatusString(challenge.Status) + + log := d.Opts.Logger.With( + "blockHash", blockHash.Hex(), + "blockIndex", challenge.BlockIndex, + "expiry", time.Unix(challenge.Expiry.Int64(), 0).Format(time.RFC1123Z), + "statusEnum", challenge.Status, + "statusString", statusString, + ) + log.Info("Pending DA challenge log event found") + + // attempt to defend the challenge by submitting a tx to the Challenge contract tx, err := d.DefendDA(challenge.BlockHash) if err != nil { - return fmt.Errorf("error defending DA: %w", err) + if strings.Contains(err.Error(), "no data commitment has been generated for the provided height") { + log.Info("Pending DA challenge is awaiting data commitment from Celestia validators, will retry later") + return nil + } else { + return fmt.Errorf("error defending DA challenge: %w", err) + } } - d.Opts.Logger.Info("DA challenge defended", "tx", tx.Hash().Hex(), "block", blockHash.Hex(), "block_index", challenge.BlockIndex, "expiry", challenge.Expiry, "status", challenge.Status) + log.Info("Pending DA challenge defended successfully", "tx", tx.Hash().Hex()) return nil } +// Attempts to defend a DA challenge for the given block hash. +// +// Queries Celestia for a proof of data availability and submits a tx to the Challenge contract. func (d *Defender) DefendDA(block common.Hash) (*types.Transaction, error) { - proof, err := d.ProveDA(block) + proof, err := d.GetDAProof(block) if err != nil { return nil, fmt.Errorf("failed to prove data availability: %w", err) } - - d.Opts.Logger.Debug("Submitting data availability proof to L1 rollup contract", "block", block.Hex(), "dataroot", hexutil.Encode(proof.Tuple.DataRoot[:])) return d.Ethereum.DefendDataRootInclusion(block, proof) } -func (d *Defender) ProveDA(block common.Hash) (*node.CelestiaProof, error) { +// Gets the Celestia pointer for the given block hash and queries Celestia for a proof +// of data availability. +func (d *Defender) GetDAProof(block common.Hash) (*node.CelestiaProof, error) { pointer, err := d.GetDAPointer(block) if err != nil { return nil, fmt.Errorf("failed to get Celestia pointer: %w", err) } - if pointer == nil { return nil, fmt.Errorf("no Celestia pointer found") } - return d.Celestia.GetProof(pointer) } -func (d *Defender) retryActiveDAChallengesWorker() { - ticker := time.NewTicker(d.Opts.WorkerDelay) - defer ticker.Stop() - - for range ticker.C { - d.Opts.Logger.Info("Retrying active DA challenges...") - challenges, err := d.Store.GetActiveDAChallenges() - if err != nil { - d.Opts.Logger.Error("error getting active DA challenges from store:", "error", err) - continue - } - for _, challenge := range challenges { - block := common.BytesToHash(challenge.BlockHash[:]) - - // Check if challenge has expired, if so delete from active challenges and continue - if challenge.Expiry.Int64() <= time.Now().Unix() { - d.Opts.Logger.Info("Active DA challenge has expired, deleting from active challenges", "challengeBlock", block, "expiry", challenge.Expiry) - err = d.Store.DeleteActiveDAChallenge(challenge.BlockHash) - if err != nil { - d.Opts.Logger.Error("error deleting active DA challenge:", "challengeBlock", block, "error", err) - } - continue - } +// Scans the Challenge.sol contract for historic DA challenges and attempts to defend them. +// +// This function runs every opts.WorkerDelay and will scan all historic Challenge.sol challenge +// logs. This ensure we don't miss any challenges that were initiated when offline. It also +// allows defenders to retry challenges that failed to be defended i.e. due to data commitments +// not being available yet. +func (d *Defender) scanAndDefendHistoricChallenges() error { + d.Opts.Logger.Debug("Starting log scan for historic pending DA challenges") - err = d.handleDAChallenge(challenge) - if err != nil { - d.Opts.Logger.Error("error retrying active DA challenge:", "challengeBlock", block, "error", err) - continue - } - - err = d.Store.DeleteActiveDAChallenge(challenge.BlockHash) - if err != nil { - d.Opts.Logger.Error("error deleting active DA challenge:", "challengeBlock", block, "error", err) - } - } - d.Opts.Logger.Info("Active DA challenges retry worker finished") - } -} - -func (d *Defender) retryMissedDAChallenges() { - d.Opts.Logger.Info("Retrying missed DA challenges...") - lastScannedBlockNumber, err := d.Store.GetLastScannedBlockNumber() + h, err := d.Ethereum.GetRollupHeader(uint64(1)) if err != nil { - d.Opts.Logger.Error("error getting last scanned block number:", "error", err) - return + return fmt.Errorf("error getting rollup header: %w", err) } opts := &bind.FilterOpts{ - Start: lastScannedBlockNumber, + Start: h.Epoch, } - status := []uint8{1} - - challenges, err := d.Ethereum.FilterChallengeDAUpdate(opts, nil, nil, status) + challenges, err := d.Ethereum.FilterChallengeDAUpdate(opts, nil, nil, []uint8{1}) if err != nil { - d.Opts.Logger.Error("error filtering challenges:", "error", err) - return + return fmt.Errorf("error filtering challenges: %w", err) } - // iterate through challenges and retry + // iterate through historic challenges events for challenges.Next() { - challenge := challenges.Event // Access the current challenge + challenge := challenges.Event - // Check if challenge has already been defended + // check if challenge has already been defended by checking the current status challengeInfo, err := d.Ethereum.GetDataRootInclusionChallenge(challenge.BlockHash) if err != nil { d.Opts.Logger.Error("error getting data root inclusion challenge:", "error", err) @@ -220,15 +221,16 @@ func (d *Defender) retryMissedDAChallenges() { } if challengeInfo.Status != 1 { - d.Opts.Logger.Info("DA challenge has already been defended", "blockIndex", challenge.BlockIndex) continue } err = d.handleDAChallenge(challenge) if err != nil { - d.Opts.Logger.Error("error retrying missed DA challenge:", "blockIndex", challenge.BlockIndex, "error", err) continue } + } - d.Opts.Logger.Info("Missed DA challenges retry finished") + d.Opts.Logger.Debug("Finished log scan for historic pending DA challenges") + + return nil } diff --git a/node/store.go b/node/store.go index ba7c69a..c14fb3d 100644 --- a/node/store.go +++ b/node/store.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" - challengeContract "hummingbird/node/contracts/Challenge.sol" - "github.com/ethereum/go-ethereum/common" "github.com/syndtr/goleveldb/leveldb" ) @@ -16,11 +14,6 @@ type KVStore interface { Put(key, value []byte) error Delete(key []byte) error GetDAPointer(hash common.Hash) (*CelestiaPointer, error) - StoreActiveDAChallenge(c *challengeContract.ChallengeChallengeDAUpdate) error - GetActiveDAChallenges() ([]*challengeContract.ChallengeChallengeDAUpdate, error) - DeleteActiveDAChallenge(blockHash common.Hash) error - StoreLastScannedBlockNumber(blockNumber uint64) error - GetLastScannedBlockNumber() (uint64, error) } type LDBStore struct { @@ -67,99 +60,3 @@ func (l *LDBStore) GetDAPointer(hash common.Hash) (*CelestiaPointer, error) { return pointer, nil } - -func (l *LDBStore) StoreActiveDAChallenge(c *challengeContract.ChallengeChallengeDAUpdate) error { - if l.db == nil { - return errors.New("no store") - } - - key := append([]byte("da_challenge_"), c.BlockHash[:]...) - buf, err := json.Marshal(c) - if err != nil { - return fmt.Errorf("failed to marshal challenge: %w", err) - } - - err = l.Put(key, buf) - if err != nil { - return fmt.Errorf("failed to store challenge: %w", err) - } - - return nil -} - -func (l *LDBStore) GetActiveDAChallenges() ([]*challengeContract.ChallengeChallengeDAUpdate, error) { - if l.db == nil { - return nil, errors.New("no store") - } - - iter := l.db.NewIterator(nil, nil) - defer iter.Release() - - challenges := []*challengeContract.ChallengeChallengeDAUpdate{} - for iter.Next() { - if string(iter.Key()[:12]) == "da_challenge" { - challenge := &challengeContract.ChallengeChallengeDAUpdate{} - err := json.Unmarshal(iter.Value(), challenge) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal challenge: %w", err) - } - - if challenge.Status == 1 { - challenges = append(challenges, challenge) - } - } - } - - return challenges, nil -} - -func (l *LDBStore) DeleteActiveDAChallenge(blockHash common.Hash) error { - if l.db == nil { - return errors.New("no store") - } - - key := append([]byte("da_challenge_"), blockHash[:]...) - err := l.Delete(key) - if err != nil { - return fmt.Errorf("failed to delete challenge: %w", err) - } - - return nil -} - -func (l *LDBStore) StoreLastScannedBlockNumber(blockNumber uint64) error { - if l.db == nil { - return errors.New("no store") - } - - buf, err := json.Marshal(blockNumber) - if err != nil { - return fmt.Errorf("failed to marshal block number: %w", err) - } - - err = l.Put([]byte("last_scanned_block_number"), buf) - if err != nil { - return fmt.Errorf("failed to store block number: %w", err) - } - - return nil -} - -func (l *LDBStore) GetLastScannedBlockNumber() (uint64, error) { - if l.db == nil { - return 0, errors.New("no store") - } - - buf, err := l.Get([]byte("last_scanned_block_number")) - if err != nil { - return 0, fmt.Errorf("failed to get block number from store: %w", err) - } - - var blockNumber uint64 - err = json.Unmarshal(buf, &blockNumber) - if err != nil { - return 0, fmt.Errorf("failed to unmarshal block number: %w", err) - } - - return blockNumber, nil -} From 66430a9fa5c7999fc6a9832fc33b683b91d0cf38 Mon Sep 17 00:00:00 2001 From: Sledro Date: Sun, 28 Jan 2024 14:21:17 +0000 Subject: [PATCH 5/8] add install + config details --- README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 69519a9..83cdd85 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,81 @@ ![LightLink Hummingbird preview screenshot]() -Hummingbird is a command line tool for interacting with the LightLink protocol. +Hummingbird is a light client for interacting with the [LightLink Protocol](https://lightlink.io). It is designed to work in unison with the [lightlink-hummingbird-contracts](https://github.com/pellartech/lightlink-hummingbird-contracts) repository. -## Commands +## Installation + +### Prerequisites + +- [Golang](https://go.dev/dl/) (v1.21.5 or higher) installed. Go version can be checked with `$ go version` + +### Option 1: Install from source (Linux / MacOS) + +```bash +git clone git@github.com:pellartech/lightlink-hummingbird.git +cd lightlink-hummingbird +git checkout -b v0.0.1 +make +``` + +### Option 2: Install from binary (Windows / Linux / MacOS) + +Download the latest release from [here]() + +### Configuration + +Hummingbird requires a configuration file to run. A sample configuration file is provided in the repository [here](config.example.json). Copy this file and rename it to `config.json`. Then fill in the required fields. + +```bash +cp config.example.json config.json +``` + +**Note**: configuration file `config.json` path can be specified with the `--config-path` flag. If not specified, the default path is `./config.json` + +```json +{ + "storePath": "./storage", // Path to the local storage + "celestia": { + "token": "abcd", // Celestia token + "endpoint": "", // Celestia rpc endpoint + "namespace": "", // Celestia namespace to identify the blobs + "tendermint_rpc": "", // Tendermint rpc endpoint + "grpc": "", // Celestia grpc endpoint + "gasPrice": 0.01 // Gas price to use when submitting new headers to Celestia + }, + "ethereum": { + "httpEndpoint": "", // Ethereum http rpc endpoint + "wsEndpoint": "", // Ethereum websocket rpc endpoint + "canonicalStateChain": "", // Canonical state chain contract address + "daOracle": "", // Data availability oracle contract address + "challenge": "", // Challenge contract address + "gasPriceIncreasePercent": 100 // Increase gas price manually when submitting new headers to L1 + }, + "lightlink": { + "endpoint": "", // LightLink rpc endpoint + "delay": 100 // Delay in ms between each block query + }, + "rollup": { + "bundleSize": 200, // Number of headers to include in each bundle + "l1pollDelay": 90000, // (90sec) Delay in ms between each L1 block query + "l2pollDelay": 30000, // (30sec) Delay in ms between each L2 block query + "storeCelestiaPointers": true, // Store celestia pointers in the local storage + "storeHeaders": true // Store headers in the local storage + }, + "defender": { + "workerDelay": 60000 // (60sec) Delay in ms between scanning for historical challenges to defend + } +} +``` + +## Usage ```bash hb rollup info # Get the current rollup state -hb rollup next # Generate the next rollup block -hb rollup start # Start the rollup loop to generate and submit bundles +hb rollup next # [Publisher Only] Generate the next rollup block +hb rollup start # [Publisher Only] Start the rollup loop to generate and submit bundles hb challenge challenge-da # Challenge data availability hb defender defend-da # Defend data availability hd defender info-da # Provides info on an existing challenge From f3b7ab4a55419515c8eae8c9b5cdfeca4c7856f5 Mon Sep 17 00:00:00 2001 From: Daniel Hayden Date: Sun, 28 Jan 2024 15:15:34 +0000 Subject: [PATCH 6/8] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83cdd85..05e41fa 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ cp config.example.json config.json **Note**: configuration file `config.json` path can be specified with the `--config-path` flag. If not specified, the default path is `./config.json` -```json +``` { "storePath": "./storage", // Path to the local storage "celestia": { @@ -99,4 +99,4 @@ see `hb --help` for more information

HummingBird -

\ No newline at end of file +

From 49284b5bc7b00d1cc9d12204bdd0aa9b20267c9e Mon Sep 17 00:00:00 2001 From: Daniel Hayden Date: Sun, 28 Jan 2024 15:16:45 +0000 Subject: [PATCH 7/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05e41fa..b511593 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ make ### Option 2: Install from binary (Windows / Linux / MacOS) -Download the latest release from [here]() +Download the latest release from [here](https://github.com/pellartech/lightlink-hummingbird/releases) ### Configuration From d2bf85fa5377c32856ff7e6e70a646db9f29afec Mon Sep 17 00:00:00 2001 From: Sledro Date: Wed, 31 Jan 2024 23:28:57 +0000 Subject: [PATCH 8/8] update readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b511593..0347dd1 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,14 @@ Hummingbird requires a configuration file to run. A sample configuration file is cp config.example.json config.json ``` +A single environment variable ETH_KEY is required to be set. This is the private key of the Ethereum account that will be used to sign transactions. This account must have sufficient funds to pay for gas fees. + +```bash +export ETH_KEY=0x... +``` + + + **Note**: configuration file `config.json` path can be specified with the `--config-path` flag. If not specified, the default path is `./config.json` ``` @@ -79,7 +87,7 @@ cp config.example.json config.json hb rollup info # Get the current rollup state hb rollup next # [Publisher Only] Generate the next rollup block hb rollup start # [Publisher Only] Start the rollup loop to generate and submit bundles -hb challenge challenge-da # Challenge data availability +hb challenger challenge-da # Challenge data availability hb defender defend-da # Defend data availability hd defender info-da # Provides info on an existing challenge hb defender prove-da # Prove data availability