├── .deepsource.toml ├── .dockerignore ├── .github └── workflows │ ├── golangci-lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── chaind.config.docker-compose.yml ├── clients.go ├── docker-compose.yml ├── docs ├── prometheus.md └── tables.md ├── go.mod ├── go.sum ├── handlers └── finality.go ├── logging.go ├── main.go ├── metrics.go ├── services ├── beaconcommittees │ └── standard │ │ ├── handler.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ └── service.go ├── blocks │ ├── mock │ │ └── service.go │ ├── service.go │ └── standard │ │ ├── handler.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ └── service.go ├── chaindb │ ├── filters.go │ ├── mock │ │ └── service.go │ ├── postgresql │ │ ├── aggregatevalidatorbalances.go │ │ ├── attestations.go │ │ ├── attesterslashing.go │ │ ├── beaconcommittees.go │ │ ├── beaconcommittees_test.go │ │ ├── blobsidecars.go │ │ ├── blocks.go │ │ ├── blocks_test.go │ │ ├── blocksummaries.go │ │ ├── blstoexecutionchanges.go │ │ ├── chainspec.go │ │ ├── chainspec_test.go │ │ ├── consolidationrequests.go │ │ ├── depositrequests.go │ │ ├── deposits.go │ │ ├── deposits_test.go │ │ ├── epochsummaries.go │ │ ├── eth1deposits.go │ │ ├── eth1deposits_test.go │ │ ├── executionpayload.go │ │ ├── forkschedule.go │ │ ├── forkschedule_test.go │ │ ├── genesis.go │ │ ├── genesis_test.go │ │ ├── main_test.go │ │ ├── metadata.go │ │ ├── parameters.go │ │ ├── partitioning.go │ │ ├── proposerduties.go │ │ ├── proposerslashings.go │ │ ├── proposerslashings_test.go │ │ ├── service.go │ │ ├── service_test.go │ │ ├── setblobsidecar.go │ │ ├── setblobsidecars.go │ │ ├── setconsolidationrequest.go │ │ ├── setconsolidationrequests.go │ │ ├── setdepositrequest.go │ │ ├── setdepositrequests.go │ │ ├── setwithdrawalrequest.go │ │ ├── setwithdrawalrequests.go │ │ ├── spec.go │ │ ├── spec_test.go │ │ ├── syncaggregate.go │ │ ├── synccommittees.go │ │ ├── tx.go │ │ ├── upgrader.go │ │ ├── validatordaysummaries.go │ │ ├── validatorepochsummaries.go │ │ ├── validators.go │ │ ├── validators_test.go │ │ ├── voluntaryexits.go │ │ └── withdrawals.go │ ├── service.go │ └── types.go ├── chaintime │ ├── mock │ │ └── service.go │ ├── service.go │ └── standard │ │ ├── parameters.go │ │ ├── service.go │ │ └── service_test.go ├── eth1deposits │ └── getlogs │ │ ├── blocknumber.go │ │ ├── blocktimestampbyhash.go │ │ ├── chainid.go │ │ ├── getlogs.go │ │ ├── handler.go │ │ ├── http.go │ │ ├── logresponse.go │ │ ├── logresponse_internal_test.go │ │ ├── main_test.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ ├── service.go │ │ ├── service_test.go │ │ ├── transaction.go │ │ ├── transactionbyhash.go │ │ ├── transactionreceipt.go │ │ └── transactionreceiptbyhash.go ├── finalizer │ └── standard │ │ ├── handler.go │ │ ├── handler_internal_test.go │ │ ├── main_test.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ ├── service.go │ │ └── service_test.go ├── metrics │ ├── null │ │ └── service.go │ ├── prometheus │ │ ├── parameters.go │ │ └── service.go │ └── service.go ├── proposerduties │ └── standard │ │ ├── handler.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ └── service.go ├── scheduler │ ├── service.go │ └── standard │ │ ├── metrics.go │ │ ├── parameters.go │ │ ├── service.go │ │ └── service_test.go ├── spec │ └── standard │ │ ├── parameters.go │ │ └── service.go ├── summarizer │ ├── service.go │ └── standard │ │ ├── block.go │ │ ├── epoch.go │ │ ├── handler.go │ │ ├── main_test.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ ├── prune.go │ │ ├── service.go │ │ ├── service_test.go │ │ ├── validatorday.go │ │ └── validatorepoch.go ├── synccommittees │ └── standard │ │ ├── handler.go │ │ ├── metadata.go │ │ ├── metrics.go │ │ ├── parameters.go │ │ └── service.go └── validators │ └── standard │ ├── handler.go │ ├── metadata.go │ ├── metrics.go │ ├── parameters.go │ └── service.go ├── tracing.go └── util ├── calendarduration.go ├── calendarduration_test.go ├── commit.go ├── logging.go ├── majordomo.go └── path.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_paths = ["github.com/wealdtech/chaind"] -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | coverage.html 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | 17 | # Vim 18 | *.sw? 19 | 20 | # Local TODO 21 | TODO.md 22 | 23 | Dockerfile 24 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: 'golangci-lint' 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | pull_request: 7 | 8 | permissions: 9 | contents: 'read' 10 | pull-requests: 'read' 11 | checks: 'write' 12 | 13 | jobs: 14 | golangci: 15 | name: 'lint' 16 | runs-on: 'ubuntu-22.04' 17 | steps: 18 | - uses: 'actions/setup-go@v5' 19 | with: 20 | cache: false 21 | go-version: '1.22' 22 | - uses: 'actions/checkout@v4' 23 | - name: 'golangci-lint' 24 | uses: 'golangci/golangci-lint-action@v6' 25 | with: 26 | version: 'latest' 27 | args: '--timeout=60m' 28 | only-new-issues: true 29 | skip-cache: true 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/setup-go@v5 12 | with: 13 | cache: false 14 | go-version: '1.22' 15 | - uses: actions/checkout@v4 16 | - uses: n8maninger/action-golang-test@v2 17 | with: 18 | args: "-race;-timeout=30m" 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | chaind 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | coverage.html 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | # Vim 19 | *.sw? 20 | 21 | # Local TODO 22 | TODO.md 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-bookworm as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | RUN go build 12 | 13 | FROM debian:bookworm-slim 14 | 15 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/* 16 | 17 | WORKDIR /app 18 | 19 | COPY --from=builder /app/chaind /app 20 | 21 | ENTRYPOINT ["/app/chaind"] 22 | -------------------------------------------------------------------------------- /chaind.config.docker-compose.yml: -------------------------------------------------------------------------------- 1 | # log-level is the base log level of the process. 2 | # 'info' should be a suitable log level, unless detailed information is 3 | # required in which case 'debug' or 'trace' can be used. 4 | log-level: debug 5 | # log-file specifies that log output should go to a file. If this is not 6 | # present log output will be to stderr. 7 | # log-file: /var/log/chaind.log 8 | chaindb: 9 | # url is the URL of the PostgreSQL database. 10 | # url: postgres://chain:secret@localhost:5432 11 | max-connections: 16 12 | # eth2client contains configuration for the Ethereum 2 client. 13 | eth2client: 14 | # log-level is the log level of the specific module. If not present the base log 15 | # level will be used. 16 | log-level: debug 17 | # address is the address of the beacon node. 18 | # address: localhost:5051 19 | # eth1client contains configuration for the Ethereum 1 client. 20 | eth1client: 21 | # address is the address of the Ethereum 1 node. 22 | # address: localhost:8545 23 | # blocks contains configuration for obtaining block-related information. 24 | blocks: 25 | # enable states if this module will be operational. 26 | enable: true 27 | # address is a separate connection for this module. If not present then 28 | # chaind will use the eth2client connection. 29 | # address: localhost:5051 30 | # start-slot is the slot from which to start. chaind should keep track of this itself, 31 | # however if you wish to start from a later slot this can be set. 32 | # start-slot: 2000 33 | # refetch will refetch block data from a beacon node even if it has already has a block 34 | # in its database. 35 | # refetch: false 36 | # validators contains configuration for obtaining validator-related information. 37 | validators: 38 | enable: true 39 | # balances contains configuration for obtaining validator balances. This is 40 | # a separate configuration flag for two reasons. First, it can take a long 41 | # time to retrieve this information. Second, the information can be 42 | # derived from the data obtained by the other modules. 43 | balances: 44 | enable: false 45 | # beacon-committees contains configuration for obtaining beacon committee-related 46 | # information. 47 | beacon-committees: 48 | enable: true 49 | # proposer-duties contains configuration for obtaining proposer duty-related 50 | # information. 51 | proposer-duties: 52 | enable: true 53 | # finalizer updates tables with information available for finalized states. 54 | finalizer: 55 | enable: true 56 | # eth1deposits contains information about transactions made to the deposit contract 57 | # on the Ethereum 1 network. 58 | eth1deposits: 59 | enable: false 60 | # start-block is the block from which to start fetching deposits. chaind should 61 | # keep track of this itself, however if you wish to start from a different block this 62 | # can be set. 63 | # start-block: 500 64 | -------------------------------------------------------------------------------- /clients.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "sync" 19 | 20 | eth2client "github.com/attestantio/go-eth2-client" 21 | "github.com/attestantio/go-eth2-client/http" 22 | "github.com/pkg/errors" 23 | "github.com/spf13/viper" 24 | "github.com/wealdtech/chaind/util" 25 | ) 26 | 27 | var ( 28 | clients map[string]eth2client.Service 29 | clientsMu sync.Mutex 30 | ) 31 | 32 | // fetchClient fetches a client service, instantiating it if required. 33 | func fetchClient(ctx context.Context, address string) (eth2client.Service, error) { 34 | clientsMu.Lock() 35 | defer clientsMu.Unlock() 36 | if clients == nil { 37 | clients = make(map[string]eth2client.Service) 38 | } 39 | 40 | var client eth2client.Service 41 | var exists bool 42 | if client, exists = clients[address]; !exists { 43 | var err error 44 | client, err = http.New(ctx, 45 | http.WithLogLevel(util.LogLevel("eth2client")), 46 | http.WithTimeout(viper.GetDuration("eth2client.timeout")), 47 | http.WithAddress(address)) 48 | if err != nil { 49 | return nil, errors.Wrap(err, "failed to initiate client") 50 | } 51 | // Confirm that the client provides the required interfaces. 52 | if err := confirmClientInterfaces(client); err != nil { 53 | return nil, errors.Wrap(err, "missing required interface") 54 | } 55 | clients[address] = client 56 | } 57 | 58 | return client, nil 59 | } 60 | 61 | func confirmClientInterfaces(client eth2client.Service) error { 62 | if _, isProvider := client.(eth2client.GenesisProvider); !isProvider { 63 | return errors.New("client is not a GenesisProvider") 64 | } 65 | if _, isProvider := client.(eth2client.SpecProvider); !isProvider { 66 | return errors.New("client is not a SpecProvider") 67 | } 68 | if _, isProvider := client.(eth2client.ForkScheduleProvider); !isProvider { 69 | return errors.New("client is not a ForkScheduleProvider") 70 | } 71 | if _, isProvider := client.(eth2client.NodeSyncingProvider); !isProvider { 72 | return errors.New("client is not a NodeSyncingProvider") 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | chaind: 5 | image: wealdtech/chaind:latest 6 | restart: unless-stopped 7 | networks: 8 | - chaind 9 | volumes: 10 | - ./chaind.config.docker-compose.yml:/app/chaind.yml:ro 11 | command: --base-dir /app 12 | environment: 13 | &environment 14 | CHAIND_CHAINDB_URL: postgres://chaind:chaind@db:5432 15 | CHAIND_ETH2CLIENT_ADDRESS: ${CHAIND_ETH2CLIENT_ADDRESS:-http://localhost:5051/} 16 | 17 | db: 18 | image: "postgres:14.2-bullseye" 19 | networks: 20 | - chaind 21 | restart: unless-stopped 22 | ports: 23 | - "127.0.0.1:5434:5432" 24 | environment: 25 | POSTGRES_USER: chaind 26 | POSTGRES_PASSWORD: chaind 27 | POSTGRES_DB: chaind 28 | volumes: 29 | - db_data:/var/lib/postgresql/data 30 | 31 | networks: 32 | chaind: 33 | driver: bridge 34 | 35 | volumes: 36 | db_data: 37 | -------------------------------------------------------------------------------- /docs/prometheus.md: -------------------------------------------------------------------------------- 1 | # Prometheus metrics 2 | chaind provides a number of metrics to check the health and performance of its activities. chaind's default implementation uses Prometheus to provide these metrics. The metrics server listens on the address provided by the `metrics.prometheus.listen-address` configuration value. 3 | 4 | ## Version 5 | The version of chaind can be found in the `chaind_release` metric, in the `version` label. 6 | 7 | ## Health 8 | Health metrics provide a mechanism to confirm if chaind is active. 9 | 10 | `chaind_start_time_secs` is the Unix timestamp at which chaind was started. This value will remain the same throughout a run of chaind; if it increments it implies that chaind has restarted. 11 | 12 | `chaind_ready` is `1` if chaind's services are all on-line and it is able to operate. If not, this will be `0`. 13 | 14 | ## Operations 15 | Operations metrics provide information about numbers of operations performed. These are generally lower-level information that can be useful to monitor activities for fine-tuning of server parameters, comparing one instance to another, _etc._ 16 | 17 | - `chaind_beaconcommittees_epochs_processed` number of epochs processed by the beacon committees module this run of chaind 18 | - `chaind_beaconcommittees_latest_epoch` latest epoch processed by the beacon committees module this run of chaind 19 | - `chaind_blocks_blocks_processed` number of blocks processed by the blocks module this run of chaind 20 | - `chaind_blocks_latest_block` latest block processed by the blocks module this run of chaind 21 | - `chaind_eth1deposits_blocks_processed` number of blocks processed by the Ethereum 1 deposits module this run of chaind 22 | - `chaind_eth1deposits_latest_block` latest block processed by the Ethereum 1 deposits module this run of chaind 23 | - `chaind_finalizer_epochs_processed` number of epochs processed by the finalizer module this run of chaind 24 | - `chaind_finalizer_latest_epoch` latest epoch processed by the finalizer module this run of chaind 25 | - `chaind_proposerduties_epochs_processed` number of epochs processed by the proposer duties module this run of chaind 26 | - `chaind_proposerduties_latest_epoch` latest epoch processed by the proposer duties module this run of chaind 27 | - `chaind_validators_epochs_processed` number of epochs processed by the validators module this run of chaind 28 | - `chaind_validators_latest_epoch` latest epoch processed by the validators module this run of chaind 29 | - `chaind_validators_balances_epochs_processed` number of epochs processed by the balances submodule of the validators module this run of chaind 30 | - `chaind_validators_balances_latest_epoch` latest epoch processed by the balances submodule of the validators module this run of chaind 31 | -------------------------------------------------------------------------------- /handlers/finality.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package handlers 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | ) 21 | 22 | // FinalityHandler provides interfaces for handling finality updates. 23 | type FinalityHandler interface { 24 | // OnFinalityUpdated is called when finality has been updated in the database. 25 | OnFinalityUpdated(ctx context.Context, epoch phase0.Epoch) 26 | } 27 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "os" 18 | 19 | "github.com/pkg/errors" 20 | "github.com/rs/zerolog" 21 | zerologger "github.com/rs/zerolog/log" 22 | "github.com/spf13/viper" 23 | "github.com/wealdtech/chaind/util" 24 | ) 25 | 26 | // log. 27 | var log zerolog.Logger 28 | 29 | // initLogging initialises logging. 30 | func initLogging() error { 31 | // We set the global logging level to trace, because if the global log level is higher than the 32 | // local log level the local level is ignored. It is then overridden for each module. 33 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 34 | 35 | // Change the output file. 36 | if viper.GetString("log-file") != "" { 37 | f, err := os.OpenFile(resolvePath(viper.GetString("log-file")), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) 38 | if err != nil { 39 | return errors.Wrap(err, "failed to open log file") 40 | } 41 | zerologger.Logger = zerologger.Logger.Output(f) 42 | } 43 | 44 | // Set the local logger from the global logger. 45 | log = zerologger.Logger.With().Logger().Level(util.LogLevel("")) 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/pkg/errors" 20 | "github.com/prometheus/client_golang/prometheus" 21 | "github.com/wealdtech/chaind/services/metrics" 22 | ) 23 | 24 | var metricsNamespace = "chaind" 25 | 26 | var ( 27 | releaseMetric *prometheus.GaugeVec 28 | readyMetric prometheus.Gauge 29 | ) 30 | 31 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 32 | if releaseMetric != nil { 33 | // Already registered. 34 | return nil 35 | } 36 | if monitor == nil { 37 | // No monitor. 38 | return nil 39 | } 40 | switch monitor.Presenter() { 41 | case "prometheus": 42 | return registerPrometheusMetrics() 43 | case "null": 44 | log.Debug().Msg("no metrics will be generated for this module") 45 | } 46 | return nil 47 | } 48 | 49 | func registerPrometheusMetrics() error { 50 | startTime := prometheus.NewGauge(prometheus.GaugeOpts{ 51 | Namespace: metricsNamespace, 52 | Name: "start_time_secs", 53 | Help: "The timestamp at which this instance started.", 54 | }) 55 | if err := prometheus.Register(startTime); err != nil { 56 | return errors.Wrap(err, "failed to regsiter start_time_secs") 57 | } 58 | startTime.SetToCurrentTime() 59 | 60 | releaseMetric = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 61 | Namespace: metricsNamespace, 62 | Name: "release", 63 | Help: "The release of this instance.", 64 | }, []string{"version"}) 65 | if err := prometheus.Register(releaseMetric); err != nil { 66 | return err 67 | } 68 | 69 | readyMetric = prometheus.NewGauge(prometheus.GaugeOpts{ 70 | Namespace: metricsNamespace, 71 | Name: "ready", 72 | Help: "1 if ready to serve requests, otherwise 0.", 73 | }) 74 | if err := prometheus.Register(readyMetric); err != nil { 75 | return errors.Wrap(err, "failed to regsiter ready") 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // SetRelease is called when the release version is established. 82 | func setRelease(_ context.Context, version string) { 83 | if releaseMetric == nil { 84 | return 85 | } 86 | 87 | releaseMetric.WithLabelValues(version).Set(1) 88 | } 89 | 90 | func setReady(_ context.Context, ready bool) { 91 | if readyMetric == nil { 92 | return 93 | } 94 | 95 | if ready { 96 | readyMetric.Set(1) 97 | } else { 98 | readyMetric.Set(0) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /services/beaconcommittees/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // metadata stored about this service. 24 | type metadata struct { 25 | LatestEpoch int64 `json:"latest_epoch"` 26 | } 27 | 28 | // metadataKey is the key for the metadata. 29 | var metadataKey = "beaconcommittees.standard" 30 | 31 | // getMetadata gets metadata for this service. 32 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 33 | md := &metadata{ 34 | LatestEpoch: -1, 35 | } 36 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "failed to fetch metadata") 39 | } 40 | if mdJSON == nil { 41 | return md, nil 42 | } 43 | if err := json.Unmarshal(mdJSON, md); err != nil { 44 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 45 | } 46 | return md, nil 47 | } 48 | 49 | // setMetadata sets metadata for this service. 50 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 51 | mdJSON, err := json.Marshal(md) 52 | if err != nil { 53 | return errors.Wrap(err, "failed to marshal metadata") 54 | } 55 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 56 | return errors.Wrap(err, "failed to update metadata") 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /services/beaconcommittees/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | var metricsNamespace = "chaind_beaconcommittees" 26 | 27 | var ( 28 | highestEpoch phase0.Epoch 29 | latestEpoch prometheus.Gauge 30 | epochsProcessed prometheus.Gauge 31 | ) 32 | 33 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 34 | if latestEpoch != nil { 35 | // Already registered. 36 | return nil 37 | } 38 | if monitor == nil { 39 | // No monitor. 40 | return nil 41 | } 42 | if monitor.Presenter() == "prometheus" { 43 | return registerPrometheusMetrics() 44 | } 45 | return nil 46 | } 47 | 48 | // skipcq: RVV-B0012 49 | func registerPrometheusMetrics() error { 50 | latestEpoch = prometheus.NewGauge(prometheus.GaugeOpts{ 51 | Namespace: metricsNamespace, 52 | Name: "latest_epoch", 53 | Help: "Latest epoch processed", 54 | }) 55 | if err := prometheus.Register(latestEpoch); err != nil { 56 | return errors.Wrap(err, "failed to register latest_epoch") 57 | } 58 | 59 | epochsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 60 | Namespace: metricsNamespace, 61 | Name: "epochs_processed", 62 | Help: "Number of epochs processed", 63 | }) 64 | if err := prometheus.Register(epochsProcessed); err != nil { 65 | return errors.Wrap(err, "failed to register epochs_processed") 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // monitorLatestEpoch sets the latest epoch without registering an 72 | // increase in blocks processed. This does not usually need to be 73 | // called directly, as it is called as part of monitorEpochProcessed. 74 | func monitorLatestEpoch(epoch phase0.Epoch) { 75 | highestEpoch = epoch 76 | if latestEpoch != nil { 77 | latestEpoch.Set(float64(epoch)) 78 | } 79 | } 80 | 81 | func monitorEpochProcessed(epoch phase0.Epoch) { 82 | if epochsProcessed != nil { 83 | epochsProcessed.Inc() 84 | if epoch > highestEpoch { 85 | monitorLatestEpoch(epoch) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /services/beaconcommittees/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "errors" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/chaintime" 23 | "github.com/wealdtech/chaind/services/metrics" 24 | ) 25 | 26 | type parameters struct { 27 | logLevel zerolog.Level 28 | monitor metrics.Service 29 | eth2Client eth2client.Service 30 | chainDB chaindb.Service 31 | chainTime chaintime.Service 32 | startEpoch int64 33 | } 34 | 35 | // Parameter is the interface for service parameters. 36 | type Parameter interface { 37 | apply(p *parameters) 38 | } 39 | 40 | type parameterFunc func(*parameters) 41 | 42 | func (f parameterFunc) apply(p *parameters) { 43 | f(p) 44 | } 45 | 46 | // WithLogLevel sets the log level for the module. 47 | func WithLogLevel(logLevel zerolog.Level) Parameter { 48 | return parameterFunc(func(p *parameters) { 49 | p.logLevel = logLevel 50 | }) 51 | } 52 | 53 | // WithMonitor sets the monitor for the module. 54 | func WithMonitor(monitor metrics.Service) Parameter { 55 | return parameterFunc(func(p *parameters) { 56 | p.monitor = monitor 57 | }) 58 | } 59 | 60 | // WithETH2Client sets the Ethereum 2 client for this module. 61 | func WithETH2Client(eth2Client eth2client.Service) Parameter { 62 | return parameterFunc(func(p *parameters) { 63 | p.eth2Client = eth2Client 64 | }) 65 | } 66 | 67 | // WithChainDB sets the chain database for this module. 68 | func WithChainDB(chainDB chaindb.Service) Parameter { 69 | return parameterFunc(func(p *parameters) { 70 | p.chainDB = chainDB 71 | }) 72 | } 73 | 74 | // WithChainTime sets the chain time service for this module. 75 | func WithChainTime(chainTime chaintime.Service) Parameter { 76 | return parameterFunc(func(p *parameters) { 77 | p.chainTime = chainTime 78 | }) 79 | } 80 | 81 | // WithStartEpoch sets the start epoch for this module. 82 | func WithStartEpoch(startEpoch int64) Parameter { 83 | return parameterFunc(func(p *parameters) { 84 | p.startEpoch = startEpoch 85 | }) 86 | } 87 | 88 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 89 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 90 | parameters := parameters{ 91 | logLevel: zerolog.GlobalLevel(), 92 | startEpoch: -1, 93 | } 94 | for _, p := range params { 95 | if params != nil { 96 | p.apply(¶meters) 97 | } 98 | } 99 | 100 | if parameters.eth2Client == nil { 101 | return nil, errors.New("no Ethereum 2 client specified") 102 | } 103 | // Ensure the eth2client can handle our requirements. 104 | if _, isProvider := parameters.eth2Client.(eth2client.BeaconCommitteesProvider); !isProvider { 105 | //nolint:stylecheck 106 | return nil, errors.New("Ethereum 2 client does not provide beacon committee information") // skipcq: SCC-ST1005 107 | } 108 | if _, isProvider := parameters.eth2Client.(eth2client.EventsProvider); !isProvider { 109 | //nolint:stylecheck 110 | return nil, errors.New("Ethereum 2 client does not provide events") // skipcq: SCC-ST1005 111 | } 112 | if parameters.chainDB == nil { 113 | return nil, errors.New("no chain database specified") 114 | } 115 | if parameters.chainTime == nil { 116 | return nil, errors.New("no chain time specified") 117 | } 118 | 119 | return ¶meters, nil 120 | } 121 | -------------------------------------------------------------------------------- /services/beaconcommittees/standard/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020, 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | api "github.com/attestantio/go-eth2-client/api/v1" 21 | "github.com/pkg/errors" 22 | "github.com/rs/zerolog" 23 | zerologger "github.com/rs/zerolog/log" 24 | "github.com/wealdtech/chaind/services/chaindb" 25 | "github.com/wealdtech/chaind/services/chaintime" 26 | "golang.org/x/sync/semaphore" 27 | ) 28 | 29 | // Service is a chain database service. 30 | type Service struct { 31 | eth2Client eth2client.Service 32 | chainDB chaindb.Service 33 | beaconCommitteesSetter chaindb.BeaconCommitteesSetter 34 | chainTime chaintime.Service 35 | activitySem *semaphore.Weighted 36 | } 37 | 38 | // module-wide log. 39 | var log zerolog.Logger 40 | 41 | // New creates a new service. 42 | func New(ctx context.Context, params ...Parameter) (*Service, error) { 43 | parameters, err := parseAndCheckParameters(params...) 44 | if err != nil { 45 | return nil, errors.Wrap(err, "problem with parameters") 46 | } 47 | 48 | // Set logging. 49 | log = zerologger.With().Str("service", "beaconcommittees").Str("impl", "standard").Logger().Level(parameters.logLevel) 50 | 51 | if err := registerMetrics(ctx, parameters.monitor); err != nil { 52 | return nil, errors.New("failed to register metrics") 53 | } 54 | 55 | beaconCommitteesSetter, isBeaconCommitteesSetter := parameters.chainDB.(chaindb.BeaconCommitteesSetter) 56 | if !isBeaconCommitteesSetter { 57 | return nil, errors.New("chain DB does not support beacon committee setting") 58 | } 59 | s := &Service{ 60 | eth2Client: parameters.eth2Client, 61 | chainDB: parameters.chainDB, 62 | beaconCommitteesSetter: beaconCommitteesSetter, 63 | chainTime: parameters.chainTime, 64 | activitySem: semaphore.NewWeighted(1), 65 | } 66 | 67 | // Update to current epoch before starting (in the background). 68 | go s.updateAfterRestart(ctx, parameters.startEpoch) 69 | 70 | return s, nil 71 | } 72 | 73 | func (s *Service) updateAfterRestart(ctx context.Context, startEpoch int64) { 74 | // Work out the epoch from which to start. 75 | md, err := s.getMetadata(ctx) 76 | if err != nil { 77 | log.Fatal().Err(err).Msg("Failed to obtain metadata before catchup") 78 | } 79 | if startEpoch >= 0 { 80 | // Explicit requirement to start at a given epoch. 81 | md.LatestEpoch = startEpoch - 1 82 | } 83 | 84 | log.Info().Int64("epoch", md.LatestEpoch+1).Msg("Catching up from epoch") 85 | // Only allow 1 handler to be active. 86 | acquired := s.activitySem.TryAcquire(1) 87 | if !acquired { 88 | log.Error().Msg("Failed to obtain activity semaphore; catchup deferred") 89 | return 90 | } 91 | s.catchup(ctx, md) 92 | s.activitySem.Release(1) 93 | 94 | // Set up the handler for new chain head updates. 95 | if err := s.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, func(event *api.Event) { 96 | eventData := event.Data.(*api.HeadEvent) 97 | s.OnBeaconChainHeadUpdated(ctx, eventData.Slot, eventData.Block, eventData.State, eventData.EpochTransition) 98 | }); err != nil { 99 | log.Fatal().Err(err).Msg("Failed to add beacon chain head updated handler") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /services/blocks/mock/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package mock 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec" 20 | ) 21 | 22 | // Service is a mock blocks service. 23 | type Service struct{} 24 | 25 | // New creates a new mock blocks service. 26 | func New() *Service { 27 | return &Service{} 28 | } 29 | 30 | // OnBlock handles a block. 31 | // This requires the context to hold an active transaction. 32 | func (*Service) OnBlock(_ context.Context, _ *spec.VersionedSignedBeaconBlock) error { 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /services/blocks/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package blocks 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec" 20 | ) 21 | 22 | // Service defines a block service. 23 | type Service interface { 24 | // OnBlock handles a block. 25 | // This requires the context to hold an active transaction. 26 | OnBlock(ctx context.Context, signedBlock *spec.VersionedSignedBeaconBlock) error 27 | } 28 | -------------------------------------------------------------------------------- /services/blocks/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // metadata stored about this service. 24 | type metadata struct { 25 | LatestSlot int64 `json:"latest_slot"` 26 | } 27 | 28 | // metadataKey is the key for the metadata. 29 | var metadataKey = "blocks.standard" 30 | 31 | // getMetadata gets metadata for this service. 32 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 33 | md := &metadata{ 34 | LatestSlot: -1, 35 | } 36 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "failed to fetch metadata") 39 | } 40 | if mdJSON == nil { 41 | return md, nil 42 | } 43 | if err := json.Unmarshal(mdJSON, md); err != nil { 44 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 45 | } 46 | return md, nil 47 | } 48 | 49 | // setMetadata sets metadata for this service. 50 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 51 | mdJSON, err := json.Marshal(md) 52 | if err != nil { 53 | return errors.Wrap(err, "failed to marshal metadata") 54 | } 55 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 56 | return errors.Wrap(err, "failed to update metadata") 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /services/blocks/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021, 2022 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | var metricsNamespace = "chaind_blocks" 26 | 27 | var ( 28 | highestSlot phase0.Slot 29 | latestSlot prometheus.Gauge 30 | slotsProcessed prometheus.Gauge 31 | ) 32 | 33 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 34 | if latestSlot != nil { 35 | // Already registered. 36 | return nil 37 | } 38 | if monitor == nil { 39 | // No monitor. 40 | return nil 41 | } 42 | if monitor.Presenter() == "prometheus" { 43 | return registerPrometheusMetrics() 44 | } 45 | return nil 46 | } 47 | 48 | // skipcq: RVV-B0012 49 | func registerPrometheusMetrics() error { 50 | latestSlot = prometheus.NewGauge(prometheus.GaugeOpts{ 51 | Namespace: metricsNamespace, 52 | Name: "latest_slot", 53 | Help: "Latest slot processed", 54 | }) 55 | if err := prometheus.Register(latestSlot); err != nil { 56 | return errors.Wrap(err, "failed to register latest_slot") 57 | } 58 | 59 | slotsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 60 | Namespace: metricsNamespace, 61 | Name: "slots_processed", 62 | Help: "Number of slots processed", 63 | }) 64 | if err := prometheus.Register(slotsProcessed); err != nil { 65 | return errors.Wrap(err, "failed to register slots_processed") 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // monitorLatestSlot sets the latest slot without registering an 72 | // increase in slots processed. This does not usually need to be 73 | // called directly, as it is called as part of monitorSlotProcessed. 74 | func monitorLatestSlot(slot phase0.Slot) { 75 | highestSlot = slot 76 | if latestSlot != nil { 77 | latestSlot.Set(float64(slot)) 78 | } 79 | } 80 | 81 | func monitorSlotProcessed(slot phase0.Slot) { 82 | if slotsProcessed != nil { 83 | slotsProcessed.Inc() 84 | if slot > highestSlot { 85 | monitorLatestSlot(slot) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /services/blocks/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "errors" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/chaintime" 23 | "github.com/wealdtech/chaind/services/metrics" 24 | "golang.org/x/sync/semaphore" 25 | ) 26 | 27 | type parameters struct { 28 | logLevel zerolog.Level 29 | monitor metrics.Service 30 | eth2Client eth2client.Service 31 | chainDB chaindb.Service 32 | chainTime chaintime.Service 33 | startSlot int64 34 | refetch bool 35 | activitySem *semaphore.Weighted 36 | } 37 | 38 | // Parameter is the interface for service parameters. 39 | type Parameter interface { 40 | apply(p *parameters) 41 | } 42 | 43 | type parameterFunc func(*parameters) 44 | 45 | func (f parameterFunc) apply(p *parameters) { 46 | f(p) 47 | } 48 | 49 | // WithLogLevel sets the log level for the module. 50 | func WithLogLevel(logLevel zerolog.Level) Parameter { 51 | return parameterFunc(func(p *parameters) { 52 | p.logLevel = logLevel 53 | }) 54 | } 55 | 56 | // WithMonitor sets the monitor for the module. 57 | func WithMonitor(monitor metrics.Service) Parameter { 58 | return parameterFunc(func(p *parameters) { 59 | p.monitor = monitor 60 | }) 61 | } 62 | 63 | // WithETH2Client sets the Ethereum 2 client for this module. 64 | func WithETH2Client(eth2Client eth2client.Service) Parameter { 65 | return parameterFunc(func(p *parameters) { 66 | p.eth2Client = eth2Client 67 | }) 68 | } 69 | 70 | // WithChainDB sets the chain database for this module. 71 | func WithChainDB(chainDB chaindb.Service) Parameter { 72 | return parameterFunc(func(p *parameters) { 73 | p.chainDB = chainDB 74 | }) 75 | } 76 | 77 | // WithChainTime sets the chain time service for this module. 78 | func WithChainTime(chainTime chaintime.Service) Parameter { 79 | return parameterFunc(func(p *parameters) { 80 | p.chainTime = chainTime 81 | }) 82 | } 83 | 84 | // WithStartSlot sets the start slot for this module. 85 | func WithStartSlot(startSlot int64) Parameter { 86 | return parameterFunc(func(p *parameters) { 87 | p.startSlot = startSlot 88 | }) 89 | } 90 | 91 | // WithRefetch sets the refetch flag for this module. 92 | func WithRefetch(refetch bool) Parameter { 93 | return parameterFunc(func(p *parameters) { 94 | p.refetch = refetch 95 | }) 96 | } 97 | 98 | // WithActivitySem sets the activity semaphore for this module. 99 | func WithActivitySem(sem *semaphore.Weighted) Parameter { 100 | return parameterFunc(func(p *parameters) { 101 | p.activitySem = sem 102 | }) 103 | } 104 | 105 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 106 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 107 | parameters := parameters{ 108 | logLevel: zerolog.GlobalLevel(), 109 | startSlot: -1, 110 | } 111 | for _, p := range params { 112 | if params != nil { 113 | p.apply(¶meters) 114 | } 115 | } 116 | 117 | if parameters.eth2Client == nil { 118 | return nil, errors.New("no Ethereum 2 client specified") 119 | } 120 | if parameters.chainDB == nil { 121 | return nil, errors.New("no chain database specified") 122 | } 123 | if parameters.chainTime == nil { 124 | return nil, errors.New("no chain time specified") 125 | } 126 | if parameters.activitySem == nil { 127 | return nil, errors.New("no activity semaphore specified") 128 | } 129 | 130 | return ¶meters, nil 131 | } 132 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/chainspec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | "time" 21 | 22 | "github.com/attestantio/go-eth2-client/spec/phase0" 23 | "github.com/rs/zerolog" 24 | "github.com/stretchr/testify/require" 25 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 26 | ) 27 | 28 | func TestSetChainSpecValue(t *testing.T) { 29 | ctx := context.Background() 30 | s, err := postgresql.New(ctx, 31 | postgresql.WithLogLevel(zerolog.Disabled), 32 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 33 | ) 34 | require.NoError(t, err) 35 | 36 | // Try to set outside of a transaction; should fail. 37 | require.EqualError(t, s.SetChainSpecValue(ctx, "TEST_VALUE", "1"), postgresql.ErrNoTransaction.Error()) 38 | 39 | ctx, cancel, err := s.BeginTx(ctx) 40 | require.NoError(t, err) 41 | defer cancel() 42 | 43 | // Set a value. 44 | require.NoError(t, s.SetChainSpecValue(ctx, "SECONDS_PER_MINUTE", 59*time.Second)) 45 | 46 | // Attempt to set the same deposit again; should succeed. 47 | require.NoError(t, s.SetChainSpecValue(ctx, "SECONDS_PER_MINUTE", 60*time.Second)) 48 | } 49 | 50 | func TestChainSpec(t *testing.T) { 51 | ctx := context.Background() 52 | s, err := postgresql.New(ctx, 53 | postgresql.WithLogLevel(zerolog.Disabled), 54 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 55 | ) 56 | require.NoError(t, err) 57 | 58 | ctx, cancel, err := s.BeginTx(ctx) 59 | require.NoError(t, err) 60 | defer cancel() 61 | 62 | tests := []struct { 63 | name string 64 | key string 65 | val any 66 | }{ 67 | { 68 | name: "Duration", 69 | key: "SECONDS_PER_MINUTE", 70 | val: 60 * time.Second, 71 | }, 72 | { 73 | name: "Integer", 74 | key: "IntegerVal", 75 | val: uint64(12345), 76 | }, 77 | { 78 | name: "String", 79 | key: "NAME", 80 | val: "mainnet", 81 | }, 82 | { 83 | name: "Domain", 84 | key: "DOMAIN_VOLUNTARY_EXIT", 85 | val: phase0.DomainType{0x01, 0x02, 0x03, 0x04}, 86 | }, 87 | { 88 | name: "ForkVersion", 89 | key: "GENESIS_FORK_VERSION", 90 | val: phase0.Version{0x01, 0x02, 0x03, 0x04}, 91 | }, 92 | { 93 | name: "Hex", 94 | key: "GENERIC_HEX_STRING", 95 | val: []byte{0x01, 0x02, 0x03, 0x04}, 96 | }, 97 | { 98 | name: "Time", 99 | key: "GENESIS_TIME", 100 | val: time.Unix(1600000000, 0), 101 | }, 102 | } 103 | 104 | // Set the values. 105 | for _, test := range tests { 106 | require.NoError(t, s.SetChainSpecValue(ctx, test.key, test.val)) 107 | } 108 | 109 | // Fetch the values 110 | for _, test := range tests { 111 | t.Run(test.name, func(t *testing.T) { 112 | val, err := s.ChainSpecValue(ctx, test.key) 113 | require.NoError(t, err) 114 | require.Equal(t, test.val, val) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/forkschedule.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021, 2023 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/api" 20 | "github.com/attestantio/go-eth2-client/spec/phase0" 21 | "github.com/pkg/errors" 22 | "go.opentelemetry.io/otel" 23 | ) 24 | 25 | // SetForkSchedule sets the fork schedule. 26 | // This carries out a complete rewrite of the table. 27 | func (s *Service) SetForkSchedule(ctx context.Context, schedule []*phase0.Fork) error { 28 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetForkSchedule") 29 | defer span.End() 30 | 31 | tx := s.tx(ctx) 32 | if tx == nil { 33 | return ErrNoTransaction 34 | } 35 | 36 | _, err := tx.Exec(ctx, ` 37 | TRUNCATE TABLE t_fork_schedule 38 | `) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | for _, fork := range schedule { 44 | _, err := tx.Exec(ctx, ` 45 | INSERT INTO t_fork_schedule(f_version 46 | ,f_epoch 47 | ,f_previous_version 48 | ) 49 | VALUES($1,$2,$3) 50 | `, 51 | fork.CurrentVersion[:], 52 | fork.Epoch, 53 | fork.PreviousVersion[:], 54 | ) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | 60 | return err 61 | } 62 | 63 | // ForkSchedule provides details of past and future changes in the chain's fork version. 64 | func (s *Service) ForkSchedule(ctx context.Context, _ *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) { 65 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "ForkSchedule") 66 | defer span.End() 67 | 68 | tx := s.tx(ctx) 69 | if tx == nil { 70 | ctx, err := s.BeginROTx(ctx) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "failed to begin transaction") 73 | } 74 | defer s.CommitROTx(ctx) 75 | tx = s.tx(ctx) 76 | } 77 | 78 | schedule := make([]*phase0.Fork, 0) 79 | rows, err := tx.Query(ctx, ` 80 | SELECT f_version 81 | ,f_epoch 82 | ,f_previous_version 83 | FROM t_fork_schedule 84 | ORDER BY f_epoch 85 | `) 86 | if err != nil { 87 | return nil, err 88 | } 89 | defer rows.Close() 90 | 91 | for rows.Next() { 92 | fork := &phase0.Fork{} 93 | var version []byte 94 | var previousVersion []byte 95 | err := rows.Scan( 96 | &version, 97 | &fork.Epoch, 98 | &previousVersion, 99 | ) 100 | if err != nil { 101 | return nil, errors.Wrap(err, "failed to scan row") 102 | } 103 | copy(fork.CurrentVersion[:], version) 104 | copy(fork.PreviousVersion[:], previousVersion) 105 | schedule = append(schedule, fork) 106 | } 107 | 108 | return &api.Response[[]*phase0.Fork]{ 109 | Data: schedule, 110 | Metadata: make(map[string]any), 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/forkschedule_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | eth2client "github.com/attestantio/go-eth2-client" 22 | "github.com/attestantio/go-eth2-client/api" 23 | "github.com/rs/zerolog" 24 | "github.com/stretchr/testify/require" 25 | "github.com/wealdtech/chaind/services/chaindb" 26 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 27 | ) 28 | 29 | func TestForkSchedule(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | var s chaindb.Service 33 | var err error 34 | s, err = postgresql.New(ctx, 35 | postgresql.WithLogLevel(zerolog.Disabled), 36 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 37 | ) 38 | require.NoError(t, err) 39 | 40 | // Ensure this meets the chaindb interface requirement. 41 | _, isProvider := s.(chaindb.ForkScheduleProvider) 42 | require.True(t, isProvider) 43 | 44 | // Ensure the value. 45 | scheduleResponse, err := s.(eth2client.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{}) 46 | require.NoError(t, err) 47 | schedule := scheduleResponse.Data 48 | 49 | require.True(t, len(schedule) > 0) 50 | } 51 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/genesis.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | "time" 19 | 20 | "github.com/attestantio/go-eth2-client/api" 21 | apiv1 "github.com/attestantio/go-eth2-client/api/v1" 22 | "github.com/pkg/errors" 23 | "go.opentelemetry.io/otel" 24 | ) 25 | 26 | // SetGenesis sets the genesis information. 27 | func (s *Service) SetGenesis(ctx context.Context, genesis *apiv1.Genesis) error { 28 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetGenesis") 29 | defer span.End() 30 | 31 | tx := s.tx(ctx) 32 | if tx == nil { 33 | return ErrNoTransaction 34 | } 35 | 36 | _, err := tx.Exec(ctx, ` 37 | INSERT INTO t_genesis(f_validators_root 38 | ,f_time 39 | ,f_fork_version) 40 | VALUES($1,$2,$3) 41 | ON CONFLICT (f_validators_root) DO 42 | UPDATE 43 | SET f_time = excluded.f_time 44 | ,f_fork_version = excluded.f_fork_version 45 | `, 46 | genesis.GenesisValidatorsRoot[:], 47 | genesis.GenesisTime, 48 | genesis.GenesisForkVersion[:], 49 | ) 50 | 51 | return err 52 | } 53 | 54 | // Genesis fetches genesis values. 55 | func (s *Service) Genesis(ctx context.Context, 56 | _ *api.GenesisOpts, 57 | ) ( 58 | *api.Response[*apiv1.Genesis], 59 | error, 60 | ) { 61 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "Genesis") 62 | defer span.End() 63 | 64 | tx := s.tx(ctx) 65 | if tx == nil { 66 | ctx, err := s.BeginROTx(ctx) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "failed to begin transaction") 69 | } 70 | defer s.CommitROTx(ctx) 71 | tx = s.tx(ctx) 72 | } 73 | 74 | genesis := &apiv1.Genesis{} 75 | var genesisValidatorsRoot []byte 76 | var genesisForkVersion []byte 77 | err := tx.QueryRow(ctx, ` 78 | SELECT f_validators_root 79 | ,f_time 80 | ,f_fork_version 81 | FROM t_genesis 82 | `).Scan( 83 | &genesisValidatorsRoot, 84 | &genesis.GenesisTime, 85 | &genesisForkVersion, 86 | ) 87 | if err != nil { 88 | return nil, err 89 | } 90 | copy(genesis.GenesisValidatorsRoot[:], genesisValidatorsRoot) 91 | copy(genesis.GenesisForkVersion[:], genesisForkVersion) 92 | 93 | return &api.Response[*apiv1.Genesis]{ 94 | Data: genesis, 95 | Metadata: make(map[string]any), 96 | }, nil 97 | } 98 | 99 | // GenesisTime provides the genesis time of the chain. 100 | func (s *Service) GenesisTime(ctx context.Context) (time.Time, error) { 101 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "GenesisTime") 102 | defer span.End() 103 | 104 | genesisResponse, err := s.Genesis(ctx, &api.GenesisOpts{}) 105 | if err != nil { 106 | return time.Time{}, errors.Wrap(err, "failed to obtain genesis") 107 | } 108 | return genesisResponse.Data.GenesisTime, nil 109 | } 110 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/genesis_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | "time" 21 | 22 | eth2client "github.com/attestantio/go-eth2-client" 23 | api "github.com/attestantio/go-eth2-client/api/v1" 24 | "github.com/attestantio/go-eth2-client/spec/phase0" 25 | "github.com/rs/zerolog" 26 | "github.com/stretchr/testify/require" 27 | "github.com/wealdtech/chaind/services/chaindb" 28 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 29 | ) 30 | 31 | func TestSetGenesis(t *testing.T) { 32 | ctx := context.Background() 33 | s, err := postgresql.New(ctx, 34 | postgresql.WithLogLevel(zerolog.Disabled), 35 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 36 | ) 37 | require.NoError(t, err) 38 | 39 | genesis := &api.Genesis{ 40 | GenesisTime: time.Unix(1600000000, 0), 41 | GenesisValidatorsRoot: phase0.Root{ 42 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 43 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 44 | }, 45 | GenesisForkVersion: phase0.Version{0x00, 0x01, 0x02, 0x03}, 46 | } 47 | 48 | // Try to set outside of a transaction; should fail. 49 | require.EqualError(t, s.SetGenesis(ctx, genesis), postgresql.ErrNoTransaction.Error()) 50 | 51 | ctx, cancel, err := s.BeginTx(ctx) 52 | require.NoError(t, err) 53 | defer cancel() 54 | 55 | // Set. 56 | require.NoError(t, s.SetGenesis(ctx, genesis)) 57 | 58 | // Attempt to set again; should succeed. 59 | require.NoError(t, s.SetGenesis(ctx, genesis)) 60 | } 61 | 62 | func TestGenesisTime(t *testing.T) { 63 | ctx := context.Background() 64 | 65 | var s chaindb.Service 66 | var err error 67 | s, err = postgresql.New(ctx, 68 | postgresql.WithLogLevel(zerolog.Disabled), 69 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 70 | ) 71 | require.NoError(t, err) 72 | 73 | // Ensure this meets the eth2client interface requirement. 74 | _, isProvider := s.(eth2client.GenesisTimeProvider) 75 | require.True(t, isProvider) 76 | 77 | // Ensure the value 78 | genesisTime, err := s.(eth2client.GenesisTimeProvider).GenesisTime(ctx) 79 | require.NoError(t, err) 80 | require.NotNil(t, genesisTime) 81 | } 82 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "os" 18 | "strconv" 19 | "testing" 20 | 21 | "github.com/attestantio/go-eth2-client/spec/phase0" 22 | "github.com/rs/zerolog" 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | zerolog.SetGlobalLevel(zerolog.Disabled) 27 | if os.Getenv("CHAINDB_URL") != "" || 28 | os.Getenv("CHAINDB_SERVER") != "" { 29 | os.Exit(m.Run()) 30 | } 31 | } 32 | 33 | func slotPtr(input phase0.Slot) *phase0.Slot { 34 | return &input 35 | } 36 | 37 | func atoi(input string) int32 { 38 | val, err := strconv.ParseInt(input, 10, 32) 39 | if err != nil { 40 | panic(err) 41 | } 42 | return int32(val) 43 | } 44 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/jackc/pgx/v5" 20 | "github.com/pkg/errors" 21 | "go.opentelemetry.io/otel" 22 | ) 23 | 24 | // SetMetadata sets a metadata key to a JSON value. 25 | func (s *Service) SetMetadata(ctx context.Context, key string, value []byte) error { 26 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetMetadata") 27 | defer span.End() 28 | 29 | tx := s.tx(ctx) 30 | if tx == nil { 31 | return ErrNoTransaction 32 | } 33 | 34 | _, err := tx.Exec(ctx, ` 35 | INSERT INTO t_metadata(f_key 36 | ,f_value) 37 | VALUES($1,$2) 38 | ON CONFLICT (f_key) DO 39 | UPDATE 40 | SET f_value = excluded.f_value`, 41 | key, 42 | value, 43 | ) 44 | 45 | return err 46 | } 47 | 48 | // Metadata obtains the JSON value from a metadata key. 49 | func (s *Service) Metadata(ctx context.Context, key string) ([]byte, error) { 50 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "Metadata") 51 | defer span.End() 52 | 53 | var err error 54 | 55 | tx := s.tx(ctx) 56 | if tx == nil { 57 | ctx, err = s.BeginROTx(ctx) 58 | if err != nil { 59 | return nil, err 60 | } 61 | tx = s.tx(ctx) 62 | defer s.CommitROTx(ctx) 63 | } 64 | 65 | res := make([]byte, 0) 66 | err = tx.QueryRow(ctx, ` 67 | SELECT f_value 68 | FROM t_metadata 69 | WHERE f_key = $1`, 70 | key).Scan( 71 | &res, 72 | ) 73 | if err != nil { 74 | if errors.Is(err, pgx.ErrNoRows) { 75 | return nil, nil 76 | } 77 | return nil, errors.Wrap(err, "failed to obtain metadata") 78 | } 79 | 80 | return res, nil 81 | } 82 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/partitioning.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/wealdtech/chaind/services/chaintime" 23 | standardchaintime "github.com/wealdtech/chaind/services/chaintime/standard" 24 | ) 25 | 26 | func (s *Service) startPartitioningTicker(ctx context.Context) error { 27 | chainTime, err := standardchaintime.New(ctx, 28 | standardchaintime.WithGenesisProvider(s), 29 | standardchaintime.WithSpecProvider(s), 30 | standardchaintime.WithForkScheduleProvider(s), 31 | ) 32 | if err != nil { 33 | return errors.Wrap(err, "failed to start chaintime service for partitioning") 34 | } 35 | 36 | go func(s *Service, 37 | ctx context.Context, 38 | chainTime chaintime.Service, 39 | ) { 40 | // Start with an immediate update. 41 | s.updatePartitions(ctx, chainTime) 42 | // Set up a ticker for future updates. 43 | ticker := time.NewTicker(4 * time.Hour) 44 | defer ticker.Stop() 45 | for { 46 | select { 47 | case <-ticker.C: 48 | s.updatePartitions(ctx, chainTime) 49 | case <-ctx.Done(): 50 | return 51 | } 52 | } 53 | }(s, ctx, chainTime) 54 | 55 | return nil 56 | } 57 | 58 | func (s *Service) updatePartitions(ctx context.Context, 59 | chainTime chaintime.Service, 60 | ) { 61 | today := time.Now().In(time.UTC) 62 | today = time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC) 63 | if err := s.createTablePartitions(ctx, chainTime, today); err != nil { 64 | log.Error().Err(err).Time("date", today).Msg("Failed to create table partitions for current date") 65 | } 66 | 67 | tomorrow := today.AddDate(0, 0, 1) 68 | if err := s.createTablePartitions(ctx, chainTime, tomorrow); err != nil { 69 | log.Error().Err(err).Time("date", today).Msg("Failed to create table partitions for future date") 70 | } 71 | } 72 | 73 | func (s *Service) createTablePartitions(ctx context.Context, 74 | chainTime chaintime.Service, 75 | start time.Time, 76 | ) error { 77 | suffix := fmt.Sprintf("%04d_%02d_%02d", start.Year(), start.Month(), start.Day()) 78 | 79 | startEpoch := chainTime.TimestampToEpoch(start) 80 | 81 | end := start.AddDate(0, 0, 1) 82 | // endEpoch is exclusive, which means that endEpoch for today will equal startEpoch tomorrow. 83 | // As such, no reduction in endEpoch is required. 84 | endEpoch := chainTime.TimestampToEpoch(end) 85 | 86 | ctx, cancel, err := s.BeginTx(ctx) 87 | if err != nil { 88 | return errors.Wrap(err, "failed to begin create table partitions transaction") 89 | } 90 | 91 | tx := s.tx(ctx) 92 | if tx == nil { 93 | cancel() 94 | return ErrNoTransaction 95 | } 96 | 97 | if _, err := tx.Exec(ctx, 98 | fmt.Sprintf(`CREATE TABLE IF NOT EXISTS t_validator_balances_%s 99 | PARTITION OF t_validator_balances 100 | FOR VALUES FROM (%d) TO (%d) 101 | `, suffix, 102 | startEpoch, 103 | endEpoch, 104 | )); err != nil { 105 | cancel() 106 | return errors.Wrap(err, "failed to create validator balances partition") 107 | } 108 | 109 | if _, err := tx.Exec(ctx, 110 | fmt.Sprintf(`CREATE TABLE IF NOT EXISTS t_validator_epoch_summaries_%s 111 | PARTITION OF t_validator_epoch_summaries 112 | FOR VALUES FROM (%d) TO (%d) 113 | `, suffix, 114 | startEpoch, 115 | endEpoch, 116 | )); err != nil { 117 | cancel() 118 | return errors.Wrap(err, "failed to create validator epoch summaries partition") 119 | } 120 | 121 | if err := s.CommitTx(ctx); err != nil { 122 | cancel() 123 | return errors.Wrap(err, "failed to commit create table partitions transaction") 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | "github.com/wealdtech/chaind/services/chaindb" 24 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 25 | ) 26 | 27 | func TestService(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | connectionURL string 31 | err string 32 | }{ 33 | { 34 | name: "ServerMissing", 35 | err: "problem with parameters: no server specified", 36 | }, 37 | { 38 | name: "Good", 39 | connectionURL: os.Getenv("CHAINDB_URL"), 40 | }, 41 | } 42 | 43 | for _, test := range tests { 44 | t.Run(test.name, func(t *testing.T) { 45 | ctx := context.Background() 46 | _, err := postgresql.New(ctx, 47 | postgresql.WithConnectionURL(test.connectionURL), 48 | ) 49 | if test.err != "" { 50 | assert.EqualError(t, err, test.err) 51 | } else { 52 | assert.NoError(t, err) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestInterfaces(t *testing.T) { 59 | ctx := context.Background() 60 | s, err := postgresql.New(ctx, postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL"))) 61 | require.NoError(t, err) 62 | 63 | require.Implements(t, (*chaindb.Service)(nil), s) 64 | require.Implements(t, (*chaindb.AttestationsProvider)(nil), s) 65 | require.Implements(t, (*chaindb.AttestationsSetter)(nil), s) 66 | require.Implements(t, (*chaindb.AttesterSlashingsSetter)(nil), s) 67 | require.Implements(t, (*chaindb.BeaconCommitteesProvider)(nil), s) 68 | require.Implements(t, (*chaindb.BeaconCommitteesSetter)(nil), s) 69 | require.Implements(t, (*chaindb.BlocksProvider)(nil), s) 70 | require.Implements(t, (*chaindb.BlocksSetter)(nil), s) 71 | require.Implements(t, (*chaindb.ProposerDutiesSetter)(nil), s) 72 | require.Implements(t, (*chaindb.ProposerSlashingsSetter)(nil), s) 73 | require.Implements(t, (*chaindb.ValidatorsProvider)(nil), s) 74 | require.Implements(t, (*chaindb.ValidatorsSetter)(nil), s) 75 | require.Implements(t, (*chaindb.VoluntaryExitsSetter)(nil), s) 76 | } 77 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setblobsidecar.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | 20 | "github.com/wealdtech/chaind/services/chaindb" 21 | "go.opentelemetry.io/otel" 22 | ) 23 | 24 | // SetBlobSidecar sets a blob sidecar. 25 | func (s *Service) SetBlobSidecar(ctx context.Context, blobSidecar *chaindb.BlobSidecar) error { 26 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetBlobSidecar") 27 | defer span.End() 28 | 29 | tx := s.tx(ctx) 30 | if tx == nil { 31 | return ErrNoTransaction 32 | } 33 | 34 | var blob *[]byte 35 | if len(blobSidecar.Blob) > 0 { 36 | blobBytes := blobSidecar.Blob[:] 37 | // Trim trailing 0s. 38 | blobBytes = bytes.TrimRight(blobBytes, string([]byte{0x00})) 39 | blob = &blobBytes 40 | } 41 | kzgCommitmentInclusionProof := make([]byte, 0, 17*32) 42 | for i := range blobSidecar.KZGCommitmentInclusionProof[:] { 43 | kzgCommitmentInclusionProof = append(kzgCommitmentInclusionProof, blobSidecar.KZGCommitmentInclusionProof[i][:]...) 44 | } 45 | 46 | if _, err := tx.Exec(ctx, ` 47 | INSERT INTO t_blob_sidecars(f_block_root 48 | ,f_slot 49 | ,f_index 50 | ,f_blob 51 | ,f_kzg_commitment 52 | ,f_kzg_proof 53 | ,f_kzg_commitment_inclusion_proof 54 | ) 55 | VALUES($1,$2,$3,$4,$5,$6,$7) 56 | ON CONFLICT(f_block_root,f_index) DO 57 | UPDATE 58 | SET f_slot = excluded.f_slot 59 | ,f_blob = excluded.f_blob 60 | ,f_kzg_commitment = excluded.f_kzg_commitment 61 | ,f_kzg_proof = excluded.f_kzg_proof 62 | ,f_kzg_commitment_inclusion_proof = excluded.f_kzg_commitment_inclusion_proof 63 | `, 64 | blobSidecar.InclusionBlockRoot[:], 65 | blobSidecar.InclusionSlot, 66 | blobSidecar.InclusionIndex, 67 | blob, 68 | blobSidecar.KZGCommitment[:], 69 | blobSidecar.KZGProof[:], 70 | kzgCommitmentInclusionProof, 71 | ); err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setblobsidecars.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | 20 | "github.com/jackc/pgx/v5" 21 | "github.com/pkg/errors" 22 | "github.com/wealdtech/chaind/services/chaindb" 23 | "go.opentelemetry.io/otel" 24 | ) 25 | 26 | // SetBlobSidecars sets blob sidecars. 27 | func (s *Service) SetBlobSidecars(ctx context.Context, blobSidecars []*chaindb.BlobSidecar) error { 28 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetBlobSidecars") 29 | defer span.End() 30 | 31 | tx := s.tx(ctx) 32 | if tx == nil { 33 | return ErrNoTransaction 34 | } 35 | 36 | // Create a savepoint in case the copy fails. 37 | nestedTx, err := tx.Begin(ctx) 38 | if err != nil { 39 | return errors.Wrap(err, "failed to create nested transaction") 40 | } 41 | 42 | _, err = nestedTx.CopyFrom(ctx, 43 | pgx.Identifier{"t_blob_sidecars"}, 44 | []string{ 45 | "f_block_root", 46 | "f_slot", 47 | "f_index", 48 | "f_blob", 49 | "f_kzg_commitment", 50 | "f_kzg_proof", 51 | "f_kzg_commitment_inclusion_proof", 52 | }, 53 | pgx.CopyFromSlice(len(blobSidecars), func(i int) ([]interface{}, error) { 54 | var blob *[]byte 55 | if len(blobSidecars[i].Blob) > 0 { 56 | blobBytes := blobSidecars[i].Blob[:] 57 | // Trim trailing 0s. 58 | blobBytes = bytes.TrimRight(blobBytes, string([]byte{0x00})) 59 | blob = &blobBytes 60 | } 61 | kzgCommitmentInclusionProof := make([]byte, 0, 17*32) 62 | for j := range blobSidecars[i].KZGCommitmentInclusionProof[:] { 63 | kzgCommitmentInclusionProof = append(kzgCommitmentInclusionProof, blobSidecars[i].KZGCommitmentInclusionProof[j][:]...) 64 | } 65 | 66 | return []interface{}{ 67 | blobSidecars[i].InclusionBlockRoot[:], 68 | blobSidecars[i].InclusionSlot, 69 | blobSidecars[i].InclusionIndex, 70 | blob, 71 | blobSidecars[i].KZGCommitment[:], 72 | blobSidecars[i].KZGProof[:], 73 | kzgCommitmentInclusionProof, 74 | }, nil 75 | })) 76 | if err != nil { 77 | if err := nestedTx.Rollback(ctx); err != nil { 78 | return errors.Wrap(err, "failed to roll back nested transaction") 79 | } 80 | 81 | log.Debug().Err(err).Msg("Failed to copy insert blob sidecars; applying one at a time") 82 | for _, blobSidecar := range blobSidecars { 83 | if err := s.SetBlobSidecar(ctx, blobSidecar); err != nil { 84 | return err 85 | } 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setconsolidationrequest.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/wealdtech/chaind/services/chaindb" 20 | "go.opentelemetry.io/otel" 21 | ) 22 | 23 | // SetConsolidationRequest sets a consolidation request. 24 | func (s *Service) SetConsolidationRequest(ctx context.Context, request *chaindb.ConsolidationRequest) error { 25 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetConsolidationRequest") 26 | defer span.End() 27 | 28 | tx := s.tx(ctx) 29 | if tx == nil { 30 | return ErrNoTransaction 31 | } 32 | 33 | if _, err := tx.Exec(ctx, ` 34 | INSERT INTO t_block_consolidation_requests(f_block_root 35 | ,f_slot 36 | ,f_index 37 | ,f_source_address 38 | ,f_source_pubkey 39 | ,f_target_pubkey 40 | ) 41 | VALUES($1,$2,$3,$4,$5,$6) 42 | ON CONFLICT(f_block_root,f_index) DO 43 | UPDATE 44 | SET f_slot = excluded.f_slot 45 | ,f_source_address = excluded.f_source_address 46 | ,f_source_pubkey = excluded.f_source_pubkey 47 | ,f_target_pubkey = excluded.f_target_pubkey 48 | `, 49 | request.InclusionBlockRoot[:], 50 | request.InclusionSlot, 51 | request.InclusionIndex, 52 | request.SourceAddress[:], 53 | request.SourcePubkey[:], 54 | request.TargetPubkey[:], 55 | ); err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setconsolidationrequests.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/jackc/pgx/v5" 20 | "github.com/pkg/errors" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "go.opentelemetry.io/otel" 23 | ) 24 | 25 | // SetConsolidationRequests sets consolidation requests. 26 | func (s *Service) SetConsolidationRequests(ctx context.Context, requests []*chaindb.ConsolidationRequest) error { 27 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetConsolidationRequests") 28 | defer span.End() 29 | 30 | tx := s.tx(ctx) 31 | if tx == nil { 32 | return ErrNoTransaction 33 | } 34 | 35 | // Create a savepoint in case the copy fails. 36 | nestedTx, err := tx.Begin(ctx) 37 | if err != nil { 38 | return errors.Wrap(err, "failed to create nested transaction") 39 | } 40 | 41 | _, err = nestedTx.CopyFrom(ctx, 42 | pgx.Identifier{"t_block_consolidation_requests"}, 43 | []string{ 44 | "f_block_root", 45 | "f_slot", 46 | "f_index", 47 | "f_source_address", 48 | "f_source_pubkey", 49 | "f_target_pubkey", 50 | }, 51 | pgx.CopyFromSlice(len(requests), func(i int) ([]interface{}, error) { 52 | return []interface{}{ 53 | requests[i].InclusionBlockRoot[:], 54 | requests[i].InclusionSlot, 55 | requests[i].InclusionIndex, 56 | requests[i].SourceAddress[:], 57 | requests[i].SourcePubkey[:], 58 | requests[i].TargetPubkey[:], 59 | }, nil 60 | })) 61 | if err != nil { 62 | if err := nestedTx.Rollback(ctx); err != nil { 63 | return errors.Wrap(err, "failed to roll back nested transaction") 64 | } 65 | 66 | log.Debug().Err(err).Msg("Failed to copy insert consolidation requests; applying one at a time") 67 | for _, request := range requests { 68 | if err := s.SetConsolidationRequest(ctx, request); err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setdepositrequest.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/wealdtech/chaind/services/chaindb" 20 | "go.opentelemetry.io/otel" 21 | ) 22 | 23 | // SetDepositRequest sets a deposit request. 24 | func (s *Service) SetDepositRequest(ctx context.Context, request *chaindb.DepositRequest) error { 25 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetDepositRequest") 26 | defer span.End() 27 | 28 | tx := s.tx(ctx) 29 | if tx == nil { 30 | return ErrNoTransaction 31 | } 32 | 33 | if _, err := tx.Exec(ctx, ` 34 | INSERT INTO t_block_deposit_requests(f_block_root 35 | ,f_slot 36 | ,f_index 37 | ,f_pubkey 38 | ,f_withdrawal_credentials 39 | ,f_amount 40 | ,f_signature 41 | ,f_deposit_index 42 | ) 43 | VALUES($1,$2,$3,$4,$5,$6,$7,$8) 44 | ON CONFLICT(f_block_root,f_index) DO 45 | UPDATE 46 | SET f_slot = excluded.f_slot 47 | ,f_pubkey = excluded.f_pubkey 48 | ,f_withdrawal_credentials = excluded.f_withdrawal_credentials 49 | ,f_amount = excluded.f_amount 50 | ,f_signature = excluded.f_signature 51 | ,f_index = excluded.f_index 52 | `, 53 | request.InclusionBlockRoot[:], 54 | request.InclusionSlot, 55 | request.InclusionIndex, 56 | request.Pubkey[:], 57 | request.WithdrawalCredentials[:], 58 | request.Amount, 59 | request.Signature[:], 60 | request.Index, 61 | ); err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setdepositrequests.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/jackc/pgx/v5" 20 | "github.com/pkg/errors" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "go.opentelemetry.io/otel" 23 | ) 24 | 25 | // SetDepositRequests sets deposit requests. 26 | func (s *Service) SetDepositRequests(ctx context.Context, requests []*chaindb.DepositRequest) error { 27 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetDepositRequests") 28 | defer span.End() 29 | 30 | tx := s.tx(ctx) 31 | if tx == nil { 32 | return ErrNoTransaction 33 | } 34 | 35 | // Create a savepoint in case the copy fails. 36 | nestedTx, err := tx.Begin(ctx) 37 | if err != nil { 38 | return errors.Wrap(err, "failed to create nested transaction") 39 | } 40 | 41 | _, err = nestedTx.CopyFrom(ctx, 42 | pgx.Identifier{"t_block_deposit_requests"}, 43 | []string{ 44 | "f_block_root", 45 | "f_slot", 46 | "f_index", 47 | "f_pubkey", 48 | "f_withdrawal_credentials", 49 | "f_amount", 50 | "f_signature", 51 | "f_deposit_index", 52 | }, 53 | pgx.CopyFromSlice(len(requests), func(i int) ([]interface{}, error) { 54 | return []interface{}{ 55 | requests[i].InclusionBlockRoot[:], 56 | requests[i].InclusionSlot, 57 | requests[i].InclusionIndex, 58 | requests[i].Pubkey[:], 59 | requests[i].WithdrawalCredentials[:], 60 | requests[i].Amount, 61 | requests[i].Signature[:], 62 | requests[i].Index, 63 | }, nil 64 | })) 65 | if err != nil { 66 | if err := nestedTx.Rollback(ctx); err != nil { 67 | return errors.Wrap(err, "failed to roll back nested transaction") 68 | } 69 | 70 | log.Debug().Err(err).Msg("Failed to copy insert deposit requests; applying one at a time") 71 | for _, request := range requests { 72 | if err := s.SetDepositRequest(ctx, request); err != nil { 73 | return err 74 | } 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setwithdrawalrequest.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/wealdtech/chaind/services/chaindb" 20 | "go.opentelemetry.io/otel" 21 | ) 22 | 23 | // SetWithdrawalRequest sets a withdrawal request. 24 | func (s *Service) SetWithdrawalRequest(ctx context.Context, request *chaindb.WithdrawalRequest) error { 25 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetWithdrawalRequest") 26 | defer span.End() 27 | 28 | tx := s.tx(ctx) 29 | if tx == nil { 30 | return ErrNoTransaction 31 | } 32 | 33 | if _, err := tx.Exec(ctx, ` 34 | INSERT INTO t_block_withdrawal_requests(f_block_root 35 | ,f_slot 36 | ,f_index 37 | ,f_source_address 38 | ,f_validator_pubkey 39 | ,f_amount 40 | ) 41 | VALUES($1,$2,$3,$4,$5,$6) 42 | ON CONFLICT(f_block_root,f_index) DO 43 | UPDATE 44 | SET f_slot = excluded.f_slot 45 | ,f_source_address = excluded.f_source_address 46 | ,f_validator_pubkey = excluded.f_validator_pubkey 47 | ,f_amount = excluded.f_amount 48 | `, 49 | request.InclusionBlockRoot[:], 50 | request.InclusionSlot, 51 | request.InclusionIndex, 52 | request.SourceAddress[:], 53 | request.ValidatorPubkey[:], 54 | request.Amount, 55 | ); err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/setwithdrawalrequests.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/jackc/pgx/v5" 20 | "github.com/pkg/errors" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "go.opentelemetry.io/otel" 23 | ) 24 | 25 | // SetWithdrawalRequests sets withdrawal requests. 26 | func (s *Service) SetWithdrawalRequests(ctx context.Context, requests []*chaindb.WithdrawalRequest) error { 27 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetWithdrawalRequests") 28 | defer span.End() 29 | 30 | tx := s.tx(ctx) 31 | if tx == nil { 32 | return ErrNoTransaction 33 | } 34 | 35 | // Create a savepoint in case the copy fails. 36 | nestedTx, err := tx.Begin(ctx) 37 | if err != nil { 38 | return errors.Wrap(err, "failed to create nested transaction") 39 | } 40 | 41 | _, err = nestedTx.CopyFrom(ctx, 42 | pgx.Identifier{"t_block_withdrawal_requests"}, 43 | []string{ 44 | "f_block_root", 45 | "f_slot", 46 | "f_index", 47 | "f_source_address", 48 | "f_validator_pubkey", 49 | "f_amount", 50 | }, 51 | pgx.CopyFromSlice(len(requests), func(i int) ([]interface{}, error) { 52 | return []interface{}{ 53 | requests[i].InclusionBlockRoot[:], 54 | requests[i].InclusionSlot, 55 | requests[i].InclusionIndex, 56 | requests[i].SourceAddress[:], 57 | requests[i].ValidatorPubkey[:], 58 | requests[i].Amount, 59 | }, nil 60 | })) 61 | if err != nil { 62 | if err := nestedTx.Rollback(ctx); err != nil { 63 | return errors.Wrap(err, "failed to roll back nested transaction") 64 | } 65 | 66 | log.Debug().Err(err).Msg("Failed to copy insert withdrawal requests; applying one at a time") 67 | for _, request := range requests { 68 | if err := s.SetWithdrawalRequest(ctx, request); err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/api" 20 | ) 21 | 22 | // Spec provides the spec information of the chain. 23 | func (s *Service) Spec(ctx context.Context, 24 | _ *api.SpecOpts, 25 | ) ( 26 | *api.Response[map[string]any], 27 | error, 28 | ) { 29 | res, err := s.ChainSpec(ctx) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &api.Response[map[string]any]{ 35 | Data: res, 36 | Metadata: make(map[string]any), 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/spec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | eth2client "github.com/attestantio/go-eth2-client" 22 | "github.com/attestantio/go-eth2-client/api" 23 | "github.com/rs/zerolog" 24 | "github.com/stretchr/testify/require" 25 | "github.com/wealdtech/chaind/services/chaindb" 26 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 27 | ) 28 | 29 | func TestSpec(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | var s chaindb.Service 33 | var err error 34 | s, err = postgresql.New(ctx, 35 | postgresql.WithLogLevel(zerolog.Disabled), 36 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 37 | ) 38 | require.NoError(t, err) 39 | 40 | // Ensure this meets the chaindb interface requirement. 41 | _, isProvider := s.(chaindb.ChainSpecProvider) 42 | require.True(t, isProvider) 43 | 44 | // Ensure the value. 45 | specResponse, err := s.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{}) 46 | require.NoError(t, err) 47 | // This is hard-coded to the documented value; may fail on non-standard chains. 48 | require.Equal(t, uint64(32), specResponse.Data["SLOTS_PER_EPOCH"]) 49 | } 50 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/synccommittees.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "go.opentelemetry.io/otel" 23 | ) 24 | 25 | // SetSyncCommittee sets a sync committee. 26 | func (s *Service) SetSyncCommittee(ctx context.Context, syncCommittee *chaindb.SyncCommittee) error { 27 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetSyncCommittee") 28 | defer span.End() 29 | 30 | tx := s.tx(ctx) 31 | if tx == nil { 32 | return ErrNoTransaction 33 | } 34 | 35 | _, err := tx.Exec(ctx, ` 36 | INSERT INTO t_sync_committees(f_period 37 | ,f_committee) 38 | VALUES($1,$2) 39 | ON CONFLICT (f_period) DO 40 | UPDATE 41 | SET f_committee = excluded.f_committee 42 | `, 43 | syncCommittee.Period, 44 | syncCommittee.Committee, 45 | ) 46 | 47 | return err 48 | } 49 | 50 | // SyncCommittee provides a sync committee for the given sync committee period. 51 | func (s *Service) SyncCommittee(ctx context.Context, period uint64) (*chaindb.SyncCommittee, error) { 52 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SyncCommittee") 53 | defer span.End() 54 | 55 | tx := s.tx(ctx) 56 | if tx == nil { 57 | ctx, err := s.BeginROTx(ctx) 58 | if err != nil { 59 | return nil, errors.Wrap(err, "failed to begin transaction") 60 | } 61 | defer s.CommitROTx(ctx) 62 | tx = s.tx(ctx) 63 | } 64 | 65 | committee := &chaindb.SyncCommittee{} 66 | var committeeMembers []uint64 67 | 68 | err := tx.QueryRow(ctx, ` 69 | SELECT f_period 70 | ,f_committee 71 | FROM t_sync_committees 72 | WHERE f_period = $1 73 | `, 74 | period, 75 | ).Scan( 76 | &committee.Period, 77 | &committeeMembers, 78 | ) 79 | if err != nil { 80 | return nil, err 81 | } 82 | committee.Committee = make([]phase0.ValidatorIndex, len(committeeMembers)) 83 | for i := range committeeMembers { 84 | committee.Committee[i] = phase0.ValidatorIndex(committeeMembers[i]) 85 | } 86 | return committee, nil 87 | } 88 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/validators_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/attestantio/go-eth2-client/spec/phase0" 22 | "github.com/stretchr/testify/require" 23 | "github.com/wealdtech/chaind/services/chaindb" 24 | "github.com/wealdtech/chaind/services/chaindb/postgresql" 25 | ) 26 | 27 | func TestSetValidators(t *testing.T) { 28 | ctx := context.Background() 29 | s, err := postgresql.New(ctx, 30 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 31 | ) 32 | require.NoError(t, err) 33 | 34 | validator1 := &chaindb.Validator{ 35 | PublicKey: phase0.BLSPubKey{ 36 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 37 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 38 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 39 | }, 40 | Index: 1, 41 | EffectiveBalance: 32000000000, 42 | Slashed: false, 43 | ActivationEligibilityEpoch: 1, 44 | ActivationEpoch: 2, 45 | ExitEpoch: 3, 46 | WithdrawableEpoch: 4, 47 | } 48 | validator2 := &chaindb.Validator{ 49 | PublicKey: phase0.BLSPubKey{ 50 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 51 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 52 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 53 | }, 54 | Index: 2, 55 | EffectiveBalance: 0, 56 | Slashed: false, 57 | ActivationEligibilityEpoch: 0xffffffffffffffff, 58 | ActivationEpoch: 0xffffffffffffffff, 59 | ExitEpoch: 0xffffffffffffffff, 60 | WithdrawableEpoch: 0xffffffffffffffff, 61 | } 62 | 63 | // Attempt to set the validator without a transaction; should fail. 64 | require.Error(t, s.SetValidator(ctx, validator1)) 65 | 66 | ctx, cancel, err := s.BeginTx(ctx) 67 | require.NoError(t, err) 68 | defer cancel() 69 | 70 | // Set the validator. 71 | require.NoError(t, s.SetValidator(ctx, validator1)) 72 | // Update the validator. 73 | require.NoError(t, s.SetValidator(ctx, validator1)) 74 | 75 | // Ensure the correct data is returned. 76 | res, err := s.ValidatorsByPublicKey(ctx, []phase0.BLSPubKey{validator1.PublicKey}) 77 | require.NoError(t, err) 78 | require.Len(t, res, 1) 79 | require.Equal(t, validator1, res[validator1.PublicKey]) 80 | 81 | // Ensure that validator with far future epoch values is stored and returned. 82 | require.NoError(t, s.SetValidator(ctx, validator2)) 83 | res, err = s.ValidatorsByPublicKey(ctx, []phase0.BLSPubKey{validator2.PublicKey}) 84 | require.NoError(t, err) 85 | require.Len(t, res, 1) 86 | require.Equal(t, validator2, res[validator2.PublicKey]) 87 | } 88 | 89 | func TestValidators(t *testing.T) { 90 | ctx := context.Background() 91 | s, err := postgresql.New(ctx, 92 | postgresql.WithConnectionURL(os.Getenv("CHAINDB_URL")), 93 | ) 94 | require.NoError(t, err) 95 | 96 | validators, err := s.Validators(ctx) 97 | require.NoError(t, err) 98 | require.True(t, len(validators) > 0) 99 | } 100 | -------------------------------------------------------------------------------- /services/chaindb/postgresql/voluntaryexits.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package postgresql 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/wealdtech/chaind/services/chaindb" 20 | "go.opentelemetry.io/otel" 21 | ) 22 | 23 | // SetVoluntaryExit sets a voluntary exit. 24 | func (s *Service) SetVoluntaryExit(ctx context.Context, voluntaryExit *chaindb.VoluntaryExit) error { 25 | ctx, span := otel.Tracer("wealdtech.chaind.services.chaindb.postgresql").Start(ctx, "SetVoluntaryExit") 26 | defer span.End() 27 | 28 | tx := s.tx(ctx) 29 | if tx == nil { 30 | return ErrNoTransaction 31 | } 32 | 33 | _, err := tx.Exec(ctx, ` 34 | INSERT INTO t_voluntary_exits(f_inclusion_slot 35 | ,f_inclusion_block_root 36 | ,f_inclusion_index 37 | ,f_validator_index 38 | ,f_epoch 39 | ) 40 | VALUES($1,$2,$3,$4,$5) 41 | ON CONFLICT (f_inclusion_slot,f_inclusion_block_root,f_inclusion_index) DO 42 | UPDATE 43 | SET f_validator_index = excluded.f_validator_index 44 | ,f_epoch = excluded.f_epoch 45 | `, 46 | voluntaryExit.InclusionSlot, 47 | voluntaryExit.InclusionBlockRoot[:], 48 | voluntaryExit.InclusionIndex, 49 | voluntaryExit.ValidatorIndex, 50 | voluntaryExit.Epoch, 51 | ) 52 | 53 | return err 54 | } 55 | -------------------------------------------------------------------------------- /services/chaintime/mock/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package chaintime 15 | 16 | import ( 17 | "time" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/wealdtech/chaind/services/chaintime" 21 | ) 22 | 23 | type service struct{} 24 | 25 | // New creates a new mock chain time service. 26 | func New() chaintime.Service { 27 | return &service{} 28 | } 29 | 30 | // GenesisTime provides the time of the chain's genesis. 31 | func (s *service) GenesisTime() time.Time { return time.Time{} } 32 | 33 | // SlotDuration provides the duration of a single slot. 34 | func (s *service) SlotDuration() time.Duration { 35 | return 12 * time.Second 36 | } 37 | 38 | // SlotsPerEpoch provides the number of slots in an epoch. 39 | func (s *service) SlotsPerEpoch() uint64 { 40 | return 12 41 | } 42 | 43 | // StartOfSlot provides the time at which a given slot starts. 44 | func (s *service) StartOfSlot(_ phase0.Slot) time.Time { return time.Time{} } 45 | 46 | // StartOfEpoch provides the time at which a given epoch starts. 47 | func (s *service) StartOfEpoch(_ phase0.Epoch) time.Time { return time.Time{} } 48 | 49 | // CurrentSlot provides the current slot. 50 | func (s *service) CurrentSlot() phase0.Slot { 51 | return 0 52 | } 53 | 54 | // CurrentEpoch provides the current epoch. 55 | func (s *service) CurrentEpoch() phase0.Epoch { 56 | return 0 57 | } 58 | 59 | // CurrentSyncCommitteePeriod provides the current sync committee period. 60 | func (s *service) CurrentSyncCommitteePeriod() uint64 { 61 | return 0 62 | } 63 | 64 | // SlotToEpoch provides the epoch of the given slot. 65 | func (s *service) SlotToEpoch(_ phase0.Slot) phase0.Epoch { 66 | return 0 67 | } 68 | 69 | // SlotToSyncCommitteePeriod provides the sync committee period of the given slot. 70 | func (s *service) SlotToSyncCommitteePeriod(_ phase0.Slot) uint64 { 71 | return 0 72 | } 73 | 74 | // EpochToSyncCommitteePeriod provides the sync committee period of the given epoch. 75 | func (s *service) EpochToSyncCommitteePeriod(_ phase0.Epoch) uint64 { 76 | return 0 77 | } 78 | 79 | // FirstSlotOfEpoch provides the first slot of the given epoch. 80 | func (s *service) FirstSlotOfEpoch(_ phase0.Epoch) phase0.Slot { 81 | return 0 82 | } 83 | 84 | // LastSlotOfEpoch provides the last slot of the given epoch. 85 | func (s *service) LastSlotOfEpoch(_ phase0.Epoch) phase0.Slot { 86 | return 0 87 | } 88 | 89 | // TimestampToSlot provides the slot of the given timestamp. 90 | func (s *service) TimestampToSlot(_ time.Time) phase0.Slot { 91 | return 0 92 | } 93 | 94 | // TimestampToEpoch provides the epoch of the given timestamp. 95 | func (s *service) TimestampToEpoch(_ time.Time) phase0.Epoch { 96 | return 0 97 | } 98 | 99 | // FirstEpochOfSyncPeriod provides the first epoch of the given sync period. 100 | func (s *service) FirstEpochOfSyncPeriod(_ uint64) phase0.Epoch { 101 | return 0 102 | } 103 | 104 | // AltairInitialEpoch provides the epoch at which the Altair hard fork takes place. 105 | func (s *service) AltairInitialEpoch() phase0.Epoch { 106 | return 0 107 | } 108 | 109 | // AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place. 110 | func (s *service) AltairInitialSyncCommitteePeriod() uint64 { 111 | return 0 112 | } 113 | 114 | // BellatrixInitialEpoch provides the epoch at which the Bellatrix hard fork takes place. 115 | func (s *service) BellatrixInitialEpoch() phase0.Epoch { 116 | return 0 117 | } 118 | 119 | // CapellaInitialEpoch provides the epoch at which the Capella hard fork takes place. 120 | func (s *service) CapellaInitialEpoch() phase0.Epoch { 121 | return 0 122 | } 123 | -------------------------------------------------------------------------------- /services/chaintime/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020, 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package chaintime 15 | 16 | import ( 17 | "time" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | ) 21 | 22 | // Service provides a number of functions for calculating chain-related times. 23 | // 24 | //nolint:interfacebloat 25 | type Service interface { 26 | // GenesisTime provides the time of the chain's genesis. 27 | GenesisTime() time.Time 28 | // SlotDuration provides the duration of a single slot. 29 | SlotDuration() time.Duration 30 | // SlotsPerEpoch provides the number of slots in an epoch. 31 | SlotsPerEpoch() uint64 32 | // StartOfSlot provides the time at which a given slot starts. 33 | StartOfSlot(slot phase0.Slot) time.Time 34 | // StartOfEpoch provides the time at which a given epoch starts. 35 | StartOfEpoch(epoch phase0.Epoch) time.Time 36 | // CurrentSlot provides the current slot. 37 | CurrentSlot() phase0.Slot 38 | // CurrentEpoch provides the current epoch. 39 | CurrentEpoch() phase0.Epoch 40 | // CurrentSyncCommitteePeriod provides the current sync committee period. 41 | CurrentSyncCommitteePeriod() uint64 42 | // SlotToEpoch provides the epoch of the given slot. 43 | SlotToEpoch(slot phase0.Slot) phase0.Epoch 44 | // SlotToSyncCommitteePeriod provides the sync committee period of the given slot. 45 | SlotToSyncCommitteePeriod(slot phase0.Slot) uint64 46 | // EpochToSyncCommitteePeriod provides the sync committee period of the given epoch. 47 | EpochToSyncCommitteePeriod(epoch phase0.Epoch) uint64 48 | // FirstSlotOfEpoch provides the first slot of the given epoch. 49 | FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot 50 | // LastSlotOfEpoch provides the last slot of the given epoch. 51 | LastSlotOfEpoch(epoch phase0.Epoch) phase0.Slot 52 | // TimestampToSlot provides the slot of the given timestamp. 53 | TimestampToSlot(timestamp time.Time) phase0.Slot 54 | // TimestampToEpoch provides the epoch of the given timestamp. 55 | TimestampToEpoch(timestamp time.Time) phase0.Epoch 56 | // FirstEpochOfSyncPeriod provides the first epoch of the given sync period. 57 | FirstEpochOfSyncPeriod(period uint64) phase0.Epoch 58 | // AltairInitialEpoch provides the epoch at which the Altair hard fork takes place. 59 | AltairInitialEpoch() phase0.Epoch 60 | // AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place. 61 | AltairInitialSyncCommitteePeriod() uint64 62 | // BellatrixInitialEpoch provides the epoch at which the Bellatrix hard fork takes place. 63 | BellatrixInitialEpoch() phase0.Epoch 64 | // CapellaInitialEpoch provides the epoch at which the Capella hard fork takes place. 65 | CapellaInitialEpoch() phase0.Epoch 66 | } 67 | -------------------------------------------------------------------------------- /services/chaintime/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | eth2client "github.com/attestantio/go-eth2-client" 18 | "github.com/pkg/errors" 19 | "github.com/rs/zerolog" 20 | ) 21 | 22 | type parameters struct { 23 | logLevel zerolog.Level 24 | genesisProvider eth2client.GenesisProvider 25 | specProvider eth2client.SpecProvider 26 | forkScheduleProvider eth2client.ForkScheduleProvider 27 | } 28 | 29 | // Parameter is the interface for service parameters. 30 | type Parameter interface { 31 | apply(p *parameters) 32 | } 33 | 34 | type parameterFunc func(*parameters) 35 | 36 | func (f parameterFunc) apply(p *parameters) { 37 | f(p) 38 | } 39 | 40 | // WithLogLevel sets the log level for the module. 41 | func WithLogLevel(logLevel zerolog.Level) Parameter { 42 | return parameterFunc(func(p *parameters) { 43 | p.logLevel = logLevel 44 | }) 45 | } 46 | 47 | // WithGenesisProvider sets the genesis provider. 48 | func WithGenesisProvider(provider eth2client.GenesisProvider) Parameter { 49 | return parameterFunc(func(p *parameters) { 50 | p.genesisProvider = provider 51 | }) 52 | } 53 | 54 | // WithSpecProvider sets the spec provider. 55 | func WithSpecProvider(provider eth2client.SpecProvider) Parameter { 56 | return parameterFunc(func(p *parameters) { 57 | p.specProvider = provider 58 | }) 59 | } 60 | 61 | // WithForkScheduleProvider sets the fork schedule provider. 62 | func WithForkScheduleProvider(provider eth2client.ForkScheduleProvider) Parameter { 63 | return parameterFunc(func(p *parameters) { 64 | p.forkScheduleProvider = provider 65 | }) 66 | } 67 | 68 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 69 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 70 | parameters := parameters{ 71 | logLevel: zerolog.GlobalLevel(), 72 | } 73 | for _, p := range params { 74 | if params != nil { 75 | p.apply(¶meters) 76 | } 77 | } 78 | 79 | if parameters.specProvider == nil { 80 | return nil, errors.New("no spec provider specified") 81 | } 82 | if parameters.genesisProvider == nil { 83 | return nil, errors.New("no genesis provider specified") 84 | } 85 | if parameters.forkScheduleProvider == nil { 86 | return nil, errors.New("no fork schedule provider specified") 87 | } 88 | 89 | return ¶meters, nil 90 | } 91 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/blocknumber.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "net/url" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | type blockNumberResponse struct { 28 | Result string `json:"result"` 29 | } 30 | 31 | // blockNumber fetches the current block number from an Ethereum 1 client. 32 | func (s *Service) blockNumber(ctx context.Context) (uint64, error) { 33 | reference, err := url.Parse("") 34 | if err != nil { 35 | return 0, errors.Wrap(err, "invalid endpoint") 36 | } 37 | url := s.base.ResolveReference(reference).String() 38 | 39 | reqBody := bytes.NewBufferString(`{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1901}`) 40 | respBodyReader, err := s.post(ctx, url, reqBody) 41 | if err != nil { 42 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 43 | return 0, errors.Wrap(err, "request failed") 44 | } 45 | if respBodyReader == nil { 46 | return 0, errors.New("empty response") 47 | } 48 | 49 | var response blockNumberResponse 50 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 51 | return 0, errors.Wrap(err, "invalid response") 52 | } 53 | 54 | blockNumber, err := strconv.ParseUint(strings.TrimPrefix(response.Result, "0x"), 16, 64) 55 | if err != nil { 56 | return 0, errors.Wrap(err, "invalid block number") 57 | } 58 | 59 | return blockNumber, nil 60 | } 61 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/blocktimestampbyhash.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "net/url" 22 | "strconv" 23 | "strings" 24 | "time" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | type blockByHashResponse struct { 30 | Result *blockByHashBlockResponse `json:"result"` 31 | } 32 | type blockByHashBlockResponse struct { 33 | Timestamp string `json:"timestamp"` 34 | } 35 | 36 | // blockTimestampByHash fetches the timestamp of a block given its hash. 37 | func (s *Service) blockTimestampByHash(ctx context.Context, blockHash []byte) (time.Time, error) { 38 | reference, err := url.Parse("") 39 | if err != nil { 40 | return time.Time{}, errors.Wrap(err, "invalid endpoint") 41 | } 42 | url := s.base.ResolveReference(reference).String() 43 | 44 | reqBody := bytes.NewBufferString(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_getBlockByHash","params":["%#x",false],"id":1901}`, blockHash)) 45 | respBodyReader, err := s.post(ctx, url, reqBody) 46 | if err != nil { 47 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 48 | return time.Time{}, errors.Wrap(err, "request failed") 49 | } 50 | if respBodyReader == nil { 51 | return time.Time{}, errors.New("empty response") 52 | } 53 | 54 | var response blockByHashResponse 55 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 56 | return time.Time{}, errors.Wrap(err, "invalid response") 57 | } 58 | if response.Result == nil { 59 | return time.Time{}, errors.Wrap(err, "empty response") 60 | } 61 | 62 | timestamp, err := strconv.ParseInt(strings.TrimPrefix(response.Result.Timestamp, "0x"), 16, 64) 63 | if err != nil { 64 | return time.Time{}, errors.Wrap(err, "invalid timestamp") 65 | } 66 | 67 | return time.Unix(timestamp, 0), nil 68 | } 69 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/chainid.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "net/url" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/pkg/errors" 25 | ) 26 | 27 | type chainIDResponse struct { 28 | Result string `json:"result"` 29 | } 30 | 31 | // chainID fetches the current block number from an Ethereum 1 client. 32 | func (s *Service) chainID(ctx context.Context) (uint64, error) { 33 | reference, err := url.Parse("") 34 | if err != nil { 35 | return 0, errors.Wrap(err, "invalid endpoint") 36 | } 37 | url := s.base.ResolveReference(reference).String() 38 | 39 | reqBody := bytes.NewBufferString(`{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1901}`) 40 | respBodyReader, err := s.post(ctx, url, reqBody) 41 | if err != nil { 42 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 43 | return 0, errors.Wrap(err, "failed to request chain ID") 44 | } 45 | if respBodyReader == nil { 46 | return 0, errors.New("no response for chain ID") 47 | } 48 | 49 | var response chainIDResponse 50 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 51 | return 0, errors.Wrap(err, "failed to parse newFilter response") 52 | } 53 | 54 | chainID, err := strconv.ParseUint(strings.TrimPrefix(response.Result, "0x"), 16, 64) 55 | if err != nil { 56 | return 0, errors.Wrap(err, "failed to parse result") 57 | } 58 | 59 | return chainID, nil 60 | } 61 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/getlogs.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "net/url" 22 | 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | type getLogsResponse struct { 27 | Result []*logResponse `json:"result"` 28 | } 29 | 30 | // getLogs gets the logs for a range of blocks. 31 | func (s *Service) getLogs(ctx context.Context, startBlock uint64, endBlock uint64) ([]*logResponse, error) { 32 | reference, err := url.Parse("") 33 | if err != nil { 34 | return nil, errors.Wrap(err, "invalid endpoint") 35 | } 36 | url := s.base.ResolveReference(reference).String() 37 | 38 | reqBody := bytes.NewBufferString(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_getLogs","params":[{"address":["%#x"],"topics":["0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"],"fromBlock":"%#x","toBlock":"%#x"}],"id":11}`, s.depositContractAddress, startBlock, endBlock)) 39 | respBodyReader, err := s.post(ctx, url, reqBody) 40 | if err != nil { 41 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 42 | return nil, errors.Wrap(err, "request failed") 43 | } 44 | if respBodyReader == nil { 45 | return nil, errors.New("empty response") 46 | } 47 | 48 | var response getLogsResponse 49 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 50 | return nil, errors.Wrap(err, "invalid response") 51 | } 52 | log.Trace().Uint64("start_block", startBlock).Uint64("end_block", endBlock).Int("logs", len(response.Result)).Msg("Obtained logs") 53 | 54 | return response.Result, nil 55 | } 56 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/http.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "fmt" 20 | "io" 21 | "math/rand" 22 | "net/http" 23 | "net/url" 24 | 25 | "github.com/pkg/errors" 26 | ) 27 | 28 | // post sends an HTTP post request and returns the body. 29 | func (s *Service) post(ctx context.Context, endpoint string, body io.Reader) (io.Reader, error) { 30 | // #nosec G404 31 | log := log.With().Str("id", fmt.Sprintf("%02x", rand.Int31())).Logger() 32 | if e := log.Trace(); e.Enabled() { 33 | bodyBytes, err := io.ReadAll(body) 34 | if err != nil { 35 | return nil, errors.New("failed to read request body") 36 | } 37 | body = bytes.NewReader(bodyBytes) 38 | 39 | e.Str("endpoint", endpoint).Str("body", string(bodyBytes)).Msg("POST request") 40 | } 41 | 42 | reference, err := url.Parse(endpoint) 43 | if err != nil { 44 | return nil, errors.Wrap(err, "invalid endpoint") 45 | } 46 | url := s.base.ResolveReference(reference).String() 47 | 48 | opCtx, cancel := context.WithTimeout(ctx, s.timeout) 49 | req, err := http.NewRequestWithContext(opCtx, http.MethodPost, url, body) 50 | if err != nil { 51 | cancel() 52 | return nil, errors.Wrap(err, "failed to create POST request") 53 | } 54 | req.Header.Set("Content-Type", "application/json") 55 | req.Header.Set("Accept", "application/json") 56 | resp, err := s.client.Do(req) 57 | if err != nil { 58 | cancel() 59 | return nil, errors.Wrap(err, "failed to call POST endpoint") 60 | } 61 | // skipcq:GO-S2307 62 | defer resp.Body.Close() 63 | 64 | data, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | cancel() 67 | return nil, errors.Wrap(err, "failed to read POST response") 68 | } 69 | 70 | statusFamily := resp.StatusCode / 100 71 | if statusFamily != 2 { 72 | cancel() 73 | return nil, fmt.Errorf("POST failed with status %d: %s", resp.StatusCode, string(data)) 74 | } 75 | cancel() 76 | 77 | log.Trace().Str("response", string(data)).Msg("POST response") 78 | 79 | return bytes.NewReader(data), nil 80 | } 81 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/logresponse_internal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "encoding/json" 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestLogResponse(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | input []byte 27 | err string 28 | }{ 29 | { 30 | name: "Good", 31 | input: []byte(`{"address":"0x8c5fecdc472e27bc447696f431e425d02dd46a8c","topics":["0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5"],"data":"0x00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030b55446978b2d229265caceb97cb4d59c0187ba91fcf11675330c1a373f137fa3fb553acb663a0d83f5dbcdc17c9f4f92000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020005db2c8fb17330066824de63245948b3c2077f39a7e6bebb46ae93da8271148000000000000000000000000000000000000000000000000000000000000000800405973070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060b896411caf11780020b5656c5ebf0ff3ff245e4d679d9c6860e4ccbc695a672aa59d41c27b42bb9babf4c1b458e773c708fe4fce4cfe8ae43f9630a19c938d4c18165b5a3ff5f5e5dc2bd374a8dcfa531f3e189c1ba341cd511c4cd451c488d60000000000000000000000000000000000000000000000000000000000000008c58f010000000000000000000000000000000000000000000000000000000000","blockNumber":"0x39e9b3","transactionHash":"0x4428f17853c0237564eb7d97651fbb3390f444d223de5459799144cace695f91","transactionIndex":"0x0","blockHash":"0xfa3a6f5e2f5781bbdd4c68aa6ddd9ac3de8523188a9f8a71451007ad7f2c33c4","logIndex":"0x0","removed":false}`), 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | t.Run(test.name, func(t *testing.T) { 37 | var res logResponse 38 | err := json.Unmarshal(test.input, &res) 39 | if test.err != "" { 40 | require.EqualError(t, err, test.err) 41 | } else { 42 | require.NoError(t, err) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs_test 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/rs/zerolog" 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | zerolog.SetGlobalLevel(zerolog.Disabled) 25 | if os.Getenv("CHAINDB_URL") != "" && 26 | os.Getenv("EXECCLIENT_URL") != "" { 27 | os.Exit(m.Run()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // metadata stored about this service. 24 | type metadata struct { 25 | LatestBlock uint64 `json:"latest_block"` 26 | MissedBlocks []uint64 `json:"missed_blocks,omitempty"` 27 | } 28 | 29 | // metadataKey is the key for the metadata. 30 | var metadataKey = "eth1deposit.getlogs" 31 | 32 | // getMetadata gets metadata for this service. 33 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 34 | md := &metadata{} 35 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "failed to fetch metadata") 38 | } 39 | if mdJSON == nil { 40 | return md, nil 41 | } 42 | if err := json.Unmarshal(mdJSON, md); err != nil { 43 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 44 | } 45 | return md, nil 46 | } 47 | 48 | // setMetadata sets metadata for this service. 49 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 50 | mdJSON, err := json.Marshal(md) 51 | if err != nil { 52 | return errors.Wrap(err, "failed to marshal metadata") 53 | } 54 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 55 | return errors.Wrap(err, "failed to update metadata") 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/pkg/errors" 20 | "github.com/prometheus/client_golang/prometheus" 21 | "github.com/wealdtech/chaind/services/metrics" 22 | ) 23 | 24 | var metricsNamespace = "chaind_eth1deposits" 25 | 26 | var ( 27 | highestBlock uint64 28 | latestBlock prometheus.Gauge 29 | blocksProcessed prometheus.Gauge 30 | ) 31 | 32 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 33 | if latestBlock != nil { 34 | // Already registered. 35 | return nil 36 | } 37 | if monitor == nil { 38 | // No monitor. 39 | return nil 40 | } 41 | if monitor.Presenter() == "prometheus" { 42 | return registerPrometheusMetrics() 43 | } 44 | return nil 45 | } 46 | 47 | func registerPrometheusMetrics() error { 48 | latestBlock = prometheus.NewGauge(prometheus.GaugeOpts{ 49 | Namespace: metricsNamespace, 50 | Name: "latest_block", 51 | Help: "Latest Ethereum 1 block processed", 52 | }) 53 | if err := prometheus.Register(latestBlock); err != nil { 54 | return errors.Wrap(err, "failed to register latest_block") 55 | } 56 | 57 | blocksProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 58 | Namespace: metricsNamespace, 59 | Name: "blocks_processed", 60 | Help: "Number of Ethereum 1 blocks processed", 61 | }) 62 | if err := prometheus.Register(blocksProcessed); err != nil { 63 | return errors.Wrap(err, "failed to register blocks_processed") 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func monitorBlockProcessed(block uint64) { 70 | if blocksProcessed != nil { 71 | blocksProcessed.Inc() 72 | if block > highestBlock { 73 | latestBlock.Set(float64(block)) 74 | highestBlock = block 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "strconv" 18 | 19 | "github.com/pkg/errors" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | type parameters struct { 26 | logLevel zerolog.Level 27 | monitor metrics.Service 28 | connectionURL string 29 | chainDB chaindb.Service 30 | eth1DepositsSetter chaindb.ETH1DepositsSetter 31 | eth1Confirmations uint64 32 | startBlock string 33 | } 34 | 35 | // Parameter is the interface for service parameters. 36 | type Parameter interface { 37 | apply(p *parameters) 38 | } 39 | 40 | type parameterFunc func(*parameters) 41 | 42 | func (f parameterFunc) apply(p *parameters) { 43 | f(p) 44 | } 45 | 46 | // WithLogLevel sets the log level for the module. 47 | func WithLogLevel(logLevel zerolog.Level) Parameter { 48 | return parameterFunc(func(p *parameters) { 49 | p.logLevel = logLevel 50 | }) 51 | } 52 | 53 | // WithMonitor sets the monitor for the module. 54 | func WithMonitor(monitor metrics.Service) Parameter { 55 | return parameterFunc(func(p *parameters) { 56 | p.monitor = monitor 57 | }) 58 | } 59 | 60 | // WithChainDB sets the chain database service for this module. 61 | func WithChainDB(chainDB chaindb.Service) Parameter { 62 | return parameterFunc(func(p *parameters) { 63 | p.chainDB = chainDB 64 | }) 65 | } 66 | 67 | // WithETH1DepositsSetter sets the Ethereum 1 deposits setter for this module. 68 | func WithETH1DepositsSetter(setter chaindb.ETH1DepositsSetter) Parameter { 69 | return parameterFunc(func(p *parameters) { 70 | p.eth1DepositsSetter = setter 71 | }) 72 | } 73 | 74 | // WithETH1Confirmations sets the number of confirmations we wait for before processing. 75 | func WithETH1Confirmations(confirmations uint64) Parameter { 76 | return parameterFunc(func(p *parameters) { 77 | p.eth1Confirmations = confirmations 78 | }) 79 | } 80 | 81 | // WithConnectionURL sets the Ethereum 1 connection URL service for this module. 82 | func WithConnectionURL(url string) Parameter { 83 | return parameterFunc(func(p *parameters) { 84 | p.connectionURL = url 85 | }) 86 | } 87 | 88 | // WithStartBlock sets the start block for this module. 89 | func WithStartBlock(block string) Parameter { 90 | return parameterFunc(func(p *parameters) { 91 | p.startBlock = block 92 | }) 93 | } 94 | 95 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 96 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 97 | parameters := parameters{ 98 | logLevel: zerolog.GlobalLevel(), 99 | eth1Confirmations: 12, // Default number of confirmations. 100 | } 101 | for _, p := range params { 102 | if params != nil { 103 | p.apply(¶meters) 104 | } 105 | } 106 | 107 | if parameters.chainDB == nil { 108 | return nil, errors.New("no chain database specified") 109 | } 110 | if parameters.eth1DepositsSetter == nil { 111 | return nil, errors.New("no Ethereum 1 deposits setter specified") 112 | } 113 | if parameters.connectionURL == "" { 114 | return nil, errors.New("no connection URL specified") 115 | } 116 | if parameters.startBlock != "" { 117 | _, err := strconv.ParseInt(parameters.startBlock, 10, 64) 118 | if err != nil { 119 | return nil, errors.Wrap(err, "invalid start block specified") 120 | } 121 | } 122 | 123 | return ¶meters, nil 124 | } 125 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/rs/zerolog" 22 | "github.com/stretchr/testify/require" 23 | postgresqlchaindb "github.com/wealdtech/chaind/services/chaindb/postgresql" 24 | "github.com/wealdtech/chaind/services/eth1deposits/getlogs" 25 | ) 26 | 27 | func TestService(t *testing.T) { 28 | ctx := context.Background() 29 | 30 | chainDB, err := postgresqlchaindb.New(ctx, 31 | postgresqlchaindb.WithLogLevel(zerolog.Disabled), 32 | postgresqlchaindb.WithConnectionURL(os.Getenv("CHAINDB_URL")), 33 | ) 34 | require.NoError(t, err) 35 | 36 | tests := []struct { 37 | name string 38 | params []getlogs.Parameter 39 | err string 40 | }{ 41 | { 42 | name: "ChainDBMissing", 43 | params: []getlogs.Parameter{ 44 | getlogs.WithLogLevel(zerolog.Disabled), 45 | getlogs.WithETH1DepositsSetter(chainDB), 46 | getlogs.WithConnectionURL(os.Getenv("EXECCLIENT_URL")), 47 | }, 48 | err: "problem with parameters: no chain database specified", 49 | }, 50 | { 51 | name: "ConnectionURLMissing", 52 | params: []getlogs.Parameter{ 53 | getlogs.WithLogLevel(zerolog.Disabled), 54 | getlogs.WithChainDB(chainDB), 55 | getlogs.WithETH1DepositsSetter(chainDB), 56 | }, 57 | err: "problem with parameters: no connection URL specified", 58 | }, 59 | { 60 | name: "Good", 61 | params: []getlogs.Parameter{ 62 | getlogs.WithLogLevel(zerolog.Disabled), 63 | getlogs.WithChainDB(chainDB), 64 | getlogs.WithETH1DepositsSetter(chainDB), 65 | getlogs.WithConnectionURL(os.Getenv("EXECCLIENT_URL")), 66 | }, 67 | }, 68 | } 69 | 70 | for _, test := range tests { 71 | t.Run(test.name, func(t *testing.T) { 72 | _, err := getlogs.New(ctx, test.params...) 73 | if test.err != "" { 74 | require.EqualError(t, err, test.err) 75 | } else { 76 | require.NoError(t, err) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/transaction.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | type transaction struct { 26 | GasPrice uint64 27 | } 28 | 29 | //nolint:tagliatelle 30 | type transactionJSON struct { 31 | GasPrice string `json:"gasPrice"` 32 | } 33 | 34 | // UnmarshalJSON implements json.Unmarshaler. 35 | func (t *transaction) UnmarshalJSON(input []byte) error { 36 | var transactionJSON transactionJSON 37 | var err error 38 | if err := json.Unmarshal(input, &transactionJSON); err != nil { 39 | return errors.Wrap(err, "invalid JSON") 40 | } 41 | 42 | if transactionJSON.GasPrice == "" { 43 | return errors.New("gas price missing") 44 | } 45 | t.GasPrice, err = strconv.ParseUint(strings.TrimPrefix(transactionJSON.GasPrice, "0x"), 16, 64) 46 | if err != nil { 47 | return errors.Wrap(err, "invalid format for gas price") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // MarshalJSON implements json.Marshaler. 54 | func (t *transaction) MarshalJSON() ([]byte, error) { 55 | return json.Marshal(&transactionJSON{ 56 | GasPrice: fmt.Sprintf("%#x", t.GasPrice), 57 | }) 58 | } 59 | 60 | // String returns a string version of the structure. 61 | func (t *transaction) String() string { 62 | data, err := json.Marshal(t) 63 | if err != nil { 64 | return fmt.Sprintf("ERR: %v", err) 65 | } 66 | return string(data) 67 | } 68 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/transactionbyhash.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "net/url" 22 | 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | type transactionByHashResponse struct { 27 | Result *transaction `json:"result"` 28 | } 29 | 30 | // transactionByHash fetches a transaction receipt given its hash. 31 | func (s *Service) transactionByHash(ctx context.Context, txHash []byte) (*transaction, error) { 32 | reference, err := url.Parse("") 33 | if err != nil { 34 | return nil, errors.Wrap(err, "invalid endpoint") 35 | } 36 | url := s.base.ResolveReference(reference).String() 37 | 38 | reqBody := bytes.NewBufferString(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_getTransactionByHash","params":["%#x"],"id":1901}`, txHash)) 39 | respBodyReader, err := s.post(ctx, url, reqBody) 40 | if err != nil { 41 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 42 | return nil, errors.Wrap(err, "request failed") 43 | } 44 | if respBodyReader == nil { 45 | return nil, errors.New("empty response") 46 | } 47 | 48 | var response transactionByHashResponse 49 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 50 | return nil, errors.Wrap(err, "invalid response") 51 | } 52 | 53 | return response.Result, nil 54 | } 55 | -------------------------------------------------------------------------------- /services/eth1deposits/getlogs/transactionreceiptbyhash.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package getlogs 15 | 16 | import ( 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "net/url" 22 | 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | type transactionReceiptByHashResponse struct { 27 | Result *transactionReceipt `json:"result"` 28 | } 29 | 30 | // transactionReceiptByHash fetches a transaction receipt given its hash. 31 | func (s *Service) transactionReceiptByHash(ctx context.Context, txHash []byte) (*transactionReceipt, error) { 32 | reference, err := url.Parse("") 33 | if err != nil { 34 | return nil, errors.Wrap(err, "invalid endpoint") 35 | } 36 | url := s.base.ResolveReference(reference).String() 37 | 38 | reqBody := bytes.NewBufferString(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_getTransactionReceipt","params":["%#x"],"id":1901}`, txHash)) 39 | respBodyReader, err := s.post(ctx, url, reqBody) 40 | if err != nil { 41 | log.Trace().Str("url", url).Err(err).Msg("Request failed") 42 | return nil, errors.Wrap(err, "request failed") 43 | } 44 | if respBodyReader == nil { 45 | return nil, errors.New("empty response") 46 | } 47 | 48 | var response transactionReceiptByHashResponse 49 | if err := json.NewDecoder(respBodyReader).Decode(&response); err != nil { 50 | return nil, errors.Wrap(err, "invalid response") 51 | } 52 | 53 | return response.Result, nil 54 | } 55 | -------------------------------------------------------------------------------- /services/finalizer/standard/handler_internal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/attestantio/go-eth2-client/http" 22 | "github.com/attestantio/go-eth2-client/spec/phase0" 23 | "github.com/rs/zerolog" 24 | "github.com/stretchr/testify/require" 25 | standardblocks "github.com/wealdtech/chaind/services/blocks/standard" 26 | postgresqlchaindb "github.com/wealdtech/chaind/services/chaindb/postgresql" 27 | standardchaintime "github.com/wealdtech/chaind/services/chaintime/standard" 28 | ) 29 | 30 | func TestUpdateAttestationHeadCorrect(t *testing.T) { 31 | ctx := context.Background() 32 | 33 | chainDB, err := postgresqlchaindb.New(ctx, 34 | postgresqlchaindb.WithLogLevel(zerolog.Disabled), 35 | postgresqlchaindb.WithConnectionURL(os.Getenv("CHAINDB_URL")), 36 | ) 37 | require.NoError(t, err) 38 | 39 | chainTime, err := standardchaintime.New(ctx, 40 | standardchaintime.WithGenesisProvider(chainDB), 41 | standardchaintime.WithSpecProvider(chainDB), 42 | standardchaintime.WithForkScheduleProvider(chainDB), 43 | ) 44 | require.NoError(t, err) 45 | 46 | consensusClient, err := http.New(ctx, 47 | http.WithAddress(os.Getenv("ETH2CLIENT_ADDRESS")), 48 | ) 49 | require.NoError(t, err) 50 | 51 | blocks, err := standardblocks.New(ctx, 52 | standardblocks.WithChainDB(chainDB), 53 | standardblocks.WithChainTime(chainTime), 54 | standardblocks.WithETH2Client(consensusClient), 55 | ) 56 | require.NoError(t, err) 57 | 58 | s, err := New(ctx, 59 | WithChainDB(chainDB), 60 | WithChainTime(chainTime), 61 | WithETH2Client(consensusClient), 62 | WithBlocks(blocks), 63 | WithLogLevel(zerolog.TraceLevel), 64 | ) 65 | require.NoError(t, err) 66 | 67 | tests := []struct { 68 | name string 69 | slot phase0.Slot 70 | validator phase0.ValidatorIndex 71 | headCorrect bool 72 | }{ 73 | { 74 | name: "329828-7", 75 | slot: 329828, 76 | validator: 7, 77 | headCorrect: true, 78 | }, 79 | } 80 | 81 | for _, test := range tests { 82 | t.Run(test.name, func(t *testing.T) { 83 | attestations, err := chainDB.AttestationsForSlotRange(ctx, test.slot, test.slot+1) 84 | require.NoError(t, err) 85 | 86 | for _, attestation := range attestations { 87 | for _, validatorIndex := range attestation.AggregationIndices { 88 | if validatorIndex == test.validator { 89 | err = s.updateAttestationHeadCorrect(ctx, attestation, make(map[phase0.Slot]phase0.Root)) 90 | require.NoError(t, err) 91 | require.NotNil(t, attestation.HeadCorrect) 92 | require.Equal(t, test.headCorrect, *attestation.HeadCorrect) 93 | } 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /services/finalizer/standard/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard_test 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/rs/zerolog" 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | zerolog.SetGlobalLevel(zerolog.Disabled) 25 | if os.Getenv("CHAINDB_URL") != "" && 26 | os.Getenv("ETH2CLIENT_ADDRESS") != "" { 27 | os.Exit(m.Run()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/finalizer/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // metadata stored about this service. 24 | type metadata struct { 25 | LastFinalizedEpoch int64 `json:"latest_epoch"` 26 | LatestCanonicalSlot int64 `json:"latest_canonical_slot"` 27 | MissedEpochs []int64 `json:"missed_epochs,omitempty"` 28 | } 29 | 30 | // metadataKey is the key for the metadata. 31 | var metadataKey = "finalizer.standard" 32 | 33 | // getMetadata gets metadata for this service. 34 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 35 | md := &metadata{ 36 | LastFinalizedEpoch: -1, 37 | LatestCanonicalSlot: -1, 38 | } 39 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 40 | if err != nil { 41 | return nil, errors.Wrap(err, "failed to fetch metadata") 42 | } 43 | if mdJSON == nil { 44 | return md, nil 45 | } 46 | if err := json.Unmarshal(mdJSON, md); err != nil { 47 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 48 | } 49 | return md, nil 50 | } 51 | 52 | // setMetadata sets metadata for this service. 53 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 54 | mdJSON, err := json.Marshal(md) 55 | if err != nil { 56 | return errors.Wrap(err, "failed to marshal metadata") 57 | } 58 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 59 | return errors.Wrap(err, "failed to update metadata") 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /services/finalizer/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021, 2022 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | var metricsNamespace = "chaind_finalizer" 26 | 27 | var ( 28 | highestEpoch phase0.Epoch 29 | latestEpoch prometheus.Gauge 30 | epochsProcessed prometheus.Gauge 31 | ) 32 | 33 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 34 | if latestEpoch != nil { 35 | // Already registered. 36 | return nil 37 | } 38 | if monitor == nil { 39 | // No monitor. 40 | return nil 41 | } 42 | if monitor.Presenter() == "prometheus" { 43 | return registerPrometheusMetrics() 44 | } 45 | return nil 46 | } 47 | 48 | func registerPrometheusMetrics() error { 49 | latestEpoch = prometheus.NewGauge(prometheus.GaugeOpts{ 50 | Namespace: metricsNamespace, 51 | Name: "latest_epoch", 52 | Help: "Latest epoch processed for finalizer", 53 | }) 54 | if err := prometheus.Register(latestEpoch); err != nil { 55 | return errors.Wrap(err, "failed to register latest_epoch") 56 | } 57 | 58 | epochsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 59 | Namespace: metricsNamespace, 60 | Name: "epochs_processed", 61 | Help: "Number of epochs processed", 62 | }) 63 | if err := prometheus.Register(epochsProcessed); err != nil { 64 | return errors.Wrap(err, "failed to register epochs_processed") 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // monitorLatestEpoch sets the latest epoch without registering an 71 | // increase in epochs processed. This does not usually need to be 72 | // called directly, as it is called as part ofr monitorEpochProcessed. 73 | func monitorLatestEpoch(epoch phase0.Epoch) { 74 | highestEpoch = epoch 75 | if latestEpoch != nil { 76 | latestEpoch.Set(float64(epoch)) 77 | } 78 | } 79 | 80 | func monitorEpochProcessed(epoch phase0.Epoch) { 81 | if epochsProcessed != nil { 82 | epochsProcessed.Inc() 83 | if epoch > highestEpoch { 84 | monitorLatestEpoch(epoch) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /services/finalizer/standard/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/attestantio/go-eth2-client/http" 22 | "github.com/rs/zerolog" 23 | "github.com/stretchr/testify/require" 24 | mockblocks "github.com/wealdtech/chaind/services/blocks/mock" 25 | postgresqlchaindb "github.com/wealdtech/chaind/services/chaindb/postgresql" 26 | standardchaintime "github.com/wealdtech/chaind/services/chaintime/standard" 27 | "github.com/wealdtech/chaind/services/finalizer/standard" 28 | ) 29 | 30 | func TestService(t *testing.T) { 31 | ctx := context.Background() 32 | 33 | chainDB, err := postgresqlchaindb.New(ctx, 34 | postgresqlchaindb.WithLogLevel(zerolog.Disabled), 35 | postgresqlchaindb.WithConnectionURL(os.Getenv("CHAINDB_URL")), 36 | ) 37 | require.NoError(t, err) 38 | 39 | consensusClient, err := http.New(ctx, 40 | http.WithAddress(os.Getenv("ETH2CLIENT_ADDRESS")), 41 | ) 42 | require.NoError(t, err) 43 | 44 | chainTime, err := standardchaintime.New(ctx, 45 | standardchaintime.WithGenesisProvider(chainDB), 46 | standardchaintime.WithSpecProvider(chainDB), 47 | standardchaintime.WithForkScheduleProvider(chainDB), 48 | ) 49 | require.NoError(t, err) 50 | 51 | blocks := mockblocks.New() 52 | 53 | tests := []struct { 54 | name string 55 | params []standard.Parameter 56 | err string 57 | }{ 58 | { 59 | name: "ChainDBMissing", 60 | params: []standard.Parameter{ 61 | standard.WithLogLevel(zerolog.Disabled), 62 | standard.WithChainTime(chainTime), 63 | standard.WithETH2Client(consensusClient), 64 | standard.WithBlocks(blocks), 65 | }, 66 | err: "problem with parameters: no chain database specified", 67 | }, 68 | { 69 | name: "ChainTimeMissing", 70 | params: []standard.Parameter{ 71 | standard.WithLogLevel(zerolog.Disabled), 72 | standard.WithChainDB(chainDB), 73 | standard.WithETH2Client(consensusClient), 74 | standard.WithBlocks(blocks), 75 | }, 76 | err: "problem with parameters: no chain time specified", 77 | }, 78 | { 79 | name: "ETH2ClientMissing", 80 | params: []standard.Parameter{ 81 | standard.WithLogLevel(zerolog.Disabled), 82 | standard.WithChainDB(chainDB), 83 | standard.WithChainTime(chainTime), 84 | standard.WithBlocks(blocks), 85 | }, 86 | err: "problem with parameters: no Ethereum 2 client specified", 87 | }, 88 | { 89 | name: "BlocksMissing", 90 | params: []standard.Parameter{ 91 | standard.WithLogLevel(zerolog.Disabled), 92 | standard.WithChainDB(chainDB), 93 | standard.WithChainTime(chainTime), 94 | standard.WithETH2Client(consensusClient), 95 | }, 96 | err: "problem with parameters: no blocks specified", 97 | }, 98 | { 99 | name: "Good", 100 | params: []standard.Parameter{ 101 | standard.WithLogLevel(zerolog.Disabled), 102 | standard.WithChainDB(chainDB), 103 | standard.WithChainTime(chainTime), 104 | standard.WithETH2Client(consensusClient), 105 | standard.WithBlocks(blocks), 106 | }, 107 | }, 108 | } 109 | 110 | for _, test := range tests { 111 | t.Run(test.name, func(t *testing.T) { 112 | _, err := standard.New(context.Background(), test.params...) 113 | if test.err != "" { 114 | require.EqualError(t, err, test.err) 115 | } else { 116 | require.NoError(t, err) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /services/metrics/null/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package null 15 | 16 | // Service is a metrics service that drops metrics. 17 | type Service struct{} 18 | 19 | // Presenter provides the presenter for this service. 20 | func (s *Service) Presenter() string { 21 | return "null" 22 | } 23 | -------------------------------------------------------------------------------- /services/metrics/prometheus/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package prometheus 15 | 16 | import ( 17 | "errors" 18 | 19 | "github.com/rs/zerolog" 20 | ) 21 | 22 | type parameters struct { 23 | logLevel zerolog.Level 24 | address string 25 | } 26 | 27 | // Parameter is the interface for service parameters. 28 | type Parameter interface { 29 | apply(p *parameters) 30 | } 31 | 32 | type parameterFunc func(*parameters) 33 | 34 | func (f parameterFunc) apply(p *parameters) { 35 | f(p) 36 | } 37 | 38 | // WithLogLevel sets the log level for the module. 39 | func WithLogLevel(logLevel zerolog.Level) Parameter { 40 | return parameterFunc(func(p *parameters) { 41 | p.logLevel = logLevel 42 | }) 43 | } 44 | 45 | // WithAddress sets the address. 46 | func WithAddress(address string) Parameter { 47 | return parameterFunc(func(p *parameters) { 48 | p.address = address 49 | }) 50 | } 51 | 52 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 53 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 54 | parameters := parameters{ 55 | logLevel: zerolog.GlobalLevel(), 56 | } 57 | for _, p := range params { 58 | if params != nil { 59 | p.apply(¶meters) 60 | } 61 | } 62 | 63 | if parameters.address == "" { 64 | return nil, errors.New("no address specified") 65 | } 66 | 67 | return ¶meters, nil 68 | } 69 | -------------------------------------------------------------------------------- /services/metrics/prometheus/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package prometheus 15 | 16 | import ( 17 | "context" 18 | "net/http" 19 | "time" 20 | 21 | "github.com/pkg/errors" 22 | "github.com/prometheus/client_golang/prometheus/promhttp" 23 | "github.com/rs/zerolog" 24 | zerologger "github.com/rs/zerolog/log" 25 | ) 26 | 27 | // Service is a metrics service exposing metrics via prometheus. 28 | type Service struct{} 29 | 30 | // module-wide log. 31 | var log zerolog.Logger 32 | 33 | // New creates a new prometheus metrics service. 34 | func New(_ context.Context, params ...Parameter) (*Service, error) { 35 | parameters, err := parseAndCheckParameters(params...) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "problem with parameters") 38 | } 39 | 40 | // Set logging. 41 | log = zerologger.With().Str("service", "metrics").Str("impl", "prometheus").Logger() 42 | if parameters.logLevel != log.GetLevel() { 43 | log = log.Level(parameters.logLevel) 44 | } 45 | 46 | s := &Service{} 47 | 48 | go func() { 49 | http.Handle("/metrics", promhttp.Handler()) 50 | server := &http.Server{ 51 | Addr: parameters.address, 52 | ReadHeaderTimeout: 5 * time.Second, 53 | } 54 | if err := server.ListenAndServe(); err != nil { 55 | log.Warn().Str("metrics_address", parameters.address).Err(err).Msg("Failed to run metrics server") 56 | } 57 | }() 58 | 59 | return s, nil 60 | } 61 | 62 | // Presenter returns the presenter for the events. 63 | func (*Service) Presenter() string { 64 | return "prometheus" 65 | } 66 | -------------------------------------------------------------------------------- /services/metrics/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package metrics provides an interface to present metrics. 15 | package metrics 16 | 17 | // Service is the generic metrics service. 18 | type Service interface { 19 | // Presenter provides the presenter for this service. 20 | Presenter() string 21 | } 22 | -------------------------------------------------------------------------------- /services/proposerduties/standard/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/attestantio/go-eth2-client/api" 21 | "github.com/attestantio/go-eth2-client/spec/phase0" 22 | "github.com/pkg/errors" 23 | "github.com/wealdtech/chaind/services/chaindb" 24 | ) 25 | 26 | // OnBeaconChainHeadUpdated receives beacon chain head updated notifications. 27 | func (s *Service) OnBeaconChainHeadUpdated( 28 | ctx context.Context, 29 | slot phase0.Slot, 30 | _ phase0.Root, 31 | _ phase0.Root, 32 | // skipcq: RVV-A0005 33 | epochTransition bool, 34 | ) { 35 | if !epochTransition { 36 | // Only interested in epoch transitions. 37 | return 38 | } 39 | 40 | // Only allow 1 handler to be active. 41 | acquired := s.activitySem.TryAcquire(1) 42 | if !acquired { 43 | log.Debug().Msg("Another handler running") 44 | return 45 | } 46 | 47 | epoch := s.chainTime.SlotToEpoch(slot) 48 | log := log.With().Uint64("epoch", uint64(epoch)).Logger() 49 | 50 | md, err := s.getMetadata(ctx) 51 | if err != nil { 52 | s.activitySem.Release(1) 53 | log.Error().Err(err).Msg("Failed to obtain metadata") 54 | return 55 | } 56 | 57 | s.catchup(ctx, md) 58 | s.activitySem.Release(1) 59 | } 60 | 61 | func (s *Service) updateProposerDutiesForEpoch(ctx context.Context, epoch phase0.Epoch) error { 62 | dutiesResponse, err := s.eth2Client.(eth2client.ProposerDutiesProvider).ProposerDuties(ctx, &api.ProposerDutiesOpts{ 63 | Epoch: epoch, 64 | }) 65 | if err != nil { 66 | return errors.Wrap(err, "failed to fetch proposer duties") 67 | } 68 | duties := dutiesResponse.Data 69 | 70 | log.Trace().Uint64("epoch", uint64(epoch)).Msg("Setting proposer duties") 71 | for _, duty := range duties { 72 | dbProposerDuty := &chaindb.ProposerDuty{ 73 | Slot: duty.Slot, 74 | ValidatorIndex: duty.ValidatorIndex, 75 | } 76 | if err := s.proposerDutiesSetter.SetProposerDuty(ctx, dbProposerDuty); err != nil { 77 | return errors.Wrap(err, "failed to set proposer duty") 78 | } 79 | } 80 | 81 | monitorEpochProcessed(epoch) 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /services/proposerduties/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/attestantio/go-eth2-client/spec/phase0" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | // metadata stored about this service. 25 | type metadata struct { 26 | LatestEpoch int64 `json:"latest_epoch"` 27 | MissedEpochs []phase0.Epoch `json:"missed_epochs,omitempty"` 28 | } 29 | 30 | // metadataKey is the key for the metadata. 31 | var metadataKey = "proposerduties.standard" 32 | 33 | // getMetadata gets metadata for this service. 34 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 35 | md := &metadata{ 36 | LatestEpoch: -1, 37 | } 38 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "failed to fetch metadata") 41 | } 42 | if mdJSON == nil { 43 | return md, nil 44 | } 45 | if err := json.Unmarshal(mdJSON, md); err != nil { 46 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 47 | } 48 | return md, nil 49 | } 50 | 51 | // setMetadata sets metadata for this service. 52 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 53 | mdJSON, err := json.Marshal(md) 54 | if err != nil { 55 | return errors.Wrap(err, "failed to marshal metadata") 56 | } 57 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 58 | return errors.Wrap(err, "failed to update metadata") 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /services/proposerduties/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | var metricsNamespace = "chaind_proposerduties" 26 | 27 | var ( 28 | highestEpoch phase0.Epoch 29 | latestEpoch prometheus.Gauge 30 | epochsProcessed prometheus.Gauge 31 | ) 32 | 33 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 34 | if latestEpoch != nil { 35 | // Already registered. 36 | return nil 37 | } 38 | if monitor == nil { 39 | // No monitor. 40 | return nil 41 | } 42 | if monitor.Presenter() == "prometheus" { 43 | return registerPrometheusMetrics() 44 | } 45 | return nil 46 | } 47 | 48 | func registerPrometheusMetrics() error { 49 | latestEpoch = prometheus.NewGauge(prometheus.GaugeOpts{ 50 | Namespace: metricsNamespace, 51 | Name: "latest_epoch", 52 | Help: "Latest epoch processed for proposer duties", 53 | }) 54 | if err := prometheus.Register(latestEpoch); err != nil { 55 | return errors.Wrap(err, "failed to register latest_epoch") 56 | } 57 | 58 | epochsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 59 | Namespace: metricsNamespace, 60 | Name: "epochs_processed", 61 | Help: "Number of epochs processed", 62 | }) 63 | if err := prometheus.Register(epochsProcessed); err != nil { 64 | return errors.Wrap(err, "failed to register epochs_processed") 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func monitorEpochProcessed(epoch phase0.Epoch) { 71 | if epochsProcessed != nil { 72 | epochsProcessed.Inc() 73 | if epoch > highestEpoch { 74 | latestEpoch.Set(float64(epoch)) 75 | highestEpoch = epoch 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /services/proposerduties/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "errors" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/chaintime" 23 | "github.com/wealdtech/chaind/services/metrics" 24 | ) 25 | 26 | type parameters struct { 27 | logLevel zerolog.Level 28 | monitor metrics.Service 29 | eth2Client eth2client.Service 30 | chainDB chaindb.Service 31 | chainTime chaintime.Service 32 | startEpoch int64 33 | } 34 | 35 | // Parameter is the interface for service parameters. 36 | type Parameter interface { 37 | apply(p *parameters) 38 | } 39 | 40 | type parameterFunc func(*parameters) 41 | 42 | func (f parameterFunc) apply(p *parameters) { 43 | f(p) 44 | } 45 | 46 | // WithLogLevel sets the log level for the module. 47 | func WithLogLevel(logLevel zerolog.Level) Parameter { 48 | return parameterFunc(func(p *parameters) { 49 | p.logLevel = logLevel 50 | }) 51 | } 52 | 53 | // WithMonitor sets the monitor for the module. 54 | func WithMonitor(monitor metrics.Service) Parameter { 55 | return parameterFunc(func(p *parameters) { 56 | p.monitor = monitor 57 | }) 58 | } 59 | 60 | // WithETH2Client sets the Ethereum 2 client for this module. 61 | func WithETH2Client(eth2Client eth2client.Service) Parameter { 62 | return parameterFunc(func(p *parameters) { 63 | p.eth2Client = eth2Client 64 | }) 65 | } 66 | 67 | // WithChainDB sets the chain database for this module. 68 | func WithChainDB(chainDB chaindb.Service) Parameter { 69 | return parameterFunc(func(p *parameters) { 70 | p.chainDB = chainDB 71 | }) 72 | } 73 | 74 | // WithChainTime sets the chain time service for this module. 75 | func WithChainTime(chainTime chaintime.Service) Parameter { 76 | return parameterFunc(func(p *parameters) { 77 | p.chainTime = chainTime 78 | }) 79 | } 80 | 81 | // WithStartEpoch sets the start epoch for this module. 82 | func WithStartEpoch(startEpoch int64) Parameter { 83 | return parameterFunc(func(p *parameters) { 84 | p.startEpoch = startEpoch 85 | }) 86 | } 87 | 88 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 89 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 90 | parameters := parameters{ 91 | logLevel: zerolog.GlobalLevel(), 92 | startEpoch: -1, 93 | } 94 | for _, p := range params { 95 | if params != nil { 96 | p.apply(¶meters) 97 | } 98 | } 99 | 100 | if parameters.eth2Client == nil { 101 | return nil, errors.New("no Ethereum 2 client specified") 102 | } 103 | if parameters.chainDB == nil { 104 | return nil, errors.New("no chain database specified") 105 | } 106 | if parameters.chainTime == nil { 107 | return nil, errors.New("no chain time specified") 108 | } 109 | 110 | return ¶meters, nil 111 | } 112 | -------------------------------------------------------------------------------- /services/scheduler/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package scheduler 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "time" 20 | ) 21 | 22 | // JobFunc is the type for jobs. 23 | type JobFunc func(context.Context, any) 24 | 25 | // RuntimeFunc is the type of a function that generates the next runtime. 26 | type RuntimeFunc func(context.Context, any) (time.Time, error) 27 | 28 | // ErrNoMoreInstances is returned by the runtime generator when it has no more instances. 29 | var ErrNoMoreInstances = errors.New("no more instances") 30 | 31 | // ErrNoSuchJob is returned when the scheduler is asked to act upon a job about which it has no information. 32 | var ErrNoSuchJob = errors.New("no such job") 33 | 34 | // ErrJobAlreadyExists is returned when the scheduler is asked to create a job that already exists. 35 | var ErrJobAlreadyExists = errors.New("job already exists") 36 | 37 | // ErrJobRunning is returned when the scheduler is asked to interact with a job that is running. 38 | var ErrJobRunning = errors.New("job running") 39 | 40 | // ErrJobFinalised is returned when the scheduler is asked to interact with a job that is finalised. 41 | var ErrJobFinalised = errors.New("job finalised") 42 | 43 | // ErrNoJobName is returned when an attempt is made to control a job without a name. 44 | var ErrNoJobName = errors.New("no job name") 45 | 46 | // ErrNoJobFunc is returned when an attempt is made to run a nil job. 47 | var ErrNoJobFunc = errors.New("no job function") 48 | 49 | // ErrNoRuntimeFunc is returned when an attempt is made to run a periodic job without a runtime function. 50 | var ErrNoRuntimeFunc = errors.New("no runtime function") 51 | 52 | // Service is the interface for schedulers. 53 | type Service interface { 54 | // ScheduleJob schedules a one-off job for a given time. 55 | // This function returns two cancel funcs. If the first is triggered the job will not run. If the second is triggered the job 56 | // runs immediately. 57 | // Note that if the parent context is cancelled the job wil not run. 58 | ScheduleJob(ctx context.Context, class string, name string, runtime time.Time, job JobFunc, data any) error 59 | 60 | // SchedulePeriodicJob schedules a job to run in a loop. 61 | // The loop starts by calling runtimeFunc, which sets the time for the first run. 62 | // Once the time as specified by runtimeFunc is met, jobFunc is called. 63 | // Once jobFunc returns, go back to the beginning of the loop. 64 | SchedulePeriodicJob(ctx context.Context, class string, name string, runtime RuntimeFunc, runtimeData any, job JobFunc, jobData any) error 65 | 66 | // CancelJob cancels a known job. 67 | // If this is a period job then all future instances are cancelled. 68 | CancelJob(ctx context.Context, name string) error 69 | 70 | // CancelJobIfExists cancels a job that may or may not exist. 71 | // If this is a period job then all future instances are cancelled. 72 | CancelJobIfExists(ctx context.Context, name string) 73 | 74 | // CancelJobs cancels all jobs with the given prefix. 75 | // If the prefix matches a period job then all future instances are cancelled. 76 | CancelJobs(ctx context.Context, prefix string) 77 | 78 | // RunJob runs a known job. 79 | // If this is a period job then the next instance will be scheduled. 80 | RunJob(ctx context.Context, name string) error 81 | 82 | // JobExists returns true if a job exists. 83 | JobExists(ctx context.Context, name string) bool 84 | 85 | // RunJobIfExists runs a job if it exists. 86 | // If this is a period job then the next instance will be scheduled. 87 | RunJobIfExists(ctx context.Context, name string) 88 | 89 | // ListJobs returns the names of all jobs. 90 | ListJobs(ctx context.Context) []string 91 | } 92 | -------------------------------------------------------------------------------- /services/scheduler/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | "github.com/wealdtech/chaind/services/metrics" 21 | ) 22 | 23 | var metricsNamespace = "scheduler" 24 | 25 | var ( 26 | schedulerJobsScheduled *prometheus.CounterVec 27 | schedulerJobsCancelled *prometheus.CounterVec 28 | schedulerJobsStarted *prometheus.CounterVec 29 | ) 30 | 31 | func registerMetrics(ctx context.Context, monitor metrics.Service) error { 32 | if schedulerJobsScheduled != nil { 33 | // Already registered. 34 | return nil 35 | } 36 | if monitor == nil { 37 | // No monitor. 38 | return nil 39 | } 40 | if monitor.Presenter() == "prometheus" { 41 | return registerPrometheusMetrics(ctx) 42 | } 43 | return nil 44 | } 45 | 46 | func registerPrometheusMetrics(_ context.Context) error { 47 | schedulerJobsScheduled = prometheus.NewCounterVec(prometheus.CounterOpts{ 48 | Namespace: metricsNamespace, 49 | Subsystem: "jobs", 50 | Name: "scheduled_total", 51 | Help: "The number of jobs scheduled.", 52 | }, []string{"class"}) 53 | if err := prometheus.Register(schedulerJobsScheduled); err != nil { 54 | return err 55 | } 56 | 57 | schedulerJobsCancelled = prometheus.NewCounterVec(prometheus.CounterOpts{ 58 | Namespace: metricsNamespace, 59 | Subsystem: "jobs", 60 | Name: "cancelled_total", 61 | Help: "The number of scheduled jobs cancelled.", 62 | }, []string{"class"}) 63 | if err := prometheus.Register(schedulerJobsCancelled); err != nil { 64 | return err 65 | } 66 | 67 | schedulerJobsStarted = prometheus.NewCounterVec(prometheus.CounterOpts{ 68 | Namespace: metricsNamespace, 69 | Subsystem: "jobs", 70 | Name: "started_total", 71 | Help: "The number of scheduled jobs started.", 72 | }, []string{"class", "trigger"}) 73 | return prometheus.Register(schedulerJobsStarted) 74 | } 75 | 76 | // jobScheduled is called when a job is scheduled. 77 | func jobScheduled(class string) { 78 | if schedulerJobsScheduled != nil { 79 | schedulerJobsScheduled.WithLabelValues(class).Inc() 80 | } 81 | } 82 | 83 | // jobCancelled is called when a scheduled job is cancelled. 84 | func jobCancelled(class string) { 85 | if schedulerJobsCancelled != nil { 86 | schedulerJobsCancelled.WithLabelValues(class).Inc() 87 | } 88 | } 89 | 90 | // jobStartedOnTimer is called when a scheduled job is started due to meeting its time. 91 | func jobStartedOnTimer(class string) { 92 | if schedulerJobsScheduled != nil { 93 | schedulerJobsStarted.WithLabelValues(class, "timer").Inc() 94 | } 95 | } 96 | 97 | // jobStartedOnSignal is called when a scheduled job is started due to being manually signalled. 98 | func jobStartedOnSignal(class string) { 99 | if schedulerJobsStarted != nil { 100 | schedulerJobsStarted.WithLabelValues(class, "signal").Inc() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /services/scheduler/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technoogy Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "github.com/rs/zerolog" 18 | "github.com/wealdtech/chaind/services/metrics" 19 | nullmetrics "github.com/wealdtech/chaind/services/metrics/null" 20 | ) 21 | 22 | type parameters struct { 23 | logLevel zerolog.Level 24 | monitor metrics.Service 25 | } 26 | 27 | // Parameter is the interface for service parameters. 28 | type Parameter interface { 29 | apply(p *parameters) 30 | } 31 | 32 | type parameterFunc func(*parameters) 33 | 34 | func (f parameterFunc) apply(p *parameters) { 35 | f(p) 36 | } 37 | 38 | // WithLogLevel sets the log level for the module. 39 | func WithLogLevel(logLevel zerolog.Level) Parameter { 40 | return parameterFunc(func(p *parameters) { 41 | p.logLevel = logLevel 42 | }) 43 | } 44 | 45 | // WithMonitor sets the monitor for this module. 46 | func WithMonitor(monitor metrics.Service) Parameter { 47 | return parameterFunc(func(p *parameters) { 48 | p.monitor = monitor 49 | }) 50 | } 51 | 52 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 53 | // 54 | //nolint:unparam 55 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 56 | parameters := parameters{ 57 | logLevel: zerolog.GlobalLevel(), 58 | } 59 | for _, p := range params { 60 | if params != nil { 61 | p.apply(¶meters) 62 | } 63 | } 64 | 65 | if parameters.monitor == nil { 66 | parameters.monitor = &nullmetrics.Service{} 67 | } 68 | 69 | return ¶meters, nil 70 | } 71 | -------------------------------------------------------------------------------- /services/spec/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021, 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "errors" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/scheduler" 23 | ) 24 | 25 | type parameters struct { 26 | logLevel zerolog.Level 27 | eth2Client eth2client.Service 28 | chainDB chaindb.Service 29 | scheduler scheduler.Service 30 | } 31 | 32 | // Parameter is the interface for service parameters. 33 | type Parameter interface { 34 | apply(p *parameters) 35 | } 36 | 37 | type parameterFunc func(*parameters) 38 | 39 | func (f parameterFunc) apply(p *parameters) { 40 | f(p) 41 | } 42 | 43 | // WithLogLevel sets the log level for the module. 44 | func WithLogLevel(logLevel zerolog.Level) Parameter { 45 | return parameterFunc(func(p *parameters) { 46 | p.logLevel = logLevel 47 | }) 48 | } 49 | 50 | // WithETH2Client sets the Ethereum 2 client for this module. 51 | func WithETH2Client(eth2Client eth2client.Service) Parameter { 52 | return parameterFunc(func(p *parameters) { 53 | p.eth2Client = eth2Client 54 | }) 55 | } 56 | 57 | // WithChainDB sets the chain database for this module. 58 | func WithChainDB(chainDB chaindb.Service) Parameter { 59 | return parameterFunc(func(p *parameters) { 60 | p.chainDB = chainDB 61 | }) 62 | } 63 | 64 | // WithScheduler sets the scheduler for this module. 65 | func WithScheduler(scheduler scheduler.Service) Parameter { 66 | return parameterFunc(func(p *parameters) { 67 | p.scheduler = scheduler 68 | }) 69 | } 70 | 71 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 72 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 73 | parameters := parameters{ 74 | logLevel: zerolog.GlobalLevel(), 75 | } 76 | for _, p := range params { 77 | if params != nil { 78 | p.apply(¶meters) 79 | } 80 | } 81 | 82 | if parameters.eth2Client == nil { 83 | return nil, errors.New("no Ethereum 2 client specified") 84 | } 85 | if parameters.chainDB == nil { 86 | return nil, errors.New("no chain database specified") 87 | } 88 | if parameters.scheduler == nil { 89 | return nil, errors.New("no scheduler specified") 90 | } 91 | 92 | return ¶meters, nil 93 | } 94 | -------------------------------------------------------------------------------- /services/summarizer/service.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package summarizer 15 | 16 | // Service is a summarizer service. 17 | type Service any 18 | -------------------------------------------------------------------------------- /services/summarizer/standard/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard_test 15 | 16 | import ( 17 | "os" 18 | "testing" 19 | 20 | "github.com/rs/zerolog" 21 | ) 22 | 23 | func TestMain(m *testing.M) { 24 | zerolog.SetGlobalLevel(zerolog.Disabled) 25 | if os.Getenv("CHAINDB_URL") != "" && 26 | os.Getenv("ETH2CLIENT_ADDRESS") != "" { 27 | os.Exit(m.Run()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/summarizer/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021, 2023 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/attestantio/go-eth2-client/spec/phase0" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | // metadata stored about this service. 25 | type metadata struct { 26 | LastValidatorEpoch phase0.Epoch `json:"latest_validator_epoch"` 27 | LastBlockEpoch phase0.Epoch `json:"latest_block_epoch"` 28 | LastEpoch phase0.Epoch `json:"latest_epoch"` 29 | LastValidatorDay int64 `json:"last_validator_day"` 30 | PeriodicValidatorRollups bool `json:"periodic_validator_rollups"` 31 | } 32 | 33 | // metadataKey is the key for the metadata. 34 | var metadataKey = "summarizer.standard" 35 | 36 | // getMetadata gets metadata for this service. 37 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 38 | md := &metadata{ 39 | LastValidatorDay: -1, 40 | } 41 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 42 | if err != nil { 43 | return nil, errors.Wrap(err, "failed to fetch metadata") 44 | } 45 | if mdJSON == nil { 46 | return md, nil 47 | } 48 | if err := json.Unmarshal(mdJSON, md); err != nil { 49 | // Assume this is the old format. 50 | omd := &oldmetadata{} 51 | if err := json.Unmarshal(mdJSON, omd); err != nil { 52 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 53 | } 54 | // Convert to new format. 55 | md.LastValidatorEpoch = phase0.Epoch(omd.LastValidatorEpoch) 56 | md.LastBlockEpoch = phase0.Epoch(omd.LastBlockEpoch) 57 | md.LastEpoch = phase0.Epoch(omd.LastEpoch) 58 | md.LastValidatorDay = omd.LastValidatorDay 59 | md.PeriodicValidatorRollups = omd.PeriodicValidatorRollups 60 | } 61 | 62 | return md, nil 63 | } 64 | 65 | // setMetadata sets metadata for this service. 66 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 67 | mdJSON, err := json.Marshal(md) 68 | if err != nil { 69 | return errors.Wrap(err, "failed to marshal metadata") 70 | } 71 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 72 | return errors.Wrap(err, "failed to update metadata") 73 | } 74 | return nil 75 | } 76 | 77 | // oldmetadata is pre-0.8.8 metadata using unquoted strings for epochs. 78 | type oldmetadata struct { 79 | LastValidatorEpoch uint64 `json:"latest_validator_epoch"` 80 | LastBlockEpoch uint64 `json:"latest_block_epoch"` 81 | LastEpoch uint64 `json:"latest_epoch"` 82 | LastValidatorDay int64 `json:"last_validator_day"` 83 | PeriodicValidatorRollups bool `json:"periodic_validator_rollups"` 84 | } 85 | -------------------------------------------------------------------------------- /services/summarizer/standard/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard_test 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/attestantio/go-eth2-client/http" 22 | "github.com/rs/zerolog" 23 | "github.com/stretchr/testify/require" 24 | mockblocks "github.com/wealdtech/chaind/services/blocks/mock" 25 | postgresqlchaindb "github.com/wealdtech/chaind/services/chaindb/postgresql" 26 | standardchaintime "github.com/wealdtech/chaind/services/chaintime/standard" 27 | "github.com/wealdtech/chaind/services/finalizer/standard" 28 | ) 29 | 30 | func TestService(t *testing.T) { 31 | ctx := context.Background() 32 | 33 | chainDB, err := postgresqlchaindb.New(ctx, 34 | postgresqlchaindb.WithLogLevel(zerolog.Disabled), 35 | postgresqlchaindb.WithConnectionURL(os.Getenv("CHAINDB_URL")), 36 | ) 37 | require.NoError(t, err) 38 | 39 | consensusClient, err := http.New(ctx, 40 | http.WithAddress(os.Getenv("ETH2CLIENT_ADDRESS")), 41 | ) 42 | require.NoError(t, err) 43 | 44 | chainTime, err := standardchaintime.New(ctx, 45 | standardchaintime.WithGenesisProvider(chainDB), 46 | standardchaintime.WithSpecProvider(chainDB), 47 | standardchaintime.WithForkScheduleProvider(chainDB), 48 | ) 49 | require.NoError(t, err) 50 | 51 | blocks := mockblocks.New() 52 | 53 | tests := []struct { 54 | name string 55 | params []standard.Parameter 56 | err string 57 | }{ 58 | { 59 | name: "ChainDBMissing", 60 | params: []standard.Parameter{ 61 | standard.WithLogLevel(zerolog.Disabled), 62 | standard.WithChainTime(chainTime), 63 | standard.WithETH2Client(consensusClient), 64 | standard.WithBlocks(blocks), 65 | }, 66 | err: "problem with parameters: no chain database specified", 67 | }, 68 | { 69 | name: "ChainTimeMissing", 70 | params: []standard.Parameter{ 71 | standard.WithLogLevel(zerolog.Disabled), 72 | standard.WithChainDB(chainDB), 73 | standard.WithETH2Client(consensusClient), 74 | standard.WithBlocks(blocks), 75 | }, 76 | err: "problem with parameters: no chain time specified", 77 | }, 78 | { 79 | name: "ETH2ClientMissing", 80 | params: []standard.Parameter{ 81 | standard.WithLogLevel(zerolog.Disabled), 82 | standard.WithChainDB(chainDB), 83 | standard.WithChainTime(chainTime), 84 | standard.WithBlocks(blocks), 85 | }, 86 | err: "problem with parameters: no Ethereum 2 client specified", 87 | }, 88 | { 89 | name: "BlocksMissing", 90 | params: []standard.Parameter{ 91 | standard.WithLogLevel(zerolog.Disabled), 92 | standard.WithChainDB(chainDB), 93 | standard.WithChainTime(chainTime), 94 | standard.WithETH2Client(consensusClient), 95 | }, 96 | err: "problem with parameters: no blocks specified", 97 | }, 98 | { 99 | name: "Good", 100 | params: []standard.Parameter{ 101 | standard.WithLogLevel(zerolog.Disabled), 102 | standard.WithChainDB(chainDB), 103 | standard.WithChainTime(chainTime), 104 | standard.WithETH2Client(consensusClient), 105 | standard.WithBlocks(blocks), 106 | }, 107 | }, 108 | } 109 | 110 | for _, test := range tests { 111 | t.Run(test.name, func(t *testing.T) { 112 | _, err := standard.New(context.Background(), test.params...) 113 | if test.err != "" { 114 | require.EqualError(t, err, test.err) 115 | } else { 116 | require.NoError(t, err) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /services/synccommittees/standard/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "github.com/attestantio/go-eth2-client/api" 21 | "github.com/attestantio/go-eth2-client/spec/phase0" 22 | "github.com/pkg/errors" 23 | "github.com/wealdtech/chaind/services/chaindb" 24 | ) 25 | 26 | // OnBeaconChainHeadUpdated receives beacon chain head updated notifications. 27 | func (s *Service) OnBeaconChainHeadUpdated( 28 | ctx context.Context, 29 | slot phase0.Slot, 30 | ) { 31 | // Only allow 1 handler to be active. 32 | acquired := s.activitySem.TryAcquire(1) 33 | if !acquired { 34 | log.Debug().Msg("Another handler running") 35 | return 36 | } 37 | defer s.activitySem.Release(1) 38 | 39 | epoch := s.chainTime.SlotToEpoch(slot) 40 | period := s.chainTime.EpochToSyncCommitteePeriod(epoch) 41 | log := log.With().Uint64("slot", uint64(slot)).Uint64("epoch", uint64(epoch)).Uint64("period", period).Logger() 42 | 43 | if epoch < s.chainTime.AltairInitialEpoch() { 44 | log.Trace().Msg("Chain is not yet generating sync committees") 45 | return 46 | } 47 | 48 | md, err := s.getMetadata(ctx) 49 | if err != nil { 50 | log.Error().Err(err).Msg("Failed to obtain metadata") 51 | return 52 | } 53 | 54 | if int64(period) <= md.LatestPeriod { 55 | log.Trace().Msg("Already have sync committees for this period") 56 | return 57 | } 58 | 59 | s.catchup(ctx, md) 60 | } 61 | 62 | func (s *Service) updateSyncCommitteeForPeriod(ctx context.Context, period uint64) error { 63 | log.Trace().Uint64("period", period).Msg("Updating sync committee") 64 | 65 | if period < s.chainTime.AltairInitialSyncCommitteePeriod() { 66 | log.Trace().Uint64("period", period).Msg("period before Altair; nothing to do") 67 | } 68 | 69 | syncCommitteeResponse, err := s.syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{ 70 | State: fmt.Sprintf("%d", s.chainTime.FirstSlotOfEpoch(s.chainTime.FirstEpochOfSyncPeriod(period))), 71 | }) 72 | if err != nil { 73 | return errors.Wrap(err, "failed to fetch sync committee") 74 | } 75 | syncCommittee := syncCommitteeResponse.Data 76 | 77 | dbSyncCommittee := &chaindb.SyncCommittee{ 78 | Period: period, 79 | Committee: syncCommittee.Validators, 80 | } 81 | if err := s.syncCommitteesSetter.SetSyncCommittee(ctx, dbSyncCommittee); err != nil { 82 | return errors.Wrap(err, "failed to set sync committee") 83 | } 84 | 85 | monitorPeriodProcessed(period) 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /services/synccommittees/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | // metadata stored about this service. 24 | type metadata struct { 25 | LatestPeriod int64 `json:"latest_period"` 26 | } 27 | 28 | // metadataKey is the key for the metadata. 29 | var metadataKey = "synccommittees.standard" 30 | 31 | // getMetadata gets metadata for this service. 32 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 33 | md := &metadata{ 34 | LatestPeriod: -1, 35 | } 36 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "failed to fetch metadata") 39 | } 40 | if mdJSON == nil { 41 | return md, nil 42 | } 43 | if err := json.Unmarshal(mdJSON, md); err != nil { 44 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 45 | } 46 | return md, nil 47 | } 48 | 49 | // setMetadata sets metadata for this service. 50 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 51 | mdJSON, err := json.Marshal(md) 52 | if err != nil { 53 | return errors.Wrap(err, "failed to marshal metadata") 54 | } 55 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 56 | return errors.Wrap(err, "failed to update metadata") 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /services/synccommittees/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/pkg/errors" 20 | "github.com/prometheus/client_golang/prometheus" 21 | "github.com/wealdtech/chaind/services/metrics" 22 | ) 23 | 24 | var metricsNamespace = "chaind_synccommittees" 25 | 26 | var ( 27 | highestPeriod uint64 28 | latestPeriod prometheus.Gauge 29 | periodsProcessed prometheus.Gauge 30 | ) 31 | 32 | func registerMetrics(ctx context.Context, monitor metrics.Service) error { 33 | if latestPeriod != nil { 34 | // Already registered. 35 | return nil 36 | } 37 | if monitor == nil { 38 | // No monitor. 39 | return nil 40 | } 41 | if monitor.Presenter() == "prometheus" { 42 | return registerPrometheusMetrics(ctx) 43 | } 44 | return nil 45 | } 46 | 47 | func registerPrometheusMetrics(_ context.Context) error { 48 | latestPeriod = prometheus.NewGauge(prometheus.GaugeOpts{ 49 | Namespace: metricsNamespace, 50 | Name: "latest_period", 51 | Help: "Latest sync committee period processed", 52 | }) 53 | if err := prometheus.Register(latestPeriod); err != nil { 54 | return errors.Wrap(err, "failed to register latest_period") 55 | } 56 | 57 | periodsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 58 | Namespace: metricsNamespace, 59 | Name: "periods_processed", 60 | Help: "Number of periods processed", 61 | }) 62 | if err := prometheus.Register(periodsProcessed); err != nil { 63 | return errors.Wrap(err, "failed to register periods_processed") 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func monitorPeriodProcessed(period uint64) { 70 | if periodsProcessed != nil { 71 | periodsProcessed.Inc() 72 | if period > highestPeriod { 73 | latestPeriod.Set(float64(period)) 74 | highestPeriod = period 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /services/validators/standard/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | 20 | "github.com/attestantio/go-eth2-client/spec/phase0" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | // metadata stored about this service. 25 | type metadata struct { 26 | LatestEpoch phase0.Epoch `json:"latest_epoch"` 27 | LatestBalancesEpoch phase0.Epoch `json:"latest_balances_epoch"` 28 | MissedEpochs []phase0.Epoch `json:"missed_epochs,omitempty"` 29 | } 30 | 31 | // metadataKey is the key for the metadata. 32 | var metadataKey = "validators.standard" 33 | 34 | // getMetadata gets metadata for this service. 35 | func (s *Service) getMetadata(ctx context.Context) (*metadata, error) { 36 | md := &metadata{} 37 | mdJSON, err := s.chainDB.Metadata(ctx, metadataKey) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "failed to obtain metadata") 40 | } 41 | if mdJSON == nil { 42 | return md, nil 43 | } 44 | if err := json.Unmarshal(mdJSON, md); err != nil { 45 | // Assume this is the old format. 46 | omd := &oldmetadata{} 47 | if err := json.Unmarshal(mdJSON, omd); err != nil { 48 | return nil, errors.Wrap(err, "failed to unmarshal metadata") 49 | } 50 | // Convert to new format. 51 | md.LatestEpoch = phase0.Epoch(omd.LatestEpoch) 52 | md.LatestBalancesEpoch = phase0.Epoch(omd.LatestBalancesEpoch) 53 | md.MissedEpochs = make([]phase0.Epoch, len(omd.MissedEpochs)) 54 | for i := range omd.MissedEpochs { 55 | md.MissedEpochs[i] = phase0.Epoch(omd.MissedEpochs[i]) 56 | } 57 | } 58 | 59 | return md, nil 60 | } 61 | 62 | // setMetadata sets metadata for this service. 63 | func (s *Service) setMetadata(ctx context.Context, md *metadata) error { 64 | mdJSON, err := json.Marshal(md) 65 | if err != nil { 66 | return errors.Wrap(err, "failed to marshal metadata") 67 | } 68 | if err := s.chainDB.SetMetadata(ctx, metadataKey, mdJSON); err != nil { 69 | return errors.Wrap(err, "failed to update metadata") 70 | } 71 | return nil 72 | } 73 | 74 | // oldmetadata is pre-0.8.8 metadata using unquoted strings for epochs. 75 | type oldmetadata struct { 76 | LatestEpoch uint64 `json:"latest_epoch"` 77 | LatestBalancesEpoch uint64 `json:"latest_balances_epoch"` 78 | MissedEpochs []uint64 `json:"missed_epochs,omitempty"` 79 | } 80 | -------------------------------------------------------------------------------- /services/validators/standard/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Weald Technology Limited. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/attestantio/go-eth2-client/spec/phase0" 20 | "github.com/pkg/errors" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/wealdtech/chaind/services/metrics" 23 | ) 24 | 25 | var metricsNamespace = "chaind_validators" 26 | 27 | var ( 28 | highestEpoch phase0.Epoch 29 | latestEpoch prometheus.Gauge 30 | epochsProcessed prometheus.Gauge 31 | ) 32 | 33 | var ( 34 | balancesHighestEpoch phase0.Epoch 35 | balancesLatestEpoch prometheus.Gauge 36 | balancesEpochsProcessed prometheus.Gauge 37 | ) 38 | 39 | func registerMetrics(_ context.Context, monitor metrics.Service) error { 40 | if latestEpoch != nil { 41 | // Already registered. 42 | return nil 43 | } 44 | if monitor == nil { 45 | // No monitor. 46 | return nil 47 | } 48 | if monitor.Presenter() == "prometheus" { 49 | return registerPrometheusMetrics() 50 | } 51 | return nil 52 | } 53 | 54 | func registerPrometheusMetrics() error { 55 | latestEpoch = prometheus.NewGauge(prometheus.GaugeOpts{ 56 | Namespace: metricsNamespace, 57 | Name: "latest_epoch", 58 | Help: "Latest epoch processed for validators", 59 | }) 60 | if err := prometheus.Register(latestEpoch); err != nil { 61 | return errors.Wrap(err, "failed to register latest_epoch") 62 | } 63 | 64 | epochsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 65 | Namespace: metricsNamespace, 66 | Name: "epochs_processed", 67 | Help: "Number of epochs processed", 68 | }) 69 | if err := prometheus.Register(epochsProcessed); err != nil { 70 | return errors.Wrap(err, "failed to register epochs_processed") 71 | } 72 | 73 | balancesLatestEpoch = prometheus.NewGauge(prometheus.GaugeOpts{ 74 | Namespace: metricsNamespace, 75 | Name: "balances_latest_epoch", 76 | Help: "Latest epoch processed for validators balances", 77 | }) 78 | if err := prometheus.Register(balancesLatestEpoch); err != nil { 79 | return errors.Wrap(err, "failed to register balances_latest_epoch") 80 | } 81 | 82 | balancesEpochsProcessed = prometheus.NewGauge(prometheus.GaugeOpts{ 83 | Namespace: metricsNamespace, 84 | Name: "balances_epochs_processed", 85 | Help: "Number of epochs processed", 86 | }) 87 | if err := prometheus.Register(balancesEpochsProcessed); err != nil { 88 | return errors.Wrap(err, "failed to register balances_epochs_processed") 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func monitorEpochProcessed(epoch phase0.Epoch) { 95 | if epochsProcessed != nil { 96 | epochsProcessed.Inc() 97 | if epoch > highestEpoch { 98 | latestEpoch.Set(float64(epoch)) 99 | highestEpoch = epoch 100 | } 101 | } 102 | } 103 | 104 | func monitorBalancesEpochProcessed(epoch phase0.Epoch) { 105 | if balancesEpochsProcessed != nil { 106 | balancesEpochsProcessed.Inc() 107 | if epoch > balancesHighestEpoch { 108 | balancesLatestEpoch.Set(float64(epoch)) 109 | balancesHighestEpoch = epoch 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /services/validators/standard/parameters.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package standard 15 | 16 | import ( 17 | "errors" 18 | 19 | eth2client "github.com/attestantio/go-eth2-client" 20 | "github.com/rs/zerolog" 21 | "github.com/wealdtech/chaind/services/chaindb" 22 | "github.com/wealdtech/chaind/services/chaintime" 23 | "github.com/wealdtech/chaind/services/metrics" 24 | ) 25 | 26 | type parameters struct { 27 | logLevel zerolog.Level 28 | monitor metrics.Service 29 | eth2Client eth2client.Service 30 | chainDB chaindb.Service 31 | chainTime chaintime.Service 32 | balances bool 33 | startEpoch int64 34 | } 35 | 36 | // Parameter is the interface for service parameters. 37 | type Parameter interface { 38 | apply(p *parameters) 39 | } 40 | 41 | type parameterFunc func(*parameters) 42 | 43 | func (f parameterFunc) apply(p *parameters) { 44 | f(p) 45 | } 46 | 47 | // WithLogLevel sets the log level for the module. 48 | func WithLogLevel(logLevel zerolog.Level) Parameter { 49 | return parameterFunc(func(p *parameters) { 50 | p.logLevel = logLevel 51 | }) 52 | } 53 | 54 | // WithMonitor sets the monitor for the module. 55 | func WithMonitor(monitor metrics.Service) Parameter { 56 | return parameterFunc(func(p *parameters) { 57 | p.monitor = monitor 58 | }) 59 | } 60 | 61 | // WithETH2Client sets the Ethereum 2 client for this module. 62 | func WithETH2Client(eth2Client eth2client.Service) Parameter { 63 | return parameterFunc(func(p *parameters) { 64 | p.eth2Client = eth2Client 65 | }) 66 | } 67 | 68 | // WithChainDB sets the chain database for this module. 69 | func WithChainDB(chainDB chaindb.Service) Parameter { 70 | return parameterFunc(func(p *parameters) { 71 | p.chainDB = chainDB 72 | }) 73 | } 74 | 75 | // WithChainTime sets the chain time service for this module. 76 | func WithChainTime(chainTime chaintime.Service) Parameter { 77 | return parameterFunc(func(p *parameters) { 78 | p.chainTime = chainTime 79 | }) 80 | } 81 | 82 | // WithStartEpoch sets the start epoch for this module. 83 | func WithStartEpoch(startEpoch int64) Parameter { 84 | return parameterFunc(func(p *parameters) { 85 | p.startEpoch = startEpoch 86 | }) 87 | } 88 | 89 | // WithBalances states if the module should fetch validator balances. 90 | func WithBalances(balances bool) Parameter { 91 | return parameterFunc(func(p *parameters) { 92 | p.balances = balances 93 | }) 94 | } 95 | 96 | // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. 97 | func parseAndCheckParameters(params ...Parameter) (*parameters, error) { 98 | parameters := parameters{ 99 | logLevel: zerolog.GlobalLevel(), 100 | startEpoch: -1, 101 | balances: false, 102 | } 103 | for _, p := range params { 104 | if params != nil { 105 | p.apply(¶meters) 106 | } 107 | } 108 | 109 | if parameters.eth2Client == nil { 110 | return nil, errors.New("no Ethereum 2 client specified") 111 | } 112 | if parameters.chainDB == nil { 113 | return nil, errors.New("no chain database specified") 114 | } 115 | if parameters.chainTime == nil { 116 | return nil, errors.New("no chain time specified") 117 | } 118 | 119 | return ¶meters, nil 120 | } 121 | -------------------------------------------------------------------------------- /util/commit.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import "runtime/debug" 17 | 18 | // CommitHash returns the commit hash of the build, if available. 19 | func CommitHash() string { 20 | if info, ok := debug.ReadBuildInfo(); ok { 21 | for _, setting := range info.Settings { 22 | if setting.Key == "vcs.revision" { 23 | return setting.Value 24 | } 25 | } 26 | } 27 | 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /util/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | 20 | "github.com/rs/zerolog" 21 | zerologger "github.com/rs/zerolog/log" 22 | "github.com/spf13/viper" 23 | ) 24 | 25 | // LogLevel returns the best log level for the path. 26 | func LogLevel(path string) zerolog.Level { 27 | if path == "" { 28 | return stringToLevel(viper.GetString("log-level")) 29 | } 30 | 31 | key := fmt.Sprintf("%s.log-level", path) 32 | if viper.GetString(key) != "" { 33 | return stringToLevel(viper.GetString(key)) 34 | } 35 | // Lop off the child and try again. 36 | lastPeriod := strings.LastIndex(path, ".") 37 | if lastPeriod == -1 { 38 | return LogLevel("") 39 | } 40 | return LogLevel(path[0:lastPeriod]) 41 | } 42 | 43 | // stringtoLevel converts a string to a log level. 44 | // It returns the user-supplied level by default. 45 | func stringToLevel(input string) zerolog.Level { 46 | switch strings.ToLower(input) { 47 | case "none": 48 | return zerolog.Disabled 49 | case "trace": 50 | return zerolog.TraceLevel 51 | case "debug": 52 | return zerolog.DebugLevel 53 | case "warn", "warning": 54 | return zerolog.WarnLevel 55 | case "info", "information": 56 | return zerolog.InfoLevel 57 | case "err", "error": 58 | return zerolog.ErrorLevel 59 | case "fatal": 60 | return zerolog.FatalLevel 61 | default: 62 | return zerologger.Logger.GetLevel() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /util/majordomo.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/aws/aws-sdk-go/aws/credentials" 20 | "github.com/pkg/errors" 21 | "github.com/spf13/viper" 22 | majordomo "github.com/wealdtech/go-majordomo" 23 | asmconfidant "github.com/wealdtech/go-majordomo/confidants/asm" 24 | directconfidant "github.com/wealdtech/go-majordomo/confidants/direct" 25 | fileconfidant "github.com/wealdtech/go-majordomo/confidants/file" 26 | gsmconfidant "github.com/wealdtech/go-majordomo/confidants/gsm" 27 | standardmajordomo "github.com/wealdtech/go-majordomo/standard" 28 | ) 29 | 30 | // InitMajordomo initialises the majordomo service, for fetching secrets. 31 | func InitMajordomo(ctx context.Context) (majordomo.Service, error) { 32 | majordomo, err := standardmajordomo.New(ctx, 33 | standardmajordomo.WithLogLevel(LogLevel("majordomo")), 34 | ) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "failed to create majordomo service") 37 | } 38 | 39 | directConfidant, err := directconfidant.New(ctx, 40 | directconfidant.WithLogLevel(LogLevel("majordomo.confidants.direct")), 41 | ) 42 | if err != nil { 43 | return nil, errors.Wrap(err, "failed to create direct confidant") 44 | } 45 | if err := majordomo.RegisterConfidant(ctx, directConfidant); err != nil { 46 | return nil, errors.Wrap(err, "failed to register direct confidant") 47 | } 48 | 49 | fileConfidant, err := fileconfidant.New(ctx, 50 | fileconfidant.WithLogLevel(LogLevel("majordomo.confidants.file")), 51 | ) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "failed to create file confidant") 54 | } 55 | if err := majordomo.RegisterConfidant(ctx, fileConfidant); err != nil { 56 | return nil, errors.Wrap(err, "failed to register file confidant") 57 | } 58 | 59 | if viper.GetString("majordomo.asm.region") != "" { 60 | var asmCredentials *credentials.Credentials 61 | if viper.GetString("majordomo.asm.id") != "" { 62 | asmCredentials = credentials.NewStaticCredentials(viper.GetString("majordomo.asm.id"), viper.GetString("ma jordomo.asm.secret"), "") 63 | } 64 | asmConfidant, err := asmconfidant.New(ctx, 65 | asmconfidant.WithLogLevel(LogLevel("majordomo.confidants.asm")), 66 | asmconfidant.WithCredentials(asmCredentials), 67 | asmconfidant.WithRegion(viper.GetString("majordomo.asm.region")), 68 | ) 69 | if err != nil { 70 | return nil, errors.Wrap(err, "failed to create AWS secrets manager confidant") 71 | } 72 | if err := majordomo.RegisterConfidant(ctx, asmConfidant); err != nil { 73 | return nil, errors.Wrap(err, "failed to register AWS secrets manager confidant") 74 | } 75 | } 76 | 77 | if viper.GetString("majordomo.gsm.project") != "" { 78 | var gsmCredentialsPath string 79 | if viper.GetString("majordomo.gsm.credentials") != "" { 80 | gsmCredentialsPath = ResolvePath(viper.GetString("majordomo.gsm.credentials")) 81 | } 82 | gsmConfidant, err := gsmconfidant.New(ctx, 83 | gsmconfidant.WithLogLevel(LogLevel("majordomo.confidants.gsm")), 84 | gsmconfidant.WithCredentialsPath(gsmCredentialsPath), 85 | gsmconfidant.WithProject(viper.GetString("majordomo.gsm.project")), 86 | ) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "failed to create Google secrets manager confidant") 89 | } 90 | if err := majordomo.RegisterConfidant(ctx, gsmConfidant); err != nil { 91 | return nil, errors.Wrap(err, "failed to register Google secrets manager confidant") 92 | } 93 | } 94 | 95 | return majordomo, nil 96 | } 97 | -------------------------------------------------------------------------------- /util/path.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Weald Technology Trading. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | import ( 17 | "path/filepath" 18 | 19 | "github.com/mitchellh/go-homedir" 20 | "github.com/spf13/viper" 21 | ) 22 | 23 | // ResolvePath resolves a potentially relative path to an absolute path. 24 | func ResolvePath(path string) string { 25 | if filepath.IsAbs(path) { 26 | return path 27 | } 28 | baseDir := viper.GetString("base-dir") 29 | if baseDir == "" { 30 | homeDir, err := homedir.Dir() 31 | if err != nil { 32 | panic("could not determine a home directory") 33 | } 34 | baseDir = homeDir 35 | } 36 | return filepath.Join(baseDir, path) 37 | } 38 | --------------------------------------------------------------------------------