diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..34e0128 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/dappnode/validator-tracker/internal/adapters" + "github.com/dappnode/validator-tracker/internal/application/services" + "github.com/dappnode/validator-tracker/internal/config" + "github.com/dappnode/validator-tracker/internal/logger" +) + +func main() { + // Load config + cfg := config.LoadConfig() + logger.Info("Loaded config: network=%s, beaconEndpoint=%s, web3SignerEndpoint=%s", + cfg.Network, cfg.BeaconEndpoint, cfg.Web3SignerEndpoint) + + // Fetch validator pubkeys + web3Signer := adapters.NewWeb3SignerAdapter(cfg.Web3SignerEndpoint) + pubkeys, err := web3Signer.GetValidatorPubkeys() + if err != nil { + logger.Fatal("Failed to get validator pubkeys from web3signer: %v", err) + } + logger.Info("Fetched %d pubkeys from web3signer", len(pubkeys)) + + // Initialize beacon chain adapter + adapter, err := adapters.NewBeaconAttestantAdapter(cfg.BeaconEndpoint) + if err != nil { + logger.Fatal("Failed to initialize beacon adapter: %v", err) + } + + // Get validator indices from pubkeys + indices, err := adapter.GetValidatorIndicesByPubkeys(context.Background(), pubkeys) + if err != nil { + logger.Fatal("Failed to get validator indices: %v", err) + } + logger.Info("Found %d validator indices active", len(indices)) + + // Prepare context and WaitGroup for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var wg sync.WaitGroup + + // Start the attestation checker service in a goroutine + logger.Info("Starting attestation checker for %d validators", len(indices)) + checker := &services.AttestationChecker{ + BeaconAdapter: adapter, + ValidatorIndices: indices, + PollInterval: 1 * time.Minute, + } + wg.Add(1) + go func() { + defer wg.Done() + checker.Run(ctx) + }() + + // Handle graceful shutdown + handleShutdown(cancel) + + // Wait for all services to stop + wg.Wait() + logger.Info("All services stopped. Shutting down.") +} + +// handleShutdown listens for SIGINT/SIGTERM and cancels the context +func handleShutdown(cancel context.CancelFunc) { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + sig := <-sigChan + logger.Info("Received signal: %s. Initiating shutdown...", sig) + cancel() + }() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ed40dd --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module github.com/dappnode/validator-tracker + +go 1.24.3 + +require ( + github.com/attestantio/go-eth2-client v0.25.2 + github.com/rs/zerolog v1.34.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/emicklei/dot v1.6.4 // indirect + github.com/fatih/color v1.10.0 // indirect + github.com/ferranbt/fastssz v0.1.4 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-yaml v1.9.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/huandu/go-clone v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pk910/dynamic-ssz v0.0.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8743cfe --- /dev/null +++ b/go.sum @@ -0,0 +1,151 @@ +github.com/attestantio/go-eth2-client v0.25.2 h1:BHOva0HlJZ47HwALQuqqfIAQ6gRIo5P/iqGpphrMsCE= +github.com/attestantio/go-eth2-client v0.25.2/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/dot v1.6.4 h1:cG9ycT67d9Yw22G+mAb4XiuUz6E6H1S0zePp/5Cwe/c= +github.com/emicklei/dot v1.6.4/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w= +github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= +github.com/huandu/go-clone v1.6.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= +github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pk910/dynamic-ssz v0.0.4 h1:DT29+1055tCEPCaR4V/ez+MOKW7BzBsmjyFvBRqx0ME= +github.com/pk910/dynamic-ssz v0.0.4/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/adapters/attestantclient_adapter.go b/internal/adapters/attestantclient_adapter.go new file mode 100644 index 0000000..45e5ebe --- /dev/null +++ b/internal/adapters/attestantclient_adapter.go @@ -0,0 +1,187 @@ +// internal/adapters/beaconchain_adapter.go +package adapters + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + nethttp "net/http" + "time" + + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/dappnode/validator-tracker/internal/application/domain" + "github.com/dappnode/validator-tracker/internal/application/ports" + "github.com/rs/zerolog" + + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +type beaconAttestantClient struct { + client *http.Service +} + +func NewBeaconAttestantAdapter(endpoint string) (ports.BeaconChainAdapter, error) { + zerolog.SetGlobalLevel(zerolog.WarnLevel) + + customHttpClient := &nethttp.Client{ + Timeout: 2000 * time.Second, + } + + client, err := http.New(context.Background(), + http.WithAddress(endpoint), + http.WithHTTPClient(customHttpClient), + http.WithTimeout(20*time.Second), // important as attestant API overrides my timeout TODO: investigate how + ) + if err != nil { + return nil, err + } + + return &beaconAttestantClient{client: client.(*http.Service)}, nil +} + +// GetFinalizedEpoch retrieves the latest finalized epoch from the beacon chain. +func (b *beaconAttestantClient) GetFinalizedEpoch(ctx context.Context) (domain.Epoch, error) { + finality, err := b.client.Finality(ctx, &api.FinalityOpts{State: "head"}) + if err != nil { + return 0, err + } + return domain.Epoch(finality.Data.Finalized.Epoch), nil +} + +// internal/adapters/beaconchain_adapter.go +func (b *beaconAttestantClient) GetValidatorDutiesBatch(ctx context.Context, epoch domain.Epoch, validatorIndices []domain.ValidatorIndex) ([]domain.ValidatorDuty, error) { + // Convert to phase0.ValidatorIndex + var indices []phase0.ValidatorIndex + for _, idx := range validatorIndices { + indices = append(indices, phase0.ValidatorIndex(idx)) + } + + duties, err := b.client.AttesterDuties(ctx, &api.AttesterDutiesOpts{ + Epoch: phase0.Epoch(epoch), + Indices: indices, + }) + if err != nil { + return nil, err + } + + // Map the response to domain.ValidatorDuty + var domainDuties []domain.ValidatorDuty + for _, d := range duties.Data { + domainDuties = append(domainDuties, domain.ValidatorDuty{ + Slot: domain.Slot(d.Slot), + CommitteeIndex: domain.CommitteeIndex(d.CommitteeIndex), + ValidatorCommitteeIdx: d.ValidatorCommitteeIndex, + ValidatorIndex: domain.ValidatorIndex(d.ValidatorIndex), // new field + }) + } + + return domainDuties, nil +} + +func (b *beaconAttestantClient) GetValidatorDuties(ctx context.Context, epoch domain.Epoch, validatorIndex domain.ValidatorIndex) (domain.ValidatorDuty, error) { + duties, err := b.client.AttesterDuties(ctx, &api.AttesterDutiesOpts{ + Epoch: phase0.Epoch(epoch), + Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(validatorIndex)}, + }) + if err != nil { + return domain.ValidatorDuty{}, err + } + + // 🚨 TODO: how to log this here? needed for validators loaded into web3signer but exited (no duties) + if len(duties.Data) == 0 { + return domain.ValidatorDuty{}, fmt.Errorf("no duties found for validator %d at epoch %d", validatorIndex, epoch) + } + + duty := duties.Data[0] + return domain.ValidatorDuty{ + Slot: domain.Slot(duty.Slot), + CommitteeIndex: domain.CommitteeIndex(duty.CommitteeIndex), + ValidatorCommitteeIdx: duty.ValidatorCommitteeIndex, + }, nil +} + +// GetCommitteeSizeMap retrieves the size of each attestation committee for a specific slot. +// This is very expensive and take a long time to execute, so it should be used sparingly. +// TODO: can we get rid of this? +func (b *beaconAttestantClient) GetCommitteeSizeMap(ctx context.Context, slot domain.Slot) (domain.CommitteeSizeMap, error) { + committees, err := b.client.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{ + State: fmt.Sprintf("%d", slot), + }) + if err != nil { + return nil, err + } + sizeMap := make(domain.CommitteeSizeMap) + for _, committee := range committees.Data { + if domain.Slot(committee.Slot) != slot { + continue + } + sizeMap[domain.CommitteeIndex(committee.Index)] = len(committee.Validators) + } + return sizeMap, nil +} + +// GetBlockAttestations retrieves all attestations include in a slot +func (b *beaconAttestantClient) GetBlockAttestations(ctx context.Context, slot domain.Slot) ([]domain.Attestation, error) { + block, err := b.client.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ + Block: fmt.Sprintf("%d", slot), + }) + if err != nil { + return nil, err + } + + var attestations []domain.Attestation + for _, att := range block.Data.Electra.Message.Body.Attestations { + attestations = append(attestations, domain.Attestation{ + DataSlot: domain.Slot(att.Data.Slot), + CommitteeBits: att.CommitteeBits, + AggregationBits: att.AggregationBits, + }) + } + return attestations, nil +} + +func (b *beaconAttestantClient) GetValidatorIndicesByPubkeys(ctx context.Context, pubkeys []string) ([]domain.ValidatorIndex, error) { + var beaconPubkeys []phase0.BLSPubKey + + // Convert hex pubkeys to BLS pubkeys + for _, hexPubkey := range pubkeys { + // Remove "0x" prefix if present + if len(hexPubkey) >= 2 && hexPubkey[:2] == "0x" { + hexPubkey = hexPubkey[2:] + } + bytes, err := hex.DecodeString(hexPubkey) + if err != nil { + return nil, errors.New("failed to decode pubkey: " + hexPubkey) + } + if len(bytes) != 48 { + return nil, errors.New("invalid pubkey length for: " + hexPubkey) + } + var blsPubkey phase0.BLSPubKey + copy(blsPubkey[:], bytes) + beaconPubkeys = append(beaconPubkeys, blsPubkey) + } + + // Only get validators in active states + // TODO: why do I need apiv1 for this struct? is there something newer? + validators, err := b.client.Validators(ctx, &api.ValidatorsOpts{ + State: "head", + PubKeys: beaconPubkeys, + ValidatorStates: []apiv1.ValidatorState{ + apiv1.ValidatorStateActiveOngoing, + apiv1.ValidatorStateActiveExiting, + apiv1.ValidatorStateActiveSlashed, + }, + }) + if err != nil { + return nil, err + } + + var indices []domain.ValidatorIndex + for _, v := range validators.Data { + indices = append(indices, domain.ValidatorIndex(v.Index)) + } + return indices, nil +} diff --git a/internal/adapters/web3signer_adapter.go b/internal/adapters/web3signer_adapter.go new file mode 100644 index 0000000..0255a0d --- /dev/null +++ b/internal/adapters/web3signer_adapter.go @@ -0,0 +1,46 @@ +// internal/adapters/web3signer_adapter.go +package adapters + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/dappnode/validator-tracker/internal/application/ports" +) + +// Web3SignerAdapter implements ports.Web3SignerAdapter +type Web3SignerAdapter struct { + Endpoint string +} + +type KeystoreResponse struct { + Data []struct { + ValidatingPubkey string `json:"validating_pubkey"` + } `json:"data"` +} + +func NewWeb3SignerAdapter(endpoint string) ports.Web3SignerAdapter { + return &Web3SignerAdapter{Endpoint: endpoint} +} + +func (w *Web3SignerAdapter) GetValidatorPubkeys() ([]string, error) { + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(fmt.Sprintf("%s/eth/v1/keystores", w.Endpoint)) + if err != nil { + return nil, fmt.Errorf("failed to fetch keystores: %w", err) + } + defer resp.Body.Close() + + var keystoreResp KeystoreResponse + if err := json.NewDecoder(resp.Body).Decode(&keystoreResp); err != nil { + return nil, fmt.Errorf("failed to parse keystores: %w", err) + } + + var pubkeys []string + for _, item := range keystoreResp.Data { + pubkeys = append(pubkeys, item.ValidatingPubkey) + } + return pubkeys, nil +} diff --git a/internal/application/domain/validator.go b/internal/application/domain/validator.go new file mode 100644 index 0000000..69501d4 --- /dev/null +++ b/internal/application/domain/validator.go @@ -0,0 +1,22 @@ +package domain + +type Epoch uint64 +type Slot uint64 +type ValidatorIndex uint64 +type CommitteeIndex uint64 + +// domain/domain.go +type ValidatorDuty struct { + Slot Slot + CommitteeIndex CommitteeIndex + ValidatorCommitteeIdx uint64 + ValidatorIndex ValidatorIndex +} + +type CommitteeSizeMap map[CommitteeIndex]int + +type Attestation struct { + DataSlot Slot + CommitteeBits []byte + AggregationBits []byte +} diff --git a/internal/application/ports/beaconchain.go b/internal/application/ports/beaconchain.go new file mode 100644 index 0000000..2201f37 --- /dev/null +++ b/internal/application/ports/beaconchain.go @@ -0,0 +1,16 @@ +package ports + +import ( + "context" + + "github.com/dappnode/validator-tracker/internal/application/domain" +) + +// ports/beaconchain_adapter.go +type BeaconChainAdapter interface { + GetFinalizedEpoch(ctx context.Context) (domain.Epoch, error) + GetValidatorDutiesBatch(ctx context.Context, epoch domain.Epoch, validatorIndices []domain.ValidatorIndex) ([]domain.ValidatorDuty, error) + GetCommitteeSizeMap(ctx context.Context, slot domain.Slot) (domain.CommitteeSizeMap, error) + GetBlockAttestations(ctx context.Context, slot domain.Slot) ([]domain.Attestation, error) + GetValidatorIndicesByPubkeys(ctx context.Context, pubkeys []string) ([]domain.ValidatorIndex, error) +} diff --git a/internal/application/ports/web3signer.go b/internal/application/ports/web3signer.go new file mode 100644 index 0000000..dfa1822 --- /dev/null +++ b/internal/application/ports/web3signer.go @@ -0,0 +1,5 @@ +package ports + +type Web3SignerAdapter interface { + GetValidatorPubkeys() ([]string, error) +} diff --git a/internal/application/services/attestation_oracle.go b/internal/application/services/attestation_oracle.go new file mode 100644 index 0000000..d6f2f01 --- /dev/null +++ b/internal/application/services/attestation_oracle.go @@ -0,0 +1,137 @@ +package services + +import ( + "context" + "time" + + "slices" + + "github.com/dappnode/validator-tracker/internal/application/domain" + "github.com/dappnode/validator-tracker/internal/application/ports" + "github.com/dappnode/validator-tracker/internal/logger" +) + +type AttestationChecker struct { + BeaconAdapter ports.BeaconChainAdapter + ValidatorIndices []domain.ValidatorIndex + PollInterval time.Duration + + lastFinalizedEpoch domain.Epoch // Track last seen finalized epoch +} + +func (a *AttestationChecker) Run(ctx context.Context) { + ticker := time.NewTicker(a.PollInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + a.checkLatestFinalizedEpoch(ctx) + case <-ctx.Done(): + return + } + } +} + +// checkLatestFinalizedEpoch checks the latest finalized epoch and verifies if validators have attested. +func (a *AttestationChecker) checkLatestFinalizedEpoch(ctx context.Context) { + finalizedEpoch, err := a.BeaconAdapter.GetFinalizedEpoch(ctx) + if err != nil { + logger.Error("Error fetching finalized epoch: %v", err) + return + } + + // Update last seen finalized epoch + a.lastFinalizedEpoch = finalizedEpoch + logger.Info("New finalized epoch %d detected. Checking attestations for %d validators...", finalizedEpoch, len(a.ValidatorIndices)) + + // Fetch all validator duties in a single call + duties, err := a.BeaconAdapter.GetValidatorDutiesBatch(ctx, finalizedEpoch, a.ValidatorIndices) + if err != nil { + logger.Error("Error fetching validator duties: %v", err) + return + } + + // Results map to track if each validator attested + attestationResults := make(map[domain.ValidatorIndex]bool) + + // TODO: careful setting duty as missed if program is interrupted here + for _, duty := range duties { + logger.Info("Checking duties for validator %d in committee %d for slot %d", duty.ValidatorIndex, duty.CommitteeIndex, duty.Slot) + committeeSizeMap, err := a.BeaconAdapter.GetCommitteeSizeMap(ctx, duty.Slot) + if err != nil { + logger.Warn("Error fetching committee sizes for slot %d: %v", duty.Slot, err) + continue + } + logger.Info("comitee gotten") + found := false + for slot := duty.Slot + 1; slot <= duty.Slot+32; slot++ { + attestations, err := a.BeaconAdapter.GetBlockAttestations(ctx, slot) + if err != nil { + logger.Warn("Error fetching attestations for slot %d: %v", slot, err) + continue + } + + for _, att := range attestations { + if att.DataSlot != duty.Slot { + continue + } + if !isBitSet(att.CommitteeBits, int(duty.CommitteeIndex)) { + continue + } + + bitPosition := computeBitPosition(duty.CommitteeIndex, duty.ValidatorCommitteeIdx, committeeSizeMap) + if !isBitSet(att.AggregationBits, bitPosition) { + continue + } + + found = true + logger.Info("✅ Found attestation for validator %d in committee %d for slot %d (included in block %d)", + duty.ValidatorIndex, duty.CommitteeIndex, duty.Slot, slot) + break + } + if found { + break + } + } + attestationResults[duty.ValidatorIndex] = found + } + + // Summary report + logger.Info("Attestation summary for finalized epoch %d:", finalizedEpoch) + for _, idx := range a.ValidatorIndices { + if attestationResults[idx] { + logger.Info("✅ Validator %d attested successfully", idx) + } else { + logger.Warn("❌ Validator %d missed attestation", idx) + } + } +} + +func computeBitPosition(committeeIndex domain.CommitteeIndex, validatorCommitteeIdx uint64, committeeSizeMap domain.CommitteeSizeMap) int { + indices := make([]domain.CommitteeIndex, 0, len(committeeSizeMap)) + for index := range committeeSizeMap { + indices = append(indices, index) + } + slices.Sort(indices) + + bitPosition := 0 + for _, index := range indices { + if index < committeeIndex { + bitPosition += committeeSizeMap[index] + } + } + bitPosition += int(validatorCommitteeIdx) + return bitPosition +} + +func isBitSet(bits []byte, index int) bool { + byteIndex := index / 8 + bitIndex := index % 8 + + if byteIndex >= len(bits) { + return false + } + + return (bits[byteIndex] & (1 << uint(bitIndex))) != 0 +} diff --git a/internal/config/config_loader.go b/internal/config/config_loader.go new file mode 100644 index 0000000..80ace22 --- /dev/null +++ b/internal/config/config_loader.go @@ -0,0 +1,46 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/dappnode/validator-tracker/internal/logger" +) + +type Config struct { + BeaconEndpoint string + Web3SignerEndpoint string + Network string +} + +func LoadConfig() Config { + network := os.Getenv("NETWORK") + if network == "" { + network = "hoodi" // default + } + + // Build the dynamic endpoints + beaconEndpoint := fmt.Sprintf("http://beacon-chain.%s.staker.dappnode:3500", network) + web3SignerEndpoint := fmt.Sprintf("http://web3signer.web3signer-%s.dappnode:9000", network) + + // Allow override via environment variables + if envBeacon := os.Getenv("BEACON_ENDPOINT"); envBeacon != "" { + beaconEndpoint = envBeacon + } + if envWeb3Signer := os.Getenv("WEB3SIGNER_ENDPOINT"); envWeb3Signer != "" { + web3SignerEndpoint = envWeb3Signer + } + + // Normalize network name for logs + network = strings.ToLower(network) + if network != "hoodi" && network != "holesky" && network != "mainnet" { + logger.Fatal("Unknown network: %s", network) + } + + return Config{ + BeaconEndpoint: beaconEndpoint, + Web3SignerEndpoint: web3SignerEndpoint, + Network: network, + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..b9b35bb --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,177 @@ +package logger + +import ( + "log" + "os" + "strings" +) + +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR + FATAL +) + +type Logger struct { + level LogLevel + debug *log.Logger + info *log.Logger + warn *log.Logger + error *log.Logger + fatal *log.Logger +} + +// Log is the exported, initialized logger instance +var Log *Logger + +// init function initializes Log with the log level from LOG_LEVEL environment variable +func init() { + level := parseLogLevelFromEnv() + Log = NewLogger(level) +} + +// parseLogLevelFromEnv reads the LOG_LEVEL environment variable and returns the corresponding LogLevel. +// Defaults to INFO if LOG_LEVEL is unset or invalid. +func parseLogLevelFromEnv() LogLevel { + logLevelStr := os.Getenv("LOG_LEVEL") + switch strings.ToUpper(logLevelStr) { + case "DEBUG": + return DEBUG + case "INFO": + return INFO + case "WARN": + return WARN + case "ERROR": + return ERROR + case "FATAL": + return FATAL + default: + return INFO // Default to INFO if LOG_LEVEL is not set or invalid + } +} + +func NewLogger(level LogLevel) *Logger { + return &Logger{ + level: level, + debug: log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime), + info: log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime), + warn: log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime), + error: log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime), + fatal: log.New(os.Stderr, "FATAL: ", log.Ldate|log.Ltime), + } +} + +// formatMessage formats the message with an optional prefix +func formatMessage(prefix, msg string) string { + if prefix != "" { + return "[" + prefix + "] " + msg + } + return msg +} + +// Debug logs debug messages with an optional prefix if the level is set to DEBUG or lower +func (l *Logger) Debug(msg string, v ...interface{}) { + l.DebugWithPrefix("", msg, v...) +} + +// DebugWithPrefix logs debug messages with a specific prefix +func (l *Logger) DebugWithPrefix(prefix, msg string, v ...interface{}) { + if l.level <= DEBUG { + l.debug.Printf(formatMessage(prefix, msg), v...) + } +} + +// Info logs informational messages with an optional prefix if the level is set to INFO or lower +func (l *Logger) Info(msg string, v ...interface{}) { + l.InfoWithPrefix("", msg, v...) +} + +// InfoWithPrefix logs informational messages with a specific prefix +func (l *Logger) InfoWithPrefix(prefix, msg string, v ...interface{}) { + if l.level <= INFO { + l.info.Printf(formatMessage(prefix, msg), v...) + } +} + +// Warn logs warning messages with an optional prefix if the level is set to WARN or lower +func (l *Logger) Warn(msg string, v ...interface{}) { + l.WarnWithPrefix("", msg, v...) +} + +// WarnWithPrefix logs warning messages with a specific prefix +func (l *Logger) WarnWithPrefix(prefix, msg string, v ...interface{}) { + if l.level <= WARN { + l.warn.Printf(formatMessage(prefix, msg), v...) + } +} + +// Error logs error messages with an optional prefix if the level is set to ERROR or lower +func (l *Logger) Error(msg string, v ...interface{}) { + l.ErrorWithPrefix("", msg, v...) +} + +// ErrorWithPrefix logs error messages with a specific prefix +func (l *Logger) ErrorWithPrefix(prefix, msg string, v ...interface{}) { + if l.level <= ERROR { + l.error.Printf(formatMessage(prefix, msg), v...) + } +} + +// Fatal logs fatal messages and exits the program +func (l *Logger) Fatal(msg string, v ...interface{}) { + l.FatalWithPrefix("", msg, v...) +} + +// FatalWithPrefix logs fatal messages with a specific prefix and exits the program +func (l *Logger) FatalWithPrefix(prefix, msg string, v ...interface{}) { + if l.level <= FATAL { + l.fatal.Printf(formatMessage(prefix, msg), v...) + os.Exit(1) // Exit the program with a non-zero status code + } +} + +// Wrapper functions to simplify logging with optional prefix + +func Debug(msg string, v ...interface{}) { + Log.Debug(msg, v...) +} + +func DebugWithPrefix(prefix, msg string, v ...interface{}) { + Log.DebugWithPrefix(prefix, msg, v...) +} + +func Info(msg string, v ...interface{}) { + Log.Info(msg, v...) +} + +func InfoWithPrefix(prefix, msg string, v ...interface{}) { + Log.InfoWithPrefix(prefix, msg, v...) +} + +func Warn(msg string, v ...interface{}) { + Log.Warn(msg, v...) +} + +func WarnWithPrefix(prefix, msg string, v ...interface{}) { + Log.WarnWithPrefix(prefix, msg, v...) +} + +func Error(msg string, v ...interface{}) { + Log.Error(msg, v...) +} + +func ErrorWithPrefix(prefix, msg string, v ...interface{}) { + Log.ErrorWithPrefix(prefix, msg, v...) +} + +func Fatal(msg string, v ...interface{}) { + Log.Fatal(msg, v...) +} + +func FatalWithPrefix(prefix, msg string, v ...interface{}) { + Log.FatalWithPrefix(prefix, msg, v...) +}