From 780a281600d76f18818f9abd73ae872fa42edfb2 Mon Sep 17 00:00:00 2001 From: Marketen Date: Mon, 2 Jun 2025 18:35:25 +0200 Subject: [PATCH 1/8] get attesterduties poc --- go.mod | 48 ++++++++++++++++++ go.sum | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 53 ++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go 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/main.go b/main.go new file mode 100644 index 0000000..c342f57 --- /dev/null +++ b/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/rs/zerolog" +) + +func main() { + // Provide a cancellable context to the creation function. + ctx, _ := context.WithCancel(context.Background()) + client, err := http.New(ctx, + // WithAddress supplies the address of the beacon node, as a URL. + http.WithAddress("http://beacon-chain.lighthouse-hoodi.dappnode:3500/"), + // LogLevel supplies the level of logging to carry out. + http.WithLogLevel(zerolog.WarnLevel), + ) + if err != nil { + panic(err) + } + + fmt.Printf("Connected to %s\n", client.Name()) + + currentFinality, error := client.(eth2client.FinalityProvider).Finality(ctx, &api.FinalityOpts{ + State: "head", + }) + if error != nil { + fmt.Printf("Error retrieving finality: %v\n", error) + } + finalizedCheckpoint := currentFinality.Data.Finalized.Epoch // uint64 + + //Check attestation duties for my validator and epoch = finalizedCheckpoint + myValidatorIndex := uint64(1) // Replace with your validator index + attestationDuties, err := client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{ + Epoch: finalizedCheckpoint, + Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(myValidatorIndex)}, + }) + if err != nil { + fmt.Printf("Error retrieving attester duties: %v\n", err) + return + } + + attestationSlot := attestationDuties.Data[0].Slot // phase0.Slot + attestationCommitteeIndex := attestationDuties.Data[0].CommitteeIndex // phase0.CommitteeIndex + + fmt.Printf("Attestation Slot: %d, Committee Index: %d\n", attestationSlot, attestationCommitteeIndex) + +} From 3ebdf9f6614cffe3653b6cf045dad08aa0668691 Mon Sep 17 00:00:00 2001 From: Marketen Date: Mon, 2 Jun 2025 19:01:47 +0200 Subject: [PATCH 2/8] wip --- main.go | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index c342f57..6f8d04b 100644 --- a/main.go +++ b/main.go @@ -45,9 +45,48 @@ func main() { return } + fmt.Println("Attestation Duties complete:", attestationDuties) attestationSlot := attestationDuties.Data[0].Slot // phase0.Slot attestationCommitteeIndex := attestationDuties.Data[0].CommitteeIndex // phase0.CommitteeIndex - + attestationCommitteePosition := attestationDuties.Data[0].ValidatorCommitteeIndex fmt.Printf("Attestation Slot: %d, Committee Index: %d\n", attestationSlot, attestationCommitteeIndex) + included := false + // Search up to 3 slots after the duty slot. + for i := 1; i <= 3; i++ { + block, err := client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ + Block: fmt.Sprintf("%d", attestationSlot), //says block but its slot actually + }) + if err != nil || block == nil { + continue + } + + attestations := block.Data.Phase0.Message.Body.Attestations + for _, att := range attestations { + // Check if attestation matches the duty slot and committee + if att.Data.Slot == attestationSlot && att.Data.Index == attestationCommitteeIndex { + aggregationBits := att.AggregationBits + byteIndex := attestationCommitteePosition / 8 + bitIndex := attestationCommitteePosition % 8 + if int(byteIndex) < len(aggregationBits) { + bit := (aggregationBits[int(byteIndex)] >> bitIndex) & 1 + if bit == 1 { + included = true + fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", attestationSlot) + break + } + } + } + } + if included { + break + } + } + + if included { + fmt.Println("✅ Validator attested successfully!") + } else { + fmt.Println("❌ Validator missed attestation duty in this finalized epoch.") + } + } From 30d0535bb8969094460319dd66d15ec3be1f5cb5 Mon Sep 17 00:00:00 2001 From: Marketen Date: Mon, 2 Jun 2025 19:18:36 +0200 Subject: [PATCH 3/8] workingwip --- main.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 6f8d04b..f8f189d 100644 --- a/main.go +++ b/main.go @@ -53,17 +53,26 @@ func main() { included := false // Search up to 3 slots after the duty slot. - for i := 1; i <= 3; i++ { + for i := 1; i <= 32; i++ { + slotToCheck := uint64(attestationSlot) + uint64(i) + fmt.Printf("Checking slot %d...\n", slotToCheck) block, err := client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ - Block: fmt.Sprintf("%d", attestationSlot), //says block but its slot actually + Block: fmt.Sprintf("%d", slotToCheck), }) - if err != nil || block == nil { + if err != nil || block == nil || block.Data == nil { continue } - attestations := block.Data.Phase0.Message.Body.Attestations + // Get Electra block data + electraBlock := block.Data.Electra + if electraBlock == nil { + continue // no electra block data + } + + fmt.Printf("Found block for slot %d (electra block slot: %d)\n", slotToCheck, electraBlock.Message.Slot) + + attestations := electraBlock.Message.Body.Attestations for _, att := range attestations { - // Check if attestation matches the duty slot and committee if att.Data.Slot == attestationSlot && att.Data.Index == attestationCommitteeIndex { aggregationBits := att.AggregationBits byteIndex := attestationCommitteePosition / 8 @@ -71,8 +80,8 @@ func main() { if int(byteIndex) < len(aggregationBits) { bit := (aggregationBits[int(byteIndex)] >> bitIndex) & 1 if bit == 1 { + fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", slotToCheck) included = true - fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", attestationSlot) break } } From f7b392bf3fc3b9d01bd99af7331885b808fb91ab Mon Sep 17 00:00:00 2001 From: Marketen Date: Tue, 3 Jun 2025 18:37:49 +0200 Subject: [PATCH 4/8] committee 0 --- main.go | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index f8f189d..a859067 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" "github.com/attestantio/go-eth2-client/http" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -26,17 +25,21 @@ func main() { fmt.Printf("Connected to %s\n", client.Name()) - currentFinality, error := client.(eth2client.FinalityProvider).Finality(ctx, &api.FinalityOpts{ + consensusClient := client.(*http.Service) + + // Retrieve the current finalized checkpoint. It contains latest finalized epoch. + currentFinality, error := consensusClient.Finality(ctx, &api.FinalityOpts{ State: "head", }) if error != nil { fmt.Printf("Error retrieving finality: %v\n", error) } finalizedCheckpoint := currentFinality.Data.Finalized.Epoch // uint64 + fmt.Println("Current finalized Epoch:", finalizedCheckpoint) //Check attestation duties for my validator and epoch = finalizedCheckpoint - myValidatorIndex := uint64(1) // Replace with your validator index - attestationDuties, err := client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{ + myValidatorIndex := uint64(12345) // Replace with your validator index + attestationDuties, err := consensusClient.AttesterDuties(ctx, &api.AttesterDutiesOpts{ Epoch: finalizedCheckpoint, Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(myValidatorIndex)}, }) @@ -49,39 +52,41 @@ func main() { attestationSlot := attestationDuties.Data[0].Slot // phase0.Slot attestationCommitteeIndex := attestationDuties.Data[0].CommitteeIndex // phase0.CommitteeIndex attestationCommitteePosition := attestationDuties.Data[0].ValidatorCommitteeIndex - fmt.Printf("Attestation Slot: %d, Committee Index: %d\n", attestationSlot, attestationCommitteeIndex) + fmt.Printf("Attestation Slot: %d, Committee Index: %d, Validator Committee Index: %d\n", + attestationSlot, attestationCommitteeIndex, attestationCommitteePosition) included := false // Search up to 3 slots after the duty slot. - for i := 1; i <= 32; i++ { - slotToCheck := uint64(attestationSlot) + uint64(i) - fmt.Printf("Checking slot %d...\n", slotToCheck) - block, err := client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ - Block: fmt.Sprintf("%d", slotToCheck), + for i := 0; i <= 32; i++ { + blockSlot := attestationSlot + phase0.Slot(i) + slotStr := fmt.Sprintf("%d", blockSlot) + fmt.Printf("🔍 Requesting block at slot: %s\n", slotStr) + + block, err := consensusClient.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ + Block: slotStr, }) - if err != nil || block == nil || block.Data == nil { + if err != nil { + fmt.Printf("❌ Error for slot %d: %v\n", blockSlot, err) continue } - - // Get Electra block data - electraBlock := block.Data.Electra - if electraBlock == nil { - continue // no electra block data + if block == nil { + fmt.Printf("❌ No block returned for slot %d\n", blockSlot) + continue } - - fmt.Printf("Found block for slot %d (electra block slot: %d)\n", slotToCheck, electraBlock.Message.Slot) - - attestations := electraBlock.Message.Body.Attestations + fmt.Printf("✅ Got block with actual slot: %d\n", block.Data.Electra.Message.Slot) + attestations := block.Data.Electra.Message.Body.Attestations for _, att := range attestations { + fmt.Printf("Block slot %d: Attestation data slot: %d, index: %d\n", blockSlot, att.Data.Slot, att.Data.Index) if att.Data.Slot == attestationSlot && att.Data.Index == attestationCommitteeIndex { + fmt.Println("🔍 Found matching attestation for slot and committee.") aggregationBits := att.AggregationBits byteIndex := attestationCommitteePosition / 8 bitIndex := attestationCommitteePosition % 8 if int(byteIndex) < len(aggregationBits) { bit := (aggregationBits[int(byteIndex)] >> bitIndex) & 1 if bit == 1 { - fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", slotToCheck) included = true + fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", attestationSlot) break } } From a763f17eba3bfd1d1b842f00eba9b5415d6fc15b Mon Sep 17 00:00:00 2001 From: Marketen Date: Tue, 3 Jun 2025 23:04:09 +0200 Subject: [PATCH 5/8] attestation working v1 --- main.go | 355 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 277 insertions(+), 78 deletions(-) diff --git a/main.go b/main.go index a859067..7ca7110 100644 --- a/main.go +++ b/main.go @@ -1,106 +1,305 @@ +// main.go +// +// This program checks whether a given validator (by index) performed its attestation duties +// correctly in the latest finalized epoch, using a Lighthouse (DappNode) Beacon API endpoint. +// It scans the next 5 slots after the assigned duty slot (since attestations can appear a few slots later). +// +// Usage: +// go run main.go +// +// Example: +// go run main.go 500502 + package main import ( - "context" + "bytes" + "encoding/hex" + "encoding/json" "fmt" - - "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/http" - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/rs/zerolog" + "io/ioutil" + "net/http" + "os" + "sort" + "strconv" + "strings" ) +// Replace with your Beacon API base URL +const baseURL = "http://beacon-chain.lighthouse-hoodi.dappnode:3500" + +// --- Structs for JSON unmarshalling --- + +// FinalityCheckpointsResponse models the response for +// GET /eth/v1/beacon/states/{state_id}/finality_checkpoints +type FinalityCheckpointsResponse struct { + Data struct { + Finalized struct { + Epoch string `json:"epoch"` + } `json:"finalized"` + } `json:"data"` +} + +// AttesterDutiesResponse models the response for +// POST /eth/v1/validator/duties/attester/{epoch} +type AttesterDutiesResponse struct { + Data []struct { + ValidatorIndex string `json:"validator_index"` + CommitteeIndex string `json:"committee_index"` + CommitteeLength string `json:"committee_length"` + CommitteesAtSlot string `json:"committees_at_slot"` + ValidatorCommitteeIdx string `json:"validator_committee_index"` + Slot string `json:"slot"` + } `json:"data"` +} + +// CommitteeEntry models one committee entry in +// GET /eth/v1/beacon/states/{state_id}/committees +type CommitteeEntry struct { + Index string `json:"index"` + Slot string `json:"slot"` + Validators []string `json:"validators"` +} + +// CommitteesResponse models the response for +// GET /eth/v1/beacon/states/{state_id}/committees +type CommitteesResponse struct { + Data []CommitteeEntry `json:"data"` +} + +// BlockAttestationsResponse models the response for +// GET /eth/v2/beacon/blocks/{block_id}/attestations +type BlockAttestationsResponse struct { + Data []struct { + AggregationBits string `json:"aggregation_bits"` // hex string, e.g. "0x..." + CommitteeBits string `json:"committee_bits"` // hex string, e.g. "0x..." + Data struct { + Slot string `json:"slot"` // the slot that this attestation is for + } `json:"data"` + } `json:"data"` +} + +// --- Helper functions --- + +// parseHexBitvector converts a hex-encoded bitvector (with "0x" prefix) into a byte slice. +// Bits are interpreted in little-endian order within each byte. +func parseHexBitvector(hexstr string) ([]byte, error) { + trimmed := strings.TrimPrefix(hexstr, "0x") + if len(trimmed)%2 != 0 { + // must be even length + trimmed = "0" + trimmed + } + return hex.DecodeString(trimmed) +} + +// getBitLE returns the bit (0 or 1) at position bitIndex in the little-endian bitvector stored in data. +// bitIndex = 0 refers to least-significant bit of data[0], bitIndex=8 refers to LSB of data[1], etc. +func getBitLE(data []byte, bitIndex int) int { + byteIdx := bitIndex / 8 + if byteIdx < 0 || byteIdx >= len(data) { + return 0 + } + b := data[byteIdx] + return int((b >> (bitIndex % 8)) & 1) +} + +// httpGetJSON sends a GET request to the given URL and unmarshals the JSON response into outStruct. +func httpGetJSON(url string, outStruct interface{}) error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("GET %s: %v", url, err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("GET %s: status %d: %s", url, resp.StatusCode, string(body)) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading response body: %v", err) + } + if err := json.Unmarshal(data, outStruct); err != nil { + return fmt.Errorf("unmarshal JSON: %v\nJSON was: %s", err, string(data)) + } + return nil +} + +// httpPostJSON sends a POST request with a JSON payload (body) to the given URL and unmarshals the JSON response into outStruct. +func httpPostJSON(url string, body []byte, outStruct interface{}) error { + resp, err := http.Post(url, "application/json", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("POST %s: %v", url, err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + respData, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("POST %s: status %d: %s", url, resp.StatusCode, string(respData)) + } + respData, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading response body: %v", err) + } + if err := json.Unmarshal(respData, outStruct); err != nil { + return fmt.Errorf("unmarshal JSON: %v\nJSON was: %s", err, string(respData)) + } + return nil +} + func main() { - // Provide a cancellable context to the creation function. - ctx, _ := context.WithCancel(context.Background()) - client, err := http.New(ctx, - // WithAddress supplies the address of the beacon node, as a URL. - http.WithAddress("http://beacon-chain.lighthouse-hoodi.dappnode:3500/"), - // LogLevel supplies the level of logging to carry out. - http.WithLogLevel(zerolog.WarnLevel), - ) + // 1) Parse validator index from command-line + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: go run main.go \n") + os.Exit(1) + } + validatorIndexStr := os.Args[1] + validatorIndex, err := strconv.ParseUint(validatorIndexStr, 10, 64) if err != nil { - panic(err) + fmt.Fprintf(os.Stderr, "Invalid validator index: %v\n", err) + os.Exit(1) } - fmt.Printf("Connected to %s\n", client.Name()) + // 2) Fetch finalized epoch (use “head” to learn the latest finalized checkpoint) + finalityURL := fmt.Sprintf("%s/eth/v1/beacon/states/head/finality_checkpoints", baseURL) + var finResp FinalityCheckpointsResponse + if err := httpGetJSON(finalityURL, &finResp); err != nil { + fmt.Fprintf(os.Stderr, "Error fetching finality checkpoints: %v\n", err) + os.Exit(1) + } + finalEpoch, err := strconv.ParseUint(finResp.Data.Finalized.Epoch, 10, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing finalized epoch: %v\n", err) + os.Exit(1) + } + fmt.Printf("Latest finalized epoch: %d\n", finalEpoch) - consensusClient := client.(*http.Service) + // 3) Fetch attester duties for that epoch, for this validator + dutiesURL := fmt.Sprintf("%s/eth/v1/validator/duties/attester/%d", baseURL, finalEpoch) + reqBody, _ := json.Marshal([]string{validatorIndexStr}) - // Retrieve the current finalized checkpoint. It contains latest finalized epoch. - currentFinality, error := consensusClient.Finality(ctx, &api.FinalityOpts{ - State: "head", - }) - if error != nil { - fmt.Printf("Error retrieving finality: %v\n", error) + var dutiesResp AttesterDutiesResponse + if err := httpPostJSON(dutiesURL, reqBody, &dutiesResp); err != nil { + fmt.Fprintf(os.Stderr, "Error fetching attester duties: %v\n", err) + os.Exit(1) } - finalizedCheckpoint := currentFinality.Data.Finalized.Epoch // uint64 - fmt.Println("Current finalized Epoch:", finalizedCheckpoint) + if len(dutiesResp.Data) == 0 { + fmt.Fprintf(os.Stderr, "No attester duties found for validator %d in epoch %d\n", validatorIndex, finalEpoch) + os.Exit(1) + } + // We expect exactly one duty entry for this validator in the given epoch + duty := dutiesResp.Data[0] + commIdx, _ := strconv.ParseUint(duty.CommitteeIndex, 10, 64) + slot, _ := strconv.ParseUint(duty.Slot, 10, 64) + valCommIdx, _ := strconv.ParseUint(duty.ValidatorCommitteeIdx, 10, 64) - //Check attestation duties for my validator and epoch = finalizedCheckpoint - myValidatorIndex := uint64(12345) // Replace with your validator index - attestationDuties, err := consensusClient.AttesterDuties(ctx, &api.AttesterDutiesOpts{ - Epoch: finalizedCheckpoint, - Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(myValidatorIndex)}, - }) - if err != nil { - fmt.Printf("Error retrieving attester duties: %v\n", err) - return - } - - fmt.Println("Attestation Duties complete:", attestationDuties) - attestationSlot := attestationDuties.Data[0].Slot // phase0.Slot - attestationCommitteeIndex := attestationDuties.Data[0].CommitteeIndex // phase0.CommitteeIndex - attestationCommitteePosition := attestationDuties.Data[0].ValidatorCommitteeIndex - fmt.Printf("Attestation Slot: %d, Committee Index: %d, Validator Committee Index: %d\n", - attestationSlot, attestationCommitteeIndex, attestationCommitteePosition) - - included := false - // Search up to 3 slots after the duty slot. - for i := 0; i <= 32; i++ { - blockSlot := attestationSlot + phase0.Slot(i) - slotStr := fmt.Sprintf("%d", blockSlot) - fmt.Printf("🔍 Requesting block at slot: %s\n", slotStr) - - block, err := consensusClient.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{ - Block: slotStr, - }) + fmt.Printf("Validator %d duties for epoch %d: slot=%d, committee_index=%d, validator_committee_index=%d\n", + validatorIndex, finalEpoch, slot, commIdx, valCommIdx) + + // 4) Fetch committees for the finalized state at that slot + commURL := fmt.Sprintf("%s/eth/v1/beacon/states/finalized/committees?slot=%d", baseURL, slot) + var commResp CommitteesResponse + if err := httpGetJSON(commURL, &commResp); err != nil { + fmt.Fprintf(os.Stderr, "Error fetching committees: %v\n", err) + os.Exit(1) + } + if len(commResp.Data) == 0 { + fmt.Fprintf(os.Stderr, "No committees found for slot %d (finalized state)\n", slot) + os.Exit(1) + } + + // Build a map of committee_index -> validator list + commMap := make(map[uint64][]string) + for _, entry := range commResp.Data { + idx, _ := strconv.ParseUint(entry.Index, 10, 64) + commMap[idx] = entry.Validators + } + if _, ok := commMap[commIdx]; !ok { + fmt.Fprintf(os.Stderr, "Committee index %d not found in committees for slot %d\n", commIdx, slot) + os.Exit(1) + } + + // Compute offset: sum sizes of all committees with index < commIdx + var indices []uint64 + for idx := range commMap { + indices = append(indices, idx) + } + sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] }) + + var offset uint64 + for _, idx := range indices { + if idx == commIdx { + break + } + offset += uint64(len(commMap[idx])) + } + fmt.Printf("Computed aggregation bit offset for committee %d: %d\n", commIdx, offset) + + // 5) Scan the next 5 slots (slot+1 .. slot+5) for attestations + // Because attestations may be included a few slots after the duty slot. + const scanWindow = 5 + attested := false + + for b := slot + 1; b <= slot+scanWindow; b++ { + blockAttURL := fmt.Sprintf("%s/eth/v2/beacon/blocks/%d/attestations", baseURL, b) + var blockAttResp BlockAttestationsResponse + + err := httpGetJSON(blockAttURL, &blockAttResp) if err != nil { - fmt.Printf("❌ Error for slot %d: %v\n", blockSlot, err) + // If the block doesn't exist or has no attestations endpoint, skip quietly + fmt.Fprintf(os.Stderr, " [slot %d] warning: could not fetch attestations: %v\n", b, err) continue } - if block == nil { - fmt.Printf("❌ No block returned for slot %d\n", blockSlot) + if len(blockAttResp.Data) == 0 { + // No attestations in this block; keep scanning continue } - fmt.Printf("✅ Got block with actual slot: %d\n", block.Data.Electra.Message.Slot) - attestations := block.Data.Electra.Message.Body.Attestations - for _, att := range attestations { - fmt.Printf("Block slot %d: Attestation data slot: %d, index: %d\n", blockSlot, att.Data.Slot, att.Data.Index) - if att.Data.Slot == attestationSlot && att.Data.Index == attestationCommitteeIndex { - fmt.Println("🔍 Found matching attestation for slot and committee.") - aggregationBits := att.AggregationBits - byteIndex := attestationCommitteePosition / 8 - bitIndex := attestationCommitteePosition % 8 - if int(byteIndex) < len(aggregationBits) { - bit := (aggregationBits[int(byteIndex)] >> bitIndex) & 1 - if bit == 1 { - included = true - fmt.Printf("✅ Validator's attestation was included in block at slot %d!\n", attestationSlot) - break - } - } + + // Inspect each attestation in the block + for _, att := range blockAttResp.Data { + // 1) Parse the attestation’s declared slot, and skip if it’s not our duty slot + attSlot, err := strconv.ParseUint(att.Data.Slot, 10, 64) + if err != nil { + // If parsing fails, skip + continue + } + if attSlot != slot { + // This attestation is for some other slot, so skip + continue + } + + // 2) Check committee_bits + cbBytes, err := parseHexBitvector(att.CommitteeBits) + if err != nil { + fmt.Fprintf(os.Stderr, " error parsing committee_bits at slot %d: %v\n", b, err) + continue + } + if getBitLE(cbBytes, int(commIdx)) == 0 { + continue + } + + // 3) Parse aggregation_bits and check our bit + aggBytes, err := parseHexBitvector(att.AggregationBits) + if err != nil { + fmt.Fprintf(os.Stderr, " error parsing aggregation_bits at slot %d: %v\n", b, err) + continue + } + targetBit := int(offset + valCommIdx) + if getBitLE(aggBytes, targetBit) == 1 { + fmt.Printf(" ▶️ Found validator bit for slot %d in block %d.\n", slot, b) + attested = true + break } } - if included { + + if attested { break } } - if included { - fmt.Println("✅ Validator attested successfully!") + // 6) Report final result + if attested { + fmt.Printf("✅ Validator %d DID attest correctly for slot %d (epoch %d).\n", validatorIndex, slot, finalEpoch) } else { - fmt.Println("❌ Validator missed attestation duty in this finalized epoch.") + fmt.Printf("❌ Validator %d did NOT attest for slot %d within the next %d slots (epoch %d).\n", + validatorIndex, slot, scanWindow, finalEpoch) } - } From 8c02b1d1577c972c9d478fad263eaa02d8bcf9dc Mon Sep 17 00:00:00 2001 From: Marketen Date: Wed, 4 Jun 2025 00:25:24 +0200 Subject: [PATCH 6/8] using attestantio --- main.go | 368 ++++++++++++++++---------------------------------------- 1 file changed, 104 insertions(+), 264 deletions(-) diff --git a/main.go b/main.go index 7ca7110..759e68a 100644 --- a/main.go +++ b/main.go @@ -1,305 +1,145 @@ -// main.go -// -// This program checks whether a given validator (by index) performed its attestation duties -// correctly in the latest finalized epoch, using a Lighthouse (DappNode) Beacon API endpoint. -// It scans the next 5 slots after the assigned duty slot (since attestations can appear a few slots later). -// -// Usage: -// go run main.go -// -// Example: -// go run main.go 500502 - package main import ( - "bytes" - "encoding/hex" - "encoding/json" + "context" "fmt" - "io/ioutil" - "net/http" - "os" - "sort" - "strconv" - "strings" -) - -// Replace with your Beacon API base URL -const baseURL = "http://beacon-chain.lighthouse-hoodi.dappnode:3500" - -// --- Structs for JSON unmarshalling --- - -// FinalityCheckpointsResponse models the response for -// GET /eth/v1/beacon/states/{state_id}/finality_checkpoints -type FinalityCheckpointsResponse struct { - Data struct { - Finalized struct { - Epoch string `json:"epoch"` - } `json:"finalized"` - } `json:"data"` -} - -// AttesterDutiesResponse models the response for -// POST /eth/v1/validator/duties/attester/{epoch} -type AttesterDutiesResponse struct { - Data []struct { - ValidatorIndex string `json:"validator_index"` - CommitteeIndex string `json:"committee_index"` - CommitteeLength string `json:"committee_length"` - CommitteesAtSlot string `json:"committees_at_slot"` - ValidatorCommitteeIdx string `json:"validator_committee_index"` - Slot string `json:"slot"` - } `json:"data"` -} + "time" -// CommitteeEntry models one committee entry in -// GET /eth/v1/beacon/states/{state_id}/committees -type CommitteeEntry struct { - Index string `json:"index"` - Slot string `json:"slot"` - Validators []string `json:"validators"` -} - -// CommitteesResponse models the response for -// GET /eth/v1/beacon/states/{state_id}/committees -type CommitteesResponse struct { - Data []CommitteeEntry `json:"data"` -} - -// BlockAttestationsResponse models the response for -// GET /eth/v2/beacon/blocks/{block_id}/attestations -type BlockAttestationsResponse struct { - Data []struct { - AggregationBits string `json:"aggregation_bits"` // hex string, e.g. "0x..." - CommitteeBits string `json:"committee_bits"` // hex string, e.g. "0x..." - Data struct { - Slot string `json:"slot"` // the slot that this attestation is for - } `json:"data"` - } `json:"data"` -} - -// --- Helper functions --- - -// parseHexBitvector converts a hex-encoded bitvector (with "0x" prefix) into a byte slice. -// Bits are interpreted in little-endian order within each byte. -func parseHexBitvector(hexstr string) ([]byte, error) { - trimmed := strings.TrimPrefix(hexstr, "0x") - if len(trimmed)%2 != 0 { - // must be even length - trimmed = "0" + trimmed - } - return hex.DecodeString(trimmed) -} - -// getBitLE returns the bit (0 or 1) at position bitIndex in the little-endian bitvector stored in data. -// bitIndex = 0 refers to least-significant bit of data[0], bitIndex=8 refers to LSB of data[1], etc. -func getBitLE(data []byte, bitIndex int) int { - byteIdx := bitIndex / 8 - if byteIdx < 0 || byteIdx >= len(data) { - return 0 - } - b := data[byteIdx] - return int((b >> (bitIndex % 8)) & 1) -} - -// httpGetJSON sends a GET request to the given URL and unmarshals the JSON response into outStruct. -func httpGetJSON(url string, outStruct interface{}) error { - resp, err := http.Get(url) - if err != nil { - return fmt.Errorf("GET %s: %v", url, err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - body, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("GET %s: status %d: %s", url, resp.StatusCode, string(body)) - } - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("reading response body: %v", err) - } - if err := json.Unmarshal(data, outStruct); err != nil { - return fmt.Errorf("unmarshal JSON: %v\nJSON was: %s", err, string(data)) - } - return nil -} + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/rs/zerolog" +) -// httpPostJSON sends a POST request with a JSON payload (body) to the given URL and unmarshals the JSON response into outStruct. -func httpPostJSON(url string, body []byte, outStruct interface{}) error { - resp, err := http.Post(url, "application/json", bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("POST %s: %v", url, err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - respData, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("POST %s: status %d: %s", url, resp.StatusCode, string(respData)) - } - respData, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("reading response body: %v", err) - } - if err := json.Unmarshal(respData, outStruct); err != nil { - return fmt.Errorf("unmarshal JSON: %v\nJSON was: %s", err, string(respData)) - } - return nil -} +const validatorIndex = 480347 // Replace with the actual validator index you want to query func main() { - // 1) Parse validator index from command-line - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: go run main.go \n") - os.Exit(1) - } - validatorIndexStr := os.Args[1] - validatorIndex, err := strconv.ParseUint(validatorIndexStr, 10, 64) - if err != nil { - fmt.Fprintf(os.Stderr, "Invalid validator index: %v\n", err) - os.Exit(1) - } - - // 2) Fetch finalized epoch (use “head” to learn the latest finalized checkpoint) - finalityURL := fmt.Sprintf("%s/eth/v1/beacon/states/head/finality_checkpoints", baseURL) - var finResp FinalityCheckpointsResponse - if err := httpGetJSON(finalityURL, &finResp); err != nil { - fmt.Fprintf(os.Stderr, "Error fetching finality checkpoints: %v\n", err) - os.Exit(1) - } - finalEpoch, err := strconv.ParseUint(finResp.Data.Finalized.Epoch, 10, 64) + // Provide a cancellable context to the creation function. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client, err := http.New(ctx, + // WithAddress supplies the address of the beacon node, as a URL. + http.WithAddress("http://beacon-chain.lighthouse-hoodi.dappnode:3500/"), + // LogLevel supplies the level of logging to carry out. + http.WithLogLevel(zerolog.WarnLevel), + ) if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing finalized epoch: %v\n", err) - os.Exit(1) + panic(err) } - fmt.Printf("Latest finalized epoch: %d\n", finalEpoch) - // 3) Fetch attester duties for that epoch, for this validator - dutiesURL := fmt.Sprintf("%s/eth/v1/validator/duties/attester/%d", baseURL, finalEpoch) - reqBody, _ := json.Marshal([]string{validatorIndexStr}) + consensusClient := client.(*http.Service) + fmt.Printf("Connected to %s\n", client.Name()) - var dutiesResp AttesterDutiesResponse - if err := httpPostJSON(dutiesURL, reqBody, &dutiesResp); err != nil { - fmt.Fprintf(os.Stderr, "Error fetching attester duties: %v\n", err) - os.Exit(1) + finalityCheckpoint, _ := consensusClient.Finality(context.Background(), &api.FinalityOpts{ + State: "head", + }) + if finalityCheckpoint == nil { + fmt.Println("No finality checkpoint found.") + return } - if len(dutiesResp.Data) == 0 { - fmt.Fprintf(os.Stderr, "No attester duties found for validator %d in epoch %d\n", validatorIndex, finalEpoch) - os.Exit(1) - } - // We expect exactly one duty entry for this validator in the given epoch - duty := dutiesResp.Data[0] - commIdx, _ := strconv.ParseUint(duty.CommitteeIndex, 10, 64) - slot, _ := strconv.ParseUint(duty.Slot, 10, 64) - valCommIdx, _ := strconv.ParseUint(duty.ValidatorCommitteeIdx, 10, 64) - fmt.Printf("Validator %d duties for epoch %d: slot=%d, committee_index=%d, validator_committee_index=%d\n", - validatorIndex, finalEpoch, slot, commIdx, valCommIdx) + finalityEpoch := finalityCheckpoint.Data.Finalized.Epoch + fmt.Printf("Finality checkpoint at epoch %d\n", finalityEpoch) - // 4) Fetch committees for the finalized state at that slot - commURL := fmt.Sprintf("%s/eth/v1/beacon/states/finalized/committees?slot=%d", baseURL, slot) - var commResp CommitteesResponse - if err := httpGetJSON(commURL, &commResp); err != nil { - fmt.Fprintf(os.Stderr, "Error fetching committees: %v\n", err) - os.Exit(1) - } - if len(commResp.Data) == 0 { - fmt.Fprintf(os.Stderr, "No committees found for slot %d (finalized state)\n", slot) - os.Exit(1) + validatorDuties, err := consensusClient.AttesterDuties(context.Background(), + &api.AttesterDutiesOpts{ + Epoch: finalityEpoch, + Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(validatorIndex)}, + }) + if err != nil { + fmt.Printf("Error fetching attester duties: %v\n", err) + return } - // Build a map of committee_index -> validator list - commMap := make(map[uint64][]string) - for _, entry := range commResp.Data { - idx, _ := strconv.ParseUint(entry.Index, 10, 64) - commMap[idx] = entry.Validators - } - if _, ok := commMap[commIdx]; !ok { - fmt.Fprintf(os.Stderr, "Committee index %d not found in committees for slot %d\n", commIdx, slot) - os.Exit(1) - } + committeeIndex := validatorDuties.Data[0].CommitteeIndex + ValidatorCommitteeIdx := validatorDuties.Data[0].ValidatorCommitteeIndex + slotToAttest := validatorDuties.Data[0].Slot - // Compute offset: sum sizes of all committees with index < commIdx - var indices []uint64 - for idx := range commMap { - indices = append(indices, idx) - } - sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] }) + fmt.Printf("Validator %d is in committee %d at slot %d and validator committee index %d \n", + validatorIndex, committeeIndex, slotToAttest, ValidatorCommitteeIdx) - var offset uint64 - for _, idx := range indices { - if idx == commIdx { - break - } - offset += uint64(len(commMap[idx])) + // Retrieve all beacon committees defined for the slotToAttest + // This is necessary to know how many validators are in each committee. + completeCommittees, err := consensusClient.BeaconCommittees(context.Background(), + &api.BeaconCommitteesOpts{ + State: fmt.Sprintf("%d", slotToAttest), + }) + if err != nil { + fmt.Printf("Error fetching beacon committees: %v\n", err) + return } - fmt.Printf("Computed aggregation bit offset for committee %d: %d\n", commIdx, offset) - - // 5) Scan the next 5 slots (slot+1 .. slot+5) for attestations - // Because attestations may be included a few slots after the duty slot. - const scanWindow = 5 - attested := false - - for b := slot + 1; b <= slot+scanWindow; b++ { - blockAttURL := fmt.Sprintf("%s/eth/v2/beacon/blocks/%d/attestations", baseURL, b) - var blockAttResp BlockAttestationsResponse - err := httpGetJSON(blockAttURL, &blockAttResp) - if err != nil { - // If the block doesn't exist or has no attestations endpoint, skip quietly - fmt.Fprintf(os.Stderr, " [slot %d] warning: could not fetch attestations: %v\n", b, err) + // Store in a map the number of validators in each committee for the slotToAttest + committeeSizeMap := make(map[phase0.CommitteeIndex]int) + for _, committee := range completeCommittees.Data { + if committee.Slot != slotToAttest { continue } - if len(blockAttResp.Data) == 0 { - // No attestations in this block; keep scanning + committeeSizeMap[committee.Index] = len(committee.Validators) + } + // Print the committee sizes for debuggin + + // Get the attestations for the slots slotToAttest +1 to slotToAttest + 4 + // This is overkill. The attestant library doesnt have an endpoint to get only the attestations for a specific slot, we have to get the full slot block. + // loop over slots slotToAttest+1 to slotToAttest+4 and perform the check + for slot := slotToAttest + 1; slot <= slotToAttest+4; slot++ { + fullBlock, err := consensusClient.SignedBeaconBlock(context.Background(), + &api.SignedBeaconBlockOpts{ + Block: fmt.Sprintf("%d", slot), + }) + if err != nil { + fmt.Printf("Error fetching signed beacon block at slot %d: %v\n", slot, err) continue } - // Inspect each attestation in the block - for _, att := range blockAttResp.Data { - // 1) Parse the attestation’s declared slot, and skip if it’s not our duty slot - attSlot, err := strconv.ParseUint(att.Data.Slot, 10, 64) - if err != nil { - // If parsing fails, skip - continue - } - if attSlot != slot { - // This attestation is for some other slot, so skip + attestations := fullBlock.Data.Electra.Message.Body.Attestations + fmt.Printf("Checking %d attestations in block at slot %d...\n", len(attestations), slot) + + // Iterate over the attestations and check if there is an attestation that matches the following criteria: + // - Attestation data slot is equal to slotToAttest + // - committeeBit is 1 for the "committeeIndex" + // - AggregationBit is 1 for the "ValidatorCommitteeIdx" + // You will need to calculate the committeeBit suposing that there can be 64 committees. + // You will need to take into account that the aggregationBit is a bitlist of the validators in each committee ordered. This means that if + // the committee has 64 validators, the first 64 bits of the aggregationBit correspond to the first committee, the next 64 bits to the second committee, and so on. + for _, attestation := range attestations { + if attestation.Data.Slot != slotToAttest { continue } - // 2) Check committee_bits - cbBytes, err := parseHexBitvector(att.CommitteeBits) - if err != nil { - fmt.Fprintf(os.Stderr, " error parsing committee_bits at slot %d: %v\n", b, err) + // Check if the committeeBit is set for the committeeIndex + if !isBitSet(attestation.CommitteeBits, int(committeeIndex)) { continue } - if getBitLE(cbBytes, int(commIdx)) == 0 { - continue + + // Calculate bit position in aggregation bits + bitPosition := 0 + for index, size := range committeeSizeMap { + if index < committeeIndex { + bitPosition += size + } } + bitPosition += int(ValidatorCommitteeIdx) - // 3) Parse aggregation_bits and check our bit - aggBytes, err := parseHexBitvector(att.AggregationBits) - if err != nil { - fmt.Fprintf(os.Stderr, " error parsing aggregation_bits at slot %d: %v\n", b, err) + if !isBitSet(attestation.AggregationBits, bitPosition) { continue } - targetBit := int(offset + valCommIdx) - if getBitLE(aggBytes, targetBit) == 1 { - fmt.Printf(" ▶️ Found validator bit for slot %d in block %d.\n", slot, b) - attested = true - break - } - } - if attested { - break + fmt.Printf("✅ Found attestation for validator %d in committee %d for slot %d (included in block %d)\n", + validatorIndex, committeeIndex, slotToAttest, slot) } } +} + +// isBitSet returns true if the bit at position 'index' is set (1) in the given byte slice. +func isBitSet(bits []byte, index int) bool { + byteIndex := index / 8 + bitIndex := index % 8 - // 6) Report final result - if attested { - fmt.Printf("✅ Validator %d DID attest correctly for slot %d (epoch %d).\n", validatorIndex, slot, finalEpoch) - } else { - fmt.Printf("❌ Validator %d did NOT attest for slot %d within the next %d slots (epoch %d).\n", - validatorIndex, slot, scanWindow, finalEpoch) + if byteIndex >= len(bits) { + return false } + + return (bits[byteIndex] & (1 << uint(bitIndex))) != 0 } From fa5d7d4ec1474c3a64b8dec28197c92e23368493 Mon Sep 17 00:00:00 2001 From: Marketen Date: Wed, 4 Jun 2025 01:45:30 +0200 Subject: [PATCH 7/8] compute offset with ordered comitees --- main.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 759e68a..349b9ca 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "sort" "time" "github.com/attestantio/go-eth2-client/api" @@ -11,7 +12,7 @@ import ( "github.com/rs/zerolog" ) -const validatorIndex = 480347 // Replace with the actual validator index you want to query +const validatorIndex = 480349 // Replace with the actual validator index you want to query func main() { // Provide a cancellable context to the creation function. @@ -56,7 +57,7 @@ func main() { ValidatorCommitteeIdx := validatorDuties.Data[0].ValidatorCommitteeIndex slotToAttest := validatorDuties.Data[0].Slot - fmt.Printf("Validator %d is in committee %d at slot %d and validator committee index %d \n", + fmt.Printf("Validator %d is in committee %d, has to attest for the slot %d and its validator committee index is %d \n", validatorIndex, committeeIndex, slotToAttest, ValidatorCommitteeIdx) // Retrieve all beacon committees defined for the slotToAttest @@ -77,13 +78,14 @@ func main() { continue } committeeSizeMap[committee.Index] = len(committee.Validators) + // fmt.Printf("Committee %d has %d validators\n", committee.Index, len(committee.Validators)) } // Print the committee sizes for debuggin // Get the attestations for the slots slotToAttest +1 to slotToAttest + 4 // This is overkill. The attestant library doesnt have an endpoint to get only the attestations for a specific slot, we have to get the full slot block. - // loop over slots slotToAttest+1 to slotToAttest+4 and perform the check - for slot := slotToAttest + 1; slot <= slotToAttest+4; slot++ { + // loop over slots slotToAttest+1 to slotToAttest+32 and perform the check + for slot := slotToAttest + 1; slot <= slotToAttest+32; slot++ { fullBlock, err := consensusClient.SignedBeaconBlock(context.Background(), &api.SignedBeaconBlockOpts{ Block: fmt.Sprintf("%d", slot), @@ -113,11 +115,21 @@ func main() { continue } - // Calculate bit position in aggregation bits + // Sort committee indices + var indices []phase0.CommitteeIndex + for index := range committeeSizeMap { + + indices = append(indices, index) + } + sort.Slice(indices, func(i, j int) bool { + return indices[i] < indices[j] + }) + + // Compute offset by iterating in order bitPosition := 0 - for index, size := range committeeSizeMap { + for _, index := range indices { if index < committeeIndex { - bitPosition += size + bitPosition += committeeSizeMap[index] } } bitPosition += int(ValidatorCommitteeIdx) From 3df7296390866da63a19a3b069dec40c9b42eda2 Mon Sep 17 00:00:00 2001 From: Marketen Date: Sat, 7 Jun 2025 21:15:02 +0200 Subject: [PATCH 8/8] hex arch + multiple validators --- cmd/main.go | 80 ++++++++ internal/adapters/attestantclient_adapter.go | 187 ++++++++++++++++++ internal/adapters/web3signer_adapter.go | 46 +++++ internal/application/domain/validator.go | 22 +++ internal/application/ports/beaconchain.go | 16 ++ internal/application/ports/web3signer.go | 5 + .../services/attestation_oracle.go | 137 +++++++++++++ internal/config/config_loader.go | 46 +++++ internal/logger/logger.go | 177 +++++++++++++++++ main.go | 157 --------------- 10 files changed, 716 insertions(+), 157 deletions(-) create mode 100644 cmd/main.go create mode 100644 internal/adapters/attestantclient_adapter.go create mode 100644 internal/adapters/web3signer_adapter.go create mode 100644 internal/application/domain/validator.go create mode 100644 internal/application/ports/beaconchain.go create mode 100644 internal/application/ports/web3signer.go create mode 100644 internal/application/services/attestation_oracle.go create mode 100644 internal/config/config_loader.go create mode 100644 internal/logger/logger.go delete mode 100644 main.go 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/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...) +} diff --git a/main.go b/main.go deleted file mode 100644 index 349b9ca..0000000 --- a/main.go +++ /dev/null @@ -1,157 +0,0 @@ -package main - -import ( - "context" - "fmt" - "sort" - "time" - - "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/http" - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/rs/zerolog" -) - -const validatorIndex = 480349 // Replace with the actual validator index you want to query - -func main() { - // Provide a cancellable context to the creation function. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - client, err := http.New(ctx, - // WithAddress supplies the address of the beacon node, as a URL. - http.WithAddress("http://beacon-chain.lighthouse-hoodi.dappnode:3500/"), - // LogLevel supplies the level of logging to carry out. - http.WithLogLevel(zerolog.WarnLevel), - ) - if err != nil { - panic(err) - } - - consensusClient := client.(*http.Service) - fmt.Printf("Connected to %s\n", client.Name()) - - finalityCheckpoint, _ := consensusClient.Finality(context.Background(), &api.FinalityOpts{ - State: "head", - }) - if finalityCheckpoint == nil { - fmt.Println("No finality checkpoint found.") - return - } - - finalityEpoch := finalityCheckpoint.Data.Finalized.Epoch - fmt.Printf("Finality checkpoint at epoch %d\n", finalityEpoch) - - validatorDuties, err := consensusClient.AttesterDuties(context.Background(), - &api.AttesterDutiesOpts{ - Epoch: finalityEpoch, - Indices: []phase0.ValidatorIndex{phase0.ValidatorIndex(validatorIndex)}, - }) - if err != nil { - fmt.Printf("Error fetching attester duties: %v\n", err) - return - } - - committeeIndex := validatorDuties.Data[0].CommitteeIndex - ValidatorCommitteeIdx := validatorDuties.Data[0].ValidatorCommitteeIndex - slotToAttest := validatorDuties.Data[0].Slot - - fmt.Printf("Validator %d is in committee %d, has to attest for the slot %d and its validator committee index is %d \n", - validatorIndex, committeeIndex, slotToAttest, ValidatorCommitteeIdx) - - // Retrieve all beacon committees defined for the slotToAttest - // This is necessary to know how many validators are in each committee. - completeCommittees, err := consensusClient.BeaconCommittees(context.Background(), - &api.BeaconCommitteesOpts{ - State: fmt.Sprintf("%d", slotToAttest), - }) - if err != nil { - fmt.Printf("Error fetching beacon committees: %v\n", err) - return - } - - // Store in a map the number of validators in each committee for the slotToAttest - committeeSizeMap := make(map[phase0.CommitteeIndex]int) - for _, committee := range completeCommittees.Data { - if committee.Slot != slotToAttest { - continue - } - committeeSizeMap[committee.Index] = len(committee.Validators) - // fmt.Printf("Committee %d has %d validators\n", committee.Index, len(committee.Validators)) - } - // Print the committee sizes for debuggin - - // Get the attestations for the slots slotToAttest +1 to slotToAttest + 4 - // This is overkill. The attestant library doesnt have an endpoint to get only the attestations for a specific slot, we have to get the full slot block. - // loop over slots slotToAttest+1 to slotToAttest+32 and perform the check - for slot := slotToAttest + 1; slot <= slotToAttest+32; slot++ { - fullBlock, err := consensusClient.SignedBeaconBlock(context.Background(), - &api.SignedBeaconBlockOpts{ - Block: fmt.Sprintf("%d", slot), - }) - if err != nil { - fmt.Printf("Error fetching signed beacon block at slot %d: %v\n", slot, err) - continue - } - - attestations := fullBlock.Data.Electra.Message.Body.Attestations - fmt.Printf("Checking %d attestations in block at slot %d...\n", len(attestations), slot) - - // Iterate over the attestations and check if there is an attestation that matches the following criteria: - // - Attestation data slot is equal to slotToAttest - // - committeeBit is 1 for the "committeeIndex" - // - AggregationBit is 1 for the "ValidatorCommitteeIdx" - // You will need to calculate the committeeBit suposing that there can be 64 committees. - // You will need to take into account that the aggregationBit is a bitlist of the validators in each committee ordered. This means that if - // the committee has 64 validators, the first 64 bits of the aggregationBit correspond to the first committee, the next 64 bits to the second committee, and so on. - for _, attestation := range attestations { - if attestation.Data.Slot != slotToAttest { - continue - } - - // Check if the committeeBit is set for the committeeIndex - if !isBitSet(attestation.CommitteeBits, int(committeeIndex)) { - continue - } - - // Sort committee indices - var indices []phase0.CommitteeIndex - for index := range committeeSizeMap { - - indices = append(indices, index) - } - sort.Slice(indices, func(i, j int) bool { - return indices[i] < indices[j] - }) - - // Compute offset by iterating in order - bitPosition := 0 - for _, index := range indices { - if index < committeeIndex { - bitPosition += committeeSizeMap[index] - } - } - bitPosition += int(ValidatorCommitteeIdx) - - if !isBitSet(attestation.AggregationBits, bitPosition) { - continue - } - - fmt.Printf("✅ Found attestation for validator %d in committee %d for slot %d (included in block %d)\n", - validatorIndex, committeeIndex, slotToAttest, slot) - } - } -} - -// isBitSet returns true if the bit at position 'index' is set (1) in the given byte slice. -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 -}