diff --git a/.gitignore b/.gitignore index 89a1fb5..56652a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ stress/pk.hex *.envrc +pk.hex + # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # diff --git a/continuous/README.md b/continuous/README.md index 63a4266..d4d2cb9 100644 --- a/continuous/README.md +++ b/continuous/README.md @@ -33,6 +33,8 @@ export CONTINUOUS_DB_NAME=shutter_metrics export CONTINUOUS_PK_FILE=/home/konrad/Projects/nethermind-tests/pk.hex # where to store analysis files export CONTINUOUS_BLAME_FOLDER="/tmp/blame" +# (optional) particular validator indices to monitor (comma-separated list) +export CONTINUOUS_VALIDATOR_INDICES= ``` Make sure, there is an [observer](https://github.com/shutter-network/observer) running and its database accessible as defined in the environment above. diff --git a/continuous/config.go b/continuous/config.go index 4497eca..ac59cef 100644 --- a/continuous/config.go +++ b/continuous/config.go @@ -6,6 +6,8 @@ import ( "log" "math/big" "os" + "strconv" + "strings" "sync" "github.com/ethereum/go-ethereum/core/types" @@ -15,18 +17,19 @@ import ( ) type Configuration struct { - accounts []utils.Account - submitAccount utils.Account - client *ethclient.Client - status Status - contracts utils.Contracts - chainID *big.Int - DbUser string - DbPass string - DbAddr string - DbName string - PkFile string - blameFolder string + accounts []utils.Account + submitAccount utils.Account + client *ethclient.Client + status Status + contracts utils.Contracts + chainID *big.Int + DbUser string + DbPass string + DbAddr string + DbName string + PkFile string + blameFolder string + validatorIndices []int64 Connection } @@ -158,5 +161,43 @@ func createConfiguration() (Configuration, error) { blameFolder = tmp } cfg.blameFolder = blameFolder + + // Read validator indices from environment (optional) + validatorIndicesStr := os.Getenv("CONTINUOUS_VALIDATOR_INDICES") + if validatorIndicesStr != "" { + indices, err := parseValidatorIndices(validatorIndicesStr) + if err != nil { + return cfg, fmt.Errorf("could not parse validator indices: %v", err) + } + cfg.validatorIndices = indices + log.Printf("Filtering by validator indices: %v\n", cfg.validatorIndices) + } else { + log.Println("No validator indices specified, running for all shutterized validators") + } + return cfg, nil } + +// parseValidatorIndices parses a comma-separated or space-separated string of validator indices +func parseValidatorIndices(indicesStr string) ([]int64, error) { + // Try comma-separated first, then space-separated + separator := "," + if !strings.Contains(indicesStr, ",") { + separator = " " + } + + parts := strings.Split(indicesStr, separator) + var indices []int64 + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + index, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid validator index '%s': %v", part, err) + } + indices = append(indices, index) + } + return indices, nil +} diff --git a/continuous/continuous.go b/continuous/continuous.go index 5ff8b30..45b7a2c 100644 --- a/continuous/continuous.go +++ b/continuous/continuous.go @@ -15,10 +15,11 @@ const NumFundedAccounts = 6 const MinimalFunding = int64(500000000000000000) // 0.5 ETH in wei type Status struct { - statusModMutex *sync.Mutex - lastShutterTS pgtype.Date - txInFlight []*ShutterTx - txDone []*ShutterTx + statusModMutex *sync.Mutex + lastShutterTS pgtype.Date + txInFlight []*ShutterTx + txDone []*ShutterTx + currentTargetedShutterSlot int64 } func (s Status) TxCount() int { @@ -71,19 +72,84 @@ func QueryAllShutterBlocks(out chan<- ShutterBlock, cfg *Configuration) { for { time.Sleep(waitBetweenQueries) fmt.Printf(".") - newShutterBlock := queryNewestShutterBlock(status.lastShutterTS, cfg) + newShutterBlock, currentTargetedShutterSlot := queryNewestShutterBlock(status.lastShutterTS, status.currentTargetedShutterSlot, cfg) if !newShutterBlock.Ts.Time.IsZero() { status.lastShutterTS = newShutterBlock.Ts + status.currentTargetedShutterSlot = currentTargetedShutterSlot // send event (block number, timestamp) to out channel out <- newShutterBlock } } } -func queryNewestShutterBlock(lastBlockTS pgtype.Date, cfg *Configuration) ShutterBlock { +func queryNewestShutterBlock(lastBlockTS pgtype.Date, currentTargetedShutterSlot int64, cfg *Configuration) (ShutterBlock, int64) { connection := GetConnection(cfg) block := int64(0) var ts pgtype.Date + + if len(cfg.validatorIndices) > 0 { + query := ` + WITH current_block AS ( + SELECT + block_number, + slot AS current_slot, + to_timestamp(block_timestamp) AS ts + FROM block + ORDER BY slot DESC + LIMIT 1 + ), + next_shutter AS ( + SELECT + pd.validator_index, + pd.slot AS next_slot + FROM proposer_duties pd + JOIN validator_status vs + ON vs.validator_index = pd.validator_index + WHERE vs.status = 'active_ongoing' + AND pd.slot > (SELECT current_slot FROM current_block) + ORDER BY pd.slot ASC + LIMIT 1 + ) + SELECT + ns.next_slot, + ns.validator_index, + cb.block_number, + cb.ts + FROM next_shutter ns + JOIN current_block cb ON TRUE + WHERE ns.validator_index = ANY($1); + ` + + var validatorIndex int64 + var nextSlot int64 + + row := connection.db.QueryRow( + context.Background(), + query, + cfg.validatorIndices, + ) + + err := row.Scan(&nextSlot, &validatorIndex, &block, &ts) + if err != nil { + return ShutterBlock{}, 0 + } + + // Skip if we have already targeted this slot + if nextSlot == currentTargetedShutterSlot { + return ShutterBlock{}, 0 + } + + log.Printf( + "FILTERED FUTURE SHUTTER MATCH: nextSlot=%d next_validator=%d current_block=%d ts=%v", + nextSlot, validatorIndex, block, ts.Time, + ) + + return ShutterBlock{ + Number: block, + Ts: ts, + }, nextSlot + } + query := ` SELECT b.block_number, @@ -97,6 +163,7 @@ func queryNewestShutterBlock(lastBlockTS pgtype.Date, cfg *Configuration) Shutte AND b.slot = p.slot AND b.block_timestamp > $1; ` + rows, err := connection.db.Query(context.Background(), query, lastBlockTS.Time.Unix()) if err != nil { panic(err) @@ -117,7 +184,7 @@ func queryNewestShutterBlock(lastBlockTS pgtype.Date, cfg *Configuration) Shutte res := ShutterBlock{} res.Number = block res.Ts = ts - return res + return res, 0 } func CheckTxInFlight(blockNumber int64, cfg *Configuration) { diff --git a/go.mod b/go.mod index 90cf5f8..163d5cb 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/supranational/blst v0.3.12 // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect diff --git a/go.sum b/go.sum index 0ccd908..2b4a9e2 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +256,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.12 h1:Vfas2U2CFHhniv2QkUm2OVa1+pGTdqtpqm9NnhUUbZ8= github.com/supranational/blst v0.3.12/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=