From bd25d686ee9b423c8cd1bdfb4210b23fc4f3e6c2 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Mon, 19 Oct 2026 16:11:51 +0530 Subject: [PATCH 1/3] feat(check): add erasure-coding threshold check --- config/config.yaml | 10 + config/local.yaml | 10 + config/testnet-bee-playground.yaml | 10 + go.mod | 14 +- go.sum | 70 ++++- pkg/check/erasurecoding/erasurecoding.go | 261 ++++++++++++++++++ pkg/check/erasurecoding/erasurecoding_test.go | 217 +++++++++++++++ pkg/check/erasurecoding/tree.go | 257 +++++++++++++++++ pkg/config/check.go | 26 ++ 9 files changed, 872 insertions(+), 3 deletions(-) create mode 100644 pkg/check/erasurecoding/erasurecoding.go create mode 100644 pkg/check/erasurecoding/erasurecoding_test.go create mode 100644 pkg/check/erasurecoding/tree.go diff --git a/config/config.yaml b/config/config.yaml index 5a806628..a016e7a9 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -311,6 +311,16 @@ checks: seed: data-size: type: redundancy + erasure-coding: + options: + levels: [1, 2, 3, 4] # medium, strong, insane, paranoid + data-sizes: [262144] + postage-ttl: 24h + postage-depth: 21 + postage-label: test-label + download-timeout: 60s + timeout: 1h + type: erasure-coding retrieval: options: chunks-per-node: 3 diff --git a/config/local.yaml b/config/local.yaml index d389ee2b..be1ce2a3 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -427,6 +427,16 @@ checks: seed: data-size: type: redundancy + ci-erasure-coding: + options: + levels: [1, 2, 3, 4] # medium, strong, insane, paranoid + data-sizes: [262144] + postage-ttl: 24h + postage-depth: 21 + postage-label: test-label + download-timeout: 60s + timeout: 1h + type: erasure-coding ci-gsoc: options: postage-ttl: 24h diff --git a/config/testnet-bee-playground.yaml b/config/testnet-bee-playground.yaml index f0a3bc80..1b127c98 100644 --- a/config/testnet-bee-playground.yaml +++ b/config/testnet-bee-playground.yaml @@ -172,6 +172,16 @@ checks: data-size: 307200 timeout: 30m type: redundancy + pg-erasure-coding: + options: + levels: [1, 2, 3, 4] # medium, strong, insane, paranoid + data-sizes: [262144] + postage-ttl: 120h # 5 days + postage-depth: 22 + postage-label: test-label + download-timeout: 60s + timeout: 1h + type: erasure-coding pg-testnet-load: options: content-size: 50000000 diff --git a/go.mod b/go.mod index fec09f5a..2b9dbfa0 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/caddyserver/certmagic v0.21.6 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/codahale/hdrhistogram v0.0.0-00010101000000-000000000000 // indirect @@ -75,8 +76,10 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -163,8 +166,13 @@ require ( github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect @@ -176,12 +184,15 @@ require ( golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -190,6 +201,7 @@ require ( k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect lukechampine.com/blake3 v1.4.1 // indirect + resenje.org/singleflight v0.4.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect diff --git a/go.sum b/go.sum index d9f2d8a1..1aadc736 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= +contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -44,6 +46,8 @@ github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZh github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -111,10 +115,19 @@ github.com/ethersphere/bmt v0.1.4 h1:+rkWYNtMgDx6bkNqGdWu+U9DgGI1rRZplpSW3YhBr1Q github.com/ethersphere/bmt v0.1.4/go.mod h1:Yd8ft1U69WDuHevZc/rwPxUv1rzPSMpMnS6xbU53aY8= github.com/ethersphere/ethproxy v0.0.5 h1:j5Mkm45jqmkET6NwGaJtaxOSFbhoAfOKzHiwHl6DBT0= github.com/ethersphere/ethproxy v0.0.5/go.mod h1:7mkVRK3+Mte00jLxFAbUQ/cBepAzwTYpkE64ItCLZYw= +github.com/ethersphere/go-storage-incentives-abi v0.9.4 h1:mSIWXQXg5OQmH10QvXMV5w0vbSibFMaRlBL37gPLTM0= +github.com/ethersphere/go-storage-incentives-abi v0.9.4/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc= github.com/ethersphere/go-sw3-abi v0.6.9 h1:TnWLnYkWE5UvC17mQBdUmdkzhPhO8GcqvWy4wvd1QJQ= github.com/ethersphere/go-sw3-abi v0.6.9/go.mod h1:BmpsvJ8idQZdYEtWnvxA8POYQ8Rl/NhyCdF0zLMOOJU= +<<<<<<< HEAD github.com/ethersphere/node-funder v0.4.0 h1:bfPjncf2QoGcosgqnWZAPLThWioGdFH4lpyhCzMtZqU= github.com/ethersphere/node-funder v0.4.0/go.mod h1:1n5y5yzYFWUAad5ocKmfbXvmTvnNN6r7V4awt36TD9Y= +======= +github.com/ethersphere/langos v1.0.0 h1:NBtNKzXTTRSue95uOlzPN4py7Aofs0xWPzyj4AI1Vcc= +github.com/ethersphere/langos v1.0.0/go.mod h1:dlcN2j4O8sQ+BlCaxeBu43bgr4RQ+inJ+pHwLeZg5Tw= +github.com/ethersphere/node-funder v0.3.0 h1:rZamN4hs53Rt8MdSYSb2HLNTNX6XGzntwcVyJkUySGU= +github.com/ethersphere/node-funder v0.3.0/go.mod h1:1n5y5yzYFWUAad5ocKmfbXvmTvnNN6r7V4awt36TD9Y= +>>>>>>> 16ae181 (feat(check): add erasure-coding threshold check) 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/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= @@ -143,6 +156,10 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -172,6 +189,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -194,6 +213,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -236,6 +257,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -407,6 +430,8 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8HanlO+2N/Wjv7w= +github.com/prometheus/statsd_exporter v0.26.1/go.mod h1:XlDdjAmRmx3JVvPPYuFNUg+Ynyb5kR69iPPkQjxXFMk= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -491,6 +516,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -499,16 +526,28 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/nolash/go-mockbytes v0.0.7 h1:9XVFpEfY67kGBVJve3uV19kzqORdlo7V+q09OE6Yo54= gitlab.com/nolash/go-mockbytes v0.0.7/go.mod h1:KKOpNTT39j2Eo+P6uUTOncntfeKY6AFh/2CxuD5MpgE= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= @@ -532,8 +571,10 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= @@ -550,17 +591,23 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= @@ -570,6 +617,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -597,15 +645,27 @@ golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -641,6 +701,12 @@ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzk k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +resenje.org/feed v0.1.2 h1:3OianQkoI4EalWx1SlzHtGjUMsoB4XTJQbeehWiyeFI= +resenje.org/feed v0.1.2/go.mod h1:ABlv4P3svuZY3dkZq3un+XIEoX+TDwbGEkjLcSP8TnM= +resenje.org/multex v0.1.0 h1:am9Ndt8dIAeGVaztD8ClsSX+e0EP3mj6UdsvjukKZig= +resenje.org/multex v0.1.0/go.mod h1:3rHOoMrzqLNzgGWPcl/1GfzN52g7iaPXhbvTQ8TjGaM= +resenje.org/singleflight v0.4.0 h1:NdOEhCxEikK2S2WxGjZV9EGSsItolQKslOOi6pE1tJc= +resenje.org/singleflight v0.4.0/go.mod h1:lAgQK7VfjG6/pgredbQfmV0RvG/uVhKo6vSuZ0vCWfk= resenje.org/x v0.6.0 h1:afn9E4XhglF4y9Kq0VH5tdSyjnsVKxiYgB6HFj7ebss= resenje.org/x v0.6.0/go.mod h1:qgwe4MCzh57EkkMDurg24ug7HHfZtAjtBkmCihNmOpM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/check/erasurecoding/erasurecoding.go b/pkg/check/erasurecoding/erasurecoding.go new file mode 100644 index 00000000..ef0d6d33 --- /dev/null +++ b/pkg/check/erasurecoding/erasurecoding.go @@ -0,0 +1,261 @@ +package erasurecoding + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/rand" + "strings" + "time" + + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/bee/api" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" + "golang.org/x/sync/errgroup" +) + +const uploadConcurrency = 16 + +// Options for the erasure-coding check. +type Options struct { + Levels []uint8 // redundancy levels to test: 1 medium, 2 strong, 3 insane, 4 paranoid + DataSizes []int64 // sizes of the random blobs to split + AllDepths bool // also test the intermediate chunks above the leaf level + PostageDepth uint64 // depth of the postage batch + PostageLabel string // label of the postage batch + PostageTTL time.Duration // lifetime of the postage batch + DownloadTimeout time.Duration // time allowed for each download + Seed int64 +} + +// NewDefaultOptions returns the default options. +func NewDefaultOptions() Options { + return Options{ + Levels: []uint8{uint8(redundancy.MEDIUM), uint8(redundancy.STRONG), uint8(redundancy.INSANE), uint8(redundancy.PARANOID)}, + DataSizes: []int64{256 * 1024}, // 256 KiB: one group at every redundancy level + PostageDepth: 22, + PostageLabel: "erasure-coding-check", + PostageTTL: 24 * time.Hour, + DownloadTimeout: 60 * time.Second, + Seed: time.Now().UnixNano(), + } +} + +var _ beekeeper.Action = (*Check)(nil) + +// Check uploads chunk trees with some chunks withheld, and checks that a download +// succeeds exactly when the loss is within what erasure coding can rebuild. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new erasure-coding check. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{logger: logger} +} + +// scenario is one upload and download. +type scenario struct { + level redundancy.Level + size int64 + group int + c Case + fallback bool +} + +func (s scenario) String() string { + return fmt.Sprintf("level %d, %d bytes, group %d, %s, fallback %v", s.level, s.size, s.group, s.c, s.fallback) +} + +// Run implements beekeeper.Action. +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, o any) error { + opts, ok := o.(Options) + if !ok { + return errors.New("invalid options type") + } + + if err := opts.validate(); err != nil { + return err + } + + uploader, downloader, err := clients(ctx, cluster, opts.Seed) + if err != nil { + return err + } + + batchID, err := uploader.GetOrCreateMutableBatch(ctx, opts.PostageTTL, opts.PostageDepth, opts.PostageLabel) + if err != nil { + return fmt.Errorf("get or create batch: %w", err) + } + + c.logger.Infof("uploading from %s, downloading from %s", uploader.Name(), downloader.Name()) + + rnd := random.PseudoGenerator(opts.Seed) + + var failures []string + total := 0 + + for _, lvl := range opts.Levels { + level := redundancy.Level(lvl) + + for _, size := range opts.DataSizes { + // A reference tree tells how many groups this size has; every scenario then + // splits fresh data of the same size, whose tree has the same shape. + ref, err := Split(ctx, randomData(rnd, size), level) + if err != nil { + return fmt.Errorf("level %d, %d bytes: %w", level, size, err) + } + + groups := len(ref.SelectGroups(opts.AllDepths)) + c.logger.Infof("level %d, %d bytes: %d chunks, %d groups under test", level, size, len(ref.Chunks), groups) + + for group := range groups { + for _, cs := range Cases { + // Fresh data for every upload: chunks left in the network or in the + // download node's cache by one scenario must not satisfy the next. + for _, fallback := range []bool{false, true} { + s := scenario{level: level, size: size, group: group, c: cs, fallback: fallback} + total++ + + if err := c.run(ctx, s, uploader, downloader, batchID, rnd, opts); err != nil { + if ctx.Err() != nil { + return fmt.Errorf("%s: %w", s, ctx.Err()) + } + c.logger.Errorf("FAIL %s: %v", s, err) + failures = append(failures, fmt.Sprintf("%s: %v", s, err)) + + continue + } + + c.logger.Infof("ok %s", s) + } + } + } + } + } + + if len(failures) > 0 { + return fmt.Errorf("%d of %d scenarios failed:\n %s", len(failures), total, strings.Join(failures, "\n ")) + } + + c.logger.Infof("all %d scenarios passed", total) + + return nil +} + +// run uploads a fresh tree minus the withheld chunks and checks the download against the expectation. +func (c *Check) run(ctx context.Context, s scenario, uploader, downloader *bee.Client, batchID string, rnd *rand.Rand, opts Options) error { + data := randomData(rnd, s.size) + + tree, err := Split(ctx, data, s.level) + if err != nil { + return err + } + + groups := tree.SelectGroups(opts.AllDepths) + if s.group >= len(groups) { + return fmt.Errorf("tree has %d groups under test, want group %d", len(groups), s.group) + } + + withheld := groups[s.group].Drops(s.c, rnd) + + if err := upload(ctx, uploader, batchID, tree.Chunks, withheld); err != nil { + return err + } + + dctx, cancel := context.WithTimeout(ctx, opts.DownloadTimeout) + defer cancel() + + got, dlErr := downloader.DownloadBytes(dctx, tree.Root, &api.DownloadOptions{RedundancyFallbackMode: &s.fallback}) + downloaded := dlErr == nil && bytes.Equal(got, data) + + c.logger.Debugf("%s: withheld %d of %d chunks, download ok %v, err %v", s, len(withheld), len(tree.Chunks), downloaded, dlErr) + + switch want := s.c.ExpectDownload(s.fallback); { + case want && !downloaded && dlErr != nil: + return fmt.Errorf("withheld %d of %d chunks, expected the download to succeed: %w", len(withheld), len(tree.Chunks), dlErr) + case want && !downloaded: + return fmt.Errorf("withheld %d of %d chunks, download returned different data", len(withheld), len(tree.Chunks)) + case !want && downloaded: + return fmt.Errorf("withheld %d of %d chunks, expected the download to fail but it returned the data", len(withheld), len(tree.Chunks)) + } + + return nil +} + +// upload sends every chunk except the withheld ones. +func upload(ctx context.Context, client *bee.Client, batchID string, chunks []swarm.Chunk, withheld []swarm.Address) error { + skip := make(map[string]bool, len(withheld)) + for _, a := range withheld { + skip[a.ByteString()] = true + } + + group, gctx := errgroup.WithContext(ctx) + group.SetLimit(uploadConcurrency) + + for _, ch := range chunks { + if skip[ch.Address().ByteString()] { + continue + } + + group.Go(func() error { + if _, err := client.UploadChunk(gctx, ch.Data(), api.UploadOptions{BatchID: batchID, Direct: true}); err != nil { + return fmt.Errorf("upload chunk %s: %w", ch.Address(), err) + } + + return nil + }) + } + + return group.Wait() +} + +func randomData(rnd *rand.Rand, size int64) []byte { + data := make([]byte, size) + _, _ = rnd.Read(data) + + return data +} + +func (o Options) validate() error { + if len(o.Levels) == 0 { + return errors.New("no redundancy levels configured") + } + for _, l := range o.Levels { + if level := redundancy.Level(l); level == redundancy.NONE || !level.Validate() { + return fmt.Errorf("redundancy level %d must be between 1 and 4", l) + } + } + if len(o.DataSizes) == 0 { + return errors.New("no data sizes configured") + } + for _, s := range o.DataSizes { + if s <= swarm.ChunkSize { + return fmt.Errorf("data size %d must be larger than one chunk (%d bytes) to have an intermediate chunk", s, swarm.ChunkSize) + } + } + if o.DownloadTimeout <= 0 { + return errors.New("download timeout must be positive") + } + + return nil +} + +// clients picks a node to upload from and a different node to download from. +func clients(ctx context.Context, cluster orchestration.Cluster, seed int64) (uploader, downloader *bee.Client, err error) { + nodes, err := cluster.ShuffledFullNodeClients(ctx, random.PseudoGenerator(seed)) + if err != nil { + return nil, nil, fmt.Errorf("get shuffled full node clients: %w", err) + } + if len(nodes) < 2 { + return nil, nil, errors.New("erasure-coding check needs at least two full nodes") + } + + return nodes[0], nodes[1], nil +} diff --git a/pkg/check/erasurecoding/erasurecoding_test.go b/pkg/check/erasurecoding/erasurecoding_test.go new file mode 100644 index 00000000..96963a43 --- /dev/null +++ b/pkg/check/erasurecoding/erasurecoding_test.go @@ -0,0 +1,217 @@ +package erasurecoding_test + +import ( + "bytes" + "context" + "io" + "math/rand" + "testing" + "time" + + "github.com/ethersphere/bee/v2/pkg/file/joiner" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/file/redundancy/getter" + "github.com/ethersphere/bee/v2/pkg/storage" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/check/erasurecoding" +) + +var levels = []redundancy.Level{redundancy.MEDIUM, redundancy.STRONG, redundancy.INSANE, redundancy.PARANOID} + +func randomData(t *testing.T, size int) []byte { + t.Helper() + + data := make([]byte, size) + if _, err := rand.New(rand.NewSource(int64(size))).Read(data); err != nil { + t.Fatal(err) + } + + return data +} + +// store serves the chunks of a tree except the withheld ones, like a network that never got them. +type store struct { + chunks map[string]swarm.Chunk +} + +func newStore(tree *erasurecoding.Tree, withheld []swarm.Address) *store { + s := &store{chunks: make(map[string]swarm.Chunk)} + for _, ch := range tree.Chunks { + s.chunks[ch.Address().ByteString()] = ch + } + for _, a := range withheld { + delete(s.chunks, a.ByteString()) + } + + return s +} + +func (s *store) Get(_ context.Context, a swarm.Address) (swarm.Chunk, error) { + if ch, ok := s.chunks[a.ByteString()]; ok { + return ch, nil + } + + return nil, storage.ErrNotFound +} + +func (s *store) Put(context.Context, swarm.Chunk) error { return nil } + +// join reads the blob back through Bee's joiner, the code path a node uses on download. +func join(t *testing.T, tree *erasurecoding.Tree, withheld []swarm.Address, fallback bool) ([]byte, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + ctx = getter.SetStrict(ctx, !fallback) + + s := newStore(tree, withheld) + + j, _, err := joiner.New(ctx, s, s, tree.Root, tree.Level) + if err != nil { + return nil, err + } + + return io.ReadAll(io.NewSectionReader(j, 0, j.Size())) +} + +func groupChildren(tree *erasurecoding.Tree) int { + n := 0 + for _, g := range tree.Groups { + n += len(g.Data) + len(g.Parity) + } + + return n +} + +func TestSplit_groups(t *testing.T) { + t.Parallel() + + for _, level := range levels { + for _, size := range []int{300 * 1024, 1024 * 1024} { + tree, err := erasurecoding.Split(context.Background(), randomData(t, size), level) + if err != nil { + t.Fatalf("level %d size %d: %v", level, size, err) + } + + if len(tree.Groups) == 0 { + t.Fatalf("level %d size %d: no intermediate chunks", level, size) + } + + have := make(map[string]bool, len(tree.Chunks)) + for _, ch := range tree.Chunks { + have[ch.Address().ByteString()] = true + } + + for _, g := range tree.Groups { + if len(g.Parity) == 0 { + t.Errorf("level %d size %d: group at depth %d has no parities", level, size, g.Depth) + } + for _, a := range append(append([]swarm.Address{}, g.Data...), g.Parity...) { + if !have[a.ByteString()] { + t.Errorf("level %d size %d: child %s was not written by the splitter", level, size, a) + } + } + } + + if want := 1 + groupChildren(tree); len(tree.Chunks) != want { + t.Errorf("level %d size %d: %d chunks, want %d (root plus every child, no root replicas)", level, size, len(tree.Chunks), want) + } + + if tree.Groups[0].Depth != 0 { + t.Errorf("level %d size %d: first group depth = %d, want the root at 0", level, size, tree.Groups[0].Depth) + } + } + } +} + +func TestSplit_twoTreeLevels(t *testing.T) { + t.Parallel() + + for _, level := range levels { + tree, err := erasurecoding.Split(context.Background(), randomData(t, 1024*1024), level) + if err != nil { + t.Fatal(err) + } + + if got := len(tree.SelectGroups(true)); got < 2 { + t.Errorf("level %d: 1 MiB gives %d selectable groups, want at least 2 tree levels", level, got) + } + } +} + +func TestGroupDrops_counts(t *testing.T) { + t.Parallel() + + for _, level := range levels { + tree, err := erasurecoding.Split(context.Background(), randomData(t, 1024*1024), level) + if err != nil { + t.Fatal(err) + } + + for _, g := range tree.SelectGroups(true) { + inGroup := make(map[string]bool) + for _, a := range append(append([]swarm.Address{}, g.Data...), g.Parity...) { + inGroup[a.ByteString()] = true + } + + for _, c := range erasurecoding.Cases { + drops := g.Drops(c, rand.New(rand.NewSource(1))) + + want := map[erasurecoding.Case]int{ + erasurecoding.ParityOnly: len(g.Parity), + erasurecoding.AtThreshold: len(g.Parity), + erasurecoding.OverThreshold: len(g.Parity) + 1, + }[c] + if len(drops) != want { + t.Errorf("level %d depth %d %s: %d drops, want %d", level, g.Depth, c, len(drops), want) + } + + seen := make(map[string]bool) + for _, a := range drops { + if a.Equal(tree.Root) { + t.Errorf("level %d depth %d %s: root chunk is withheld", level, g.Depth, c) + } + if !inGroup[a.ByteString()] { + t.Errorf("level %d depth %d %s: %s is not a child of the group", level, g.Depth, c, a) + } + if seen[a.ByteString()] { + t.Errorf("level %d depth %d %s: %s is withheld twice", level, g.Depth, c, a) + } + seen[a.ByteString()] = true + } + } + } + } +} + +// TestGroupDrops_matchBeeRecovery is the check's expectation table run against Bee's +// own joiner, with no network: it proves the thresholds before they are tried on a cluster. +func TestGroupDrops_matchBeeRecovery(t *testing.T) { + t.Parallel() + + for _, level := range levels { + data := randomData(t, 1024*1024) + + tree, err := erasurecoding.Split(context.Background(), data, level) + if err != nil { + t.Fatal(err) + } + + for _, g := range tree.SelectGroups(true) { + for _, c := range erasurecoding.Cases { + drops := g.Drops(c, rand.New(rand.NewSource(7))) + + for _, fallback := range []bool{true, false} { + got, err := join(t, tree, drops, fallback) + gotOK := err == nil && bytes.Equal(got, data) + + if want := c.ExpectDownload(fallback); gotOK != want { + t.Errorf("level %d depth %d %s fallback=%v: download ok = %v (err %v), want %v", + level, g.Depth, c, fallback, gotOK, err, want) + } + } + } + } + } +} diff --git a/pkg/check/erasurecoding/tree.go b/pkg/check/erasurecoding/tree.go new file mode 100644 index 00000000..bbc2a6b6 --- /dev/null +++ b/pkg/check/erasurecoding/tree.go @@ -0,0 +1,257 @@ +package erasurecoding + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "math/rand" + "sync" + + "github.com/ethersphere/bee/v2/pkg/file" + "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" + "github.com/ethersphere/bee/v2/pkg/file/redundancy" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +// Case is how many children of one intermediate chunk are withheld from the upload. +type Case int + +const ( + // ParityOnly withholds every parity chunk, so all data chunks are still present. + ParityOnly Case = iota + // AtThreshold withholds as many children as there are parities, the most that can be + // rebuilt: exactly as many children survive as there are data chunks. + AtThreshold + // OverThreshold withholds one child more, so fewer children survive than there are data + // chunks and the group cannot be rebuilt. + OverThreshold +) + +// Cases lists every case in the order the check runs them. +var Cases = []Case{ParityOnly, AtThreshold, OverThreshold} + +func (c Case) String() string { + switch c { + case ParityOnly: + return "parity-only" + case AtThreshold: + return "at-threshold" + case OverThreshold: + return "over-threshold" + default: + return fmt.Sprintf("case(%d)", int(c)) + } +} + +// ExpectDownload reports whether the download should succeed for the case. With the +// fallback off Bee fetches data chunks only, so any missing data chunk fails it. +func (c Case) ExpectDownload(fallback bool) bool { + switch c { + case ParityOnly: + return true + case AtThreshold: + return fallback + default: + return false + } +} + +// Group is the children of one intermediate chunk: data references followed by parity references. +type Group struct { + Depth int // distance from the root chunk, which is depth 0 + LeafParent bool // all data children are leaf data chunks + Data []swarm.Address + Parity []swarm.Address +} + +// Drops picks the chunk addresses to withhold for the case. Any set of children is +// recoverable while at least len(Data) of the len(Data)+len(Parity) survive, so the +// threshold is len(Parity) withheld children. Data chunks are withheld first, so a data +// chunk is always missing and the download has to rebuild it; PARANOID has more parities +// than data chunks, so parities make up the rest there. +func (g Group) Drops(c Case, rnd *rand.Rand) []swarm.Address { + switch c { + case ParityOnly: + return g.Parity + case AtThreshold: + return g.withhold(len(g.Parity), rnd) + case OverThreshold: + return g.withhold(len(g.Parity)+1, rnd) + default: + return nil + } +} + +// withhold picks n children at random, data chunks first. +func (g Group) withhold(n int, rnd *rand.Rand) []swarm.Address { + nData := min(n, len(g.Data)) + + picked := make([]swarm.Address, 0, n) + for _, i := range rnd.Perm(len(g.Data))[:nData] { + picked = append(picked, g.Data[i]) + } + for _, i := range rnd.Perm(len(g.Parity))[:n-nData] { + picked = append(picked, g.Parity[i]) + } + + return picked +} + +// Tree is the chunk tree of one split blob. +type Tree struct { + Root swarm.Address + Level redundancy.Level + Chunks []swarm.Chunk + Groups []Group +} + +// Split builds the chunk tree for data with Bee's splitter, keeping every chunk in memory. +func Split(ctx context.Context, data []byte, level redundancy.Level) (*Tree, error) { + store := &memStore{chunks: make(map[string]swarm.Chunk)} + + pipe := builder.NewPipelineBuilder(ctx, store, false, level) + root, err := builder.FeedPipeline(ctx, pipe, bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("split data: %w", err) + } + + groups, err := buildGroups(store.chunks, root, level) + if err != nil { + return nil, fmt.Errorf("read tree: %w", err) + } + + return &Tree{Root: root, Level: level, Chunks: treeChunks(store.list, root, groups), Groups: groups}, nil +} + +// treeChunks keeps only the chunks reachable from the root, in the order they were written. +// The splitter also writes signed replicas of the root chunk, which are not part of the +// tree and must not be uploaded as plain chunks. +func treeChunks(written []swarm.Chunk, root swarm.Address, groups []Group) []swarm.Chunk { + inTree := map[string]bool{root.ByteString(): true} + for _, g := range groups { + for _, a := range g.Data { + inTree[a.ByteString()] = true + } + for _, a := range g.Parity { + inTree[a.ByteString()] = true + } + } + + chunks := make([]swarm.Chunk, 0, len(inTree)) + for _, ch := range written { + if inTree[ch.Address().ByteString()] { + chunks = append(chunks, ch) + } + } + + return chunks +} + +// SelectGroups returns the groups to test: the first one whose children are leaf data +// chunks and, when all is set, the first one at every other depth too. +func (t *Tree) SelectGroups(all bool) []Group { + var selected []Group + + seenDepth := make(map[int]bool) + leafDone := false + + for _, g := range t.Groups { + switch { + case g.LeafParent && !leafDone: + leafDone = true + seenDepth[g.Depth] = true + selected = append(selected, g) + case all && !g.LeafParent && !seenDepth[g.Depth]: + seenDepth[g.Depth] = true + selected = append(selected, g) + } + } + + return selected +} + +// memStore collects the chunks the splitter writes. The splitter calls Put from several +// goroutines, so access is locked. +type memStore struct { + mu sync.Mutex + chunks map[string]swarm.Chunk + list []swarm.Chunk +} + +func (s *memStore) Put(_ context.Context, ch swarm.Chunk) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.chunks[ch.Address().ByteString()]; !ok { + s.chunks[ch.Address().ByteString()] = ch + s.list = append(s.list, ch) + } + + return nil +} + +// chunkSpan returns the data length a chunk covers; the level encoded in the span is dropped. +func chunkSpan(ch swarm.Chunk) uint64 { + _, span := redundancy.DecodeSpan(ch.Data()[:swarm.SpanSize]) + + return binary.LittleEndian.Uint64(span) +} + +// buildGroups walks the tree from the root and returns one Group per intermediate chunk. +func buildGroups(chunks map[string]swarm.Chunk, root swarm.Address, level redundancy.Level) ([]Group, error) { + type node struct { + addr swarm.Address + depth int + } + + var groups []Group + + queue := []node{{addr: root}} + for len(queue) > 0 { + n := queue[0] + queue = queue[1:] + + ch, ok := chunks[n.addr.ByteString()] + if !ok { + return nil, fmt.Errorf("chunk %s missing from the split output", n.addr) + } + + span := chunkSpan(ch) + if span <= swarm.ChunkSize { + continue // data chunk + } + + payload := ch.Data()[swarm.SpanSize:] + + size, err := file.ChunkPayloadSize(payload) + if err != nil { + return nil, fmt.Errorf("chunk %s: %w", n.addr, err) + } + + dataCnt, parityCnt := file.ReferenceCount(span, level, false) + addrs, shardCnt := file.ChunkAddresses(payload[:size], parityCnt, swarm.HashSize) + + if shardCnt != dataCnt || len(addrs) != dataCnt+parityCnt { + return nil, fmt.Errorf("chunk %s: found %d data and %d parity references, want %d and %d", + n.addr, shardCnt, len(addrs)-shardCnt, dataCnt, parityCnt) + } + + g := Group{Depth: n.depth, LeafParent: true, Data: addrs[:shardCnt], Parity: addrs[shardCnt:]} + + for _, child := range g.Data { + cc, ok := chunks[child.ByteString()] + if !ok { + return nil, fmt.Errorf("chunk %s missing from the split output", child) + } + if chunkSpan(cc) > swarm.ChunkSize { + g.LeafParent = false + queue = append(queue, node{addr: child, depth: n.depth + 1}) + } + } + + groups = append(groups, g) + } + + return groups, nil +} diff --git a/pkg/config/check.go b/pkg/config/check.go index 541afb7e..ef45e6e3 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -13,6 +13,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/balances" "github.com/ethersphere/beekeeper/pkg/check/cashout" "github.com/ethersphere/beekeeper/pkg/check/datadurability" + "github.com/ethersphere/beekeeper/pkg/check/erasurecoding" "github.com/ethersphere/beekeeper/pkg/check/feed" "github.com/ethersphere/beekeeper/pkg/check/fileretrieval" "github.com/ethersphere/beekeeper/pkg/check/fullconnectivity" @@ -655,6 +656,31 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "erasure-coding": { + NewAction: erasurecoding.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + Levels *[]uint8 `yaml:"levels"` + DataSizes *[]int64 `yaml:"data-sizes"` + AllDepths *bool `yaml:"all-depths"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + PostageTTL *time.Duration `yaml:"postage-ttl"` + DownloadTimeout *time.Duration `yaml:"download-timeout"` + Seed *int64 `yaml:"seed"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := erasurecoding.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "withdraw": { NewAction: withdraw.NewCheck, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { From f510639e94e92ba4df8277989db06ac441603380 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Mon, 19 Oct 2026 18:28:42 +0530 Subject: [PATCH 2/3] fix(check): use 32 KiB data size for erasure-coding check --- config/config.yaml | 2 +- config/local.yaml | 2 +- config/testnet-bee-playground.yaml | 2 +- pkg/check/erasurecoding/erasurecoding.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index a016e7a9..4f8c6fdf 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -314,7 +314,7 @@ checks: erasure-coding: options: levels: [1, 2, 3, 4] # medium, strong, insane, paranoid - data-sizes: [262144] + data-sizes: [32768] postage-ttl: 24h postage-depth: 21 postage-label: test-label diff --git a/config/local.yaml b/config/local.yaml index be1ce2a3..43e75bd7 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -430,7 +430,7 @@ checks: ci-erasure-coding: options: levels: [1, 2, 3, 4] # medium, strong, insane, paranoid - data-sizes: [262144] + data-sizes: [32768] postage-ttl: 24h postage-depth: 21 postage-label: test-label diff --git a/config/testnet-bee-playground.yaml b/config/testnet-bee-playground.yaml index 1b127c98..8004d7c3 100644 --- a/config/testnet-bee-playground.yaml +++ b/config/testnet-bee-playground.yaml @@ -175,7 +175,7 @@ checks: pg-erasure-coding: options: levels: [1, 2, 3, 4] # medium, strong, insane, paranoid - data-sizes: [262144] + data-sizes: [32768] postage-ttl: 120h # 5 days postage-depth: 22 postage-label: test-label diff --git a/pkg/check/erasurecoding/erasurecoding.go b/pkg/check/erasurecoding/erasurecoding.go index ef0d6d33..7b843aa2 100644 --- a/pkg/check/erasurecoding/erasurecoding.go +++ b/pkg/check/erasurecoding/erasurecoding.go @@ -38,7 +38,7 @@ type Options struct { func NewDefaultOptions() Options { return Options{ Levels: []uint8{uint8(redundancy.MEDIUM), uint8(redundancy.STRONG), uint8(redundancy.INSANE), uint8(redundancy.PARANOID)}, - DataSizes: []int64{256 * 1024}, // 256 KiB: one group at every redundancy level + DataSizes: []int64{32 * 1024}, // 32 KiB: one group at every redundancy level PostageDepth: 22, PostageLabel: "erasure-coding-check", PostageTTL: 24 * time.Hour, From 430f7c662fd7f3a45996611da399380db60f1369 Mon Sep 17 00:00:00 2001 From: Alok Nerurkar Date: Mon, 26 Oct 2026 19:13:15 +0530 Subject: [PATCH 3/3] fix: rebase --- go.sum | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/go.sum b/go.sum index 1aadc736..76028468 100644 --- a/go.sum +++ b/go.sum @@ -119,15 +119,10 @@ github.com/ethersphere/go-storage-incentives-abi v0.9.4 h1:mSIWXQXg5OQmH10QvXMV5 github.com/ethersphere/go-storage-incentives-abi v0.9.4/go.mod h1:SXvJVtM4sEsaSKD0jc1ClpDLw8ErPoROZDme4Wrc/Nc= github.com/ethersphere/go-sw3-abi v0.6.9 h1:TnWLnYkWE5UvC17mQBdUmdkzhPhO8GcqvWy4wvd1QJQ= github.com/ethersphere/go-sw3-abi v0.6.9/go.mod h1:BmpsvJ8idQZdYEtWnvxA8POYQ8Rl/NhyCdF0zLMOOJU= -<<<<<<< HEAD -github.com/ethersphere/node-funder v0.4.0 h1:bfPjncf2QoGcosgqnWZAPLThWioGdFH4lpyhCzMtZqU= -github.com/ethersphere/node-funder v0.4.0/go.mod h1:1n5y5yzYFWUAad5ocKmfbXvmTvnNN6r7V4awt36TD9Y= -======= github.com/ethersphere/langos v1.0.0 h1:NBtNKzXTTRSue95uOlzPN4py7Aofs0xWPzyj4AI1Vcc= github.com/ethersphere/langos v1.0.0/go.mod h1:dlcN2j4O8sQ+BlCaxeBu43bgr4RQ+inJ+pHwLeZg5Tw= -github.com/ethersphere/node-funder v0.3.0 h1:rZamN4hs53Rt8MdSYSb2HLNTNX6XGzntwcVyJkUySGU= -github.com/ethersphere/node-funder v0.3.0/go.mod h1:1n5y5yzYFWUAad5ocKmfbXvmTvnNN6r7V4awt36TD9Y= ->>>>>>> 16ae181 (feat(check): add erasure-coding threshold check) +github.com/ethersphere/node-funder v0.4.0 h1:bfPjncf2QoGcosgqnWZAPLThWioGdFH4lpyhCzMtZqU= +github.com/ethersphere/node-funder v0.4.0/go.mod h1:1n5y5yzYFWUAad5ocKmfbXvmTvnNN6r7V4awt36TD9Y= 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/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=