diff --git a/config/config.yaml b/config/config.yaml index 5a806628..615ae79e 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -355,6 +355,17 @@ checks: request-timeout: 5m timeout: 5m type: soc + socmatrix: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: socmatrix-label + sync-wait: 45s + sync-retry-interval: 2s + request-timeout: 15m + password: beekeeper + timeout: 20m + type: socmatrix stake: type: stake timeout: 5m diff --git a/config/local.yaml b/config/local.yaml index d389ee2b..a504e855 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -382,6 +382,17 @@ checks: request-timeout: 5m timeout: 5m type: soc + ci-socmatrix: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: socmatrix-label + sync-wait: 45s # wait after upload for push/pull sync before neighborhood assert (precision 11–16) + sync-retry-interval: 2s + request-timeout: 45m + password: beekeeper + timeout: 60m + type: socmatrix ci-content-availability: type: content-availability timeout: 5m diff --git a/go.mod b/go.mod index cfc1d5c9..d4c9557d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26 replace github.com/codahale/hdrhistogram => github.com/HdrHistogram/hdrhistogram-go v1.1.2 require ( - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.3 github.com/ethersphere/bee/v2 v2.7.0 github.com/ethersphere/bmt v0.1.4 github.com/ethersphere/ethproxy v0.0.5 @@ -15,7 +15,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/ipshipyard/p2p-forge v0.7.0 - github.com/libp2p/go-libp2p v0.46.0 + github.com/libp2p/go-libp2p v0.48.0 github.com/miekg/dns v1.1.66 github.com/multiformats/go-multiaddr v0.16.1 github.com/multiformats/go-multibase v0.2.0 @@ -51,7 +51,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect @@ -59,7 +59,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/ethersphere/go-sw3-abi v0.6.9 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -96,7 +96,7 @@ require ( github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-netroute v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -118,21 +118,18 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.1.4 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtcp v1.2.16 // indirect github.com/pion/rtp v1.8.19 // indirect github.com/pion/sctp v1.8.39 // indirect - github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.1.5 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/transport/v4 v4.0.2 // indirect github.com/pion/turn/v4 v4.0.2 // indirect @@ -152,7 +149,7 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/uber/jaeger-lib v2.2.0+incompatible // indirect @@ -163,9 +160,9 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.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 diff --git a/go.sum b/go.sum index ed99f4ab..bddb4f72 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= @@ -59,8 +63,8 @@ github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDd github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= @@ -80,6 +84,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= @@ -88,12 +94,12 @@ github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= github.com/ethersphere/bee/v2 v2.7.0 h1:faaB3WP60SNrZvBCz2xh5sSIc9ka3Cz/ApWyedqBvQA= github.com/ethersphere/bee/v2 v2.7.0/go.mod h1:6Hhfttnet1vQmSydC9bivOKtUmaCarT3Mqi9Slj2tkg= github.com/ethersphere/bmt v0.1.4 h1:+rkWYNtMgDx6bkNqGdWu+U9DgGI1rRZplpSW3YhBr1Q= @@ -246,24 +252,24 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.46.0 h1:0T2yvIKpZ3DVYCuPOFxPD1layhRU486pj9rSlGWYnDM= -github.com/libp2p/go-libp2p v0.46.0/go.mod h1:TbIDnpDjBLa7isdgYpbxozIVPBTmM/7qKOJP4SFySrQ= +github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= +github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= -github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg= -github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -336,7 +342,6 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= @@ -345,31 +350,27 @@ github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= -github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= @@ -399,8 +400,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= -github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -447,14 +448,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -471,7 +470,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -480,7 +478,6 @@ 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= @@ -491,14 +488,14 @@ gitlab.com/nolash/go-mockbytes v0.0.7 h1:9XVFpEfY67kGBVJve3uV19kzqORdlo7V+q09OE6 gitlab.com/nolash/go-mockbytes v0.0.7/go.mod h1:KKOpNTT39j2Eo+P6uUTOncntfeKY6AFh/2CxuD5MpgE= 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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +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/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.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +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.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= @@ -524,37 +521,21 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U 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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 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/sync v0.0.0-20190423024810-112230192c58/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/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= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -566,48 +547,28 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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= 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= diff --git a/pkg/bee/api/api.go b/pkg/bee/api/api.go index f943fc33..48158fdf 100644 --- a/pkg/bee/api/api.go +++ b/pkg/bee/api/api.go @@ -17,6 +17,7 @@ const ( apiVersion = "v1" contentType = "application/json, text/plain, */*; charset=utf-8" postageStampBatchHeader = "Swarm-Postage-Batch-Id" + postageStampHeader = "Swarm-Postage-Stamp" deferredUploadHeader = "Swarm-Deferred-Upload" swarmAct = "Swarm-Act" swarmActHistoryAddress = "Swarm-Act-History-Address" @@ -46,6 +47,7 @@ type Client struct { Bytes *BytesService Chunks *ChunksService Dirs *DirsService + Envelope *EnvelopeService Feed *FeedService Files *FilesService Node *NodeService @@ -96,6 +98,7 @@ func newClient(apiURL *url.URL, httpClient *http.Client) (c *Client) { c.Bytes = (*BytesService)(&c.service) c.Chunks = (*ChunksService)(&c.service) c.Dirs = (*DirsService)(&c.service) + c.Envelope = (*EnvelopeService)(&c.service) c.Feed = (*FeedService)(&c.service) c.Files = (*FilesService)(&c.service) c.Node = (*NodeService)(&c.service) diff --git a/pkg/bee/api/chunks.go b/pkg/bee/api/chunks.go index e384b6e3..7d257feb 100644 --- a/pkg/bee/api/chunks.go +++ b/pkg/bee/api/chunks.go @@ -36,7 +36,11 @@ func (c *ChunksService) Upload(ctx context.Context, data []byte, o UploadOptions if o.Direct { h.Add(deferredUploadHeader, "false") } - h.Add(postageStampBatchHeader, o.BatchID) + if o.Stamp != "" { + h.Add(postageStampHeader, o.Stamp) + } else { + h.Add(postageStampBatchHeader, o.BatchID) + } err := c.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/chunks", h, bytes.NewReader(data), &resp) return resp, err } diff --git a/pkg/bee/api/envelope.go b/pkg/bee/api/envelope.go new file mode 100644 index 00000000..0021f936 --- /dev/null +++ b/pkg/bee/api/envelope.go @@ -0,0 +1,53 @@ +package api + +import ( + "context" + "encoding/hex" + "fmt" + "net/http" + + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +// EnvelopeService represents Bee's postage envelope service. +type EnvelopeService service + +// EnvelopeResponse is the JSON body returned by POST /envelope/{address}. +type EnvelopeResponse struct { + Issuer string `json:"issuer"` + Index string `json:"index"` + Timestamp string `json:"timestamp"` + Signature string `json:"signature"` +} + +// Create requests a postage stamp for the given chunk address from the node's +// stamper for batchID. The stamp is returned as a postage.Stamp ready for upload. +func (e *EnvelopeService) Create(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + h := http.Header{} + h.Add(postageStampBatchHeader, batchID) + + var resp EnvelopeResponse + err := e.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/envelope/"+addr.String(), h, nil, &resp) + if err != nil { + return nil, err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return nil, fmt.Errorf("decode batch id: %w", err) + } + index, err := hex.DecodeString(resp.Index) + if err != nil { + return nil, fmt.Errorf("decode stamp index: %w", err) + } + ts, err := hex.DecodeString(resp.Timestamp) + if err != nil { + return nil, fmt.Errorf("decode stamp timestamp: %w", err) + } + sig, err := hex.DecodeString(resp.Signature) + if err != nil { + return nil, fmt.Errorf("decode stamp signature: %w", err) + } + return postage.NewStamp(batchBytes, index, ts, sig), nil +} diff --git a/pkg/bee/api/node.go b/pkg/bee/api/node.go index 789eeccc..ff03b5e9 100644 --- a/pkg/bee/api/node.go +++ b/pkg/bee/api/node.go @@ -80,19 +80,21 @@ func (n *NodeService) Balances(ctx context.Context) (resp Balances, err error) { // HasChunk returns true/false if node has a chunk func (n *NodeService) HasChunk(ctx context.Context, a swarm.Address) (bool, error) { - resp := struct { - Message string `json:"message,omitempty"` - Code int `json:"code,omitempty"` - }{} - - err := n.client.requestJSON(ctx, http.MethodGet, "/chunks/"+a.String(), nil, &resp) - if IsHTTPStatusErrorCode(err, http.StatusNotFound) { - return false, nil - } else if err != nil { - return false, err + for attempt := 0; attempt < 5; attempt++ { + err := n.client.requestJSON(ctx, http.MethodHead, "/chunks/"+a.String(), nil, nil) + if IsHTTPStatusErrorCode(err, http.StatusNotFound) { + return false, nil + } + if IsHTTPStatusErrorCode(err, http.StatusServiceUnavailable) { + time.Sleep(1 * time.Second) + continue + } + if err != nil { + return false, err + } + return true, nil } - - return true, nil + return false, fmt.Errorf("has chunk %s: exceeded retries on 503", a.String()) } // Health represents node's health diff --git a/pkg/bee/api/options.go b/pkg/bee/api/options.go index a88e7bcd..a1a81f5d 100644 --- a/pkg/bee/api/options.go +++ b/pkg/bee/api/options.go @@ -10,6 +10,7 @@ type UploadOptions struct { Pin bool Tag uint64 BatchID string + Stamp string // hex-encoded postage stamp; when set, BatchID is ignored Direct bool ActHistoryAddress swarm.Address RLevel *redundancy.Level diff --git a/pkg/bee/api/soc.go b/pkg/bee/api/soc.go index 1fc84bd9..c8f64f81 100644 --- a/pkg/bee/api/soc.go +++ b/pkg/bee/api/soc.go @@ -9,17 +9,32 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -// PSSService represents Bee's PSS service +// SOCService represents Bee's SOC service. type SOCService service +// SocResponse is the JSON body returned by POST /soc. type SocResponse struct { - Reference swarm.Address + Reference swarm.Address `json:"reference"` } -// Sends a PSS message to a recipienct with a specific topic +// UploadSOC uploads a single owner chunk using a postage batch ID. func (p *SOCService) UploadSOC(ctx context.Context, owner, ID, signature string, data io.Reader, batchID string) (*SocResponse, error) { h := http.Header{} h.Add(postageStampBatchHeader, batchID) + h.Add(deferredUploadHeader, "false") + url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) + + resp := SocResponse{} + return &resp, p.client.requestWithHeader(ctx, http.MethodPost, url, h, data, &resp) +} + +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp +// (hex-encoded Swarm-Postage-Stamp header). Direct upload is forced so the chunk +// enters the reserve immediately for pullsync. +func (p *SOCService) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data io.Reader, stampHex string) (*SocResponse, error) { + h := http.Header{} + h.Add(postageStampHeader, stampHex) + h.Add(deferredUploadHeader, "false") url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) resp := SocResponse{} diff --git a/pkg/bee/client.go b/pkg/bee/client.go index 711ffeb6..2a2a08bd 100644 --- a/pkg/bee/client.go +++ b/pkg/bee/client.go @@ -3,6 +3,7 @@ package bee import ( "bytes" "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -12,11 +13,13 @@ import ( "net/http" "net/url" "slices" + "strings" "sync" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee/api" "github.com/ethersphere/beekeeper/pkg/logging" @@ -213,13 +216,19 @@ func (c *Client) DownloadBytes(ctx context.Context, a swarm.Address, opts *api.D // DownloadChunk downloads chunk from the node func (c *Client) DownloadChunk(ctx context.Context, a swarm.Address, targets string, opts *api.DownloadOptions) (data []byte, err error) { - r, err := c.api.Chunks.Download(ctx, a, targets, opts) - if err != nil { - return nil, fmt.Errorf("download chunk %s: %w", a, err) + for attempt := 0; attempt < 5; attempt++ { + r, err := c.api.Chunks.Download(ctx, a, targets, opts) + if err != nil { + if api.IsHTTPStatusErrorCode(err, 503) || strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { + time.Sleep(1 * time.Second) + continue + } + return nil, fmt.Errorf("download chunk %s: %w", a, err) + } + defer r.Close() + return io.ReadAll(r) } - defer r.Close() - - return io.ReadAll(r) + return nil, fmt.Errorf("download chunk %s: exceeded retries on 503", a) } // DownloadFileBytes downloads a flie from the node and returns the data. @@ -640,7 +649,7 @@ func (c *Client) SendPSSMessage(ctx context.Context, nodeAddress swarm.Address, return c.api.PSS.SendMessage(ctx, nodeAddress, publicKey, topic, prefix, bytes.NewReader(data), batchID) } -// UploadSOC uploads a single owner chunk to a node with a E +// UploadSOC uploads a single owner chunk to a node with a batch ID. func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, data []byte, batchID string) (swarm.Address, error) { resp, err := c.api.SOC.UploadSOC(ctx, owner, ID, signature, bytes.NewReader(data), batchID) if err != nil { @@ -650,6 +659,21 @@ func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, dat return resp.Reference, nil } +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp. +func (c *Client) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data []byte, stamp []byte) (swarm.Address, error) { + resp, err := c.api.SOC.UploadSOCWithStamp(ctx, owner, ID, signature, bytes.NewReader(data), hex.EncodeToString(stamp)) + if err != nil { + return swarm.ZeroAddress, err + } + + return resp.Reference, nil +} + +// CreateEnvelope requests a postage stamp for addr from the node's stamper. +func (c *Client) CreateEnvelope(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + return c.api.Envelope.Create(ctx, addr, batchID) +} + // Settlements represents Settlements's response type Settlements struct { Settlements []Settlement diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go new file mode 100644 index 00000000..a0a3d92d --- /dev/null +++ b/pkg/check/socmatrix/socmatrix.go @@ -0,0 +1,1688 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socmatrix + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/soc" + "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" +) + +const bucketDepth = postage.BucketDepth + +// Options holds options for the socmatrix check. +type Options struct { + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + RequestTimeout time.Duration + SyncRetryInterval time.Duration + // SyncWait is used by precision (former chunk-convergence) scenarios. + SyncWait time.Duration + // Password decrypts the batch-issuer Swarm key for precision stamp resigning. + Password string +} + +// NewDefaultOptions returns default options for the check. +func NewDefaultOptions() Options { + return Options{ + PostageTTL: 24 * time.Hour, + PostageDepth: 16, + PostageLabel: "socmatrix-label", + RequestTimeout: 10 * time.Minute, + SyncRetryInterval: 2 * time.Second, + SyncWait: 45 * time.Second, + Password: "beekeeper", + } +} + +// Check instance. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + logger: logger, + } +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + rnd := random.PseudoGenerator(time.Now().UnixNano()) + fullNodeClients, err := cluster.ShuffledFullNodeClients(ctx, rnd) + if err != nil { + return fmt.Errorf("shuffled full node clients: %w", err) + } + + if len(fullNodeClients) < 2 { + return fmt.Errorf("socmatrix test requires at least 2 full nodes") + } + + node0 := fullNodeClients[0] + node1 := fullNodeClients[1] + + c.logger.Infof("socmatrix: running API-stamp matrix (scenarios 1-10) against %d full nodes (node0: '%s', node1: '%s')", + len(fullNodeClients), node0.Name(), node1.Name()) + + // Shared signer for owner 1 + signer1, ownerHex1, err := newTestSigner() + if err != nil { + return err + } + // Signer for owner 2 (multi-owner isolation scenario) + signer2, ownerHex2, err := newTestSigner() + if err != nil { + return err + } + + // Create postage batches on node0 and node1 for standard API upload scenarios (2, 5, 6, 7, 8, 10) + node0BatchID, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + node1BatchID, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + + // Try setting up batch issuer and signer for precomputed equal-timestamp stamp tests (1, 3, 4, 9) + issuer, batchSigner, issuerBatchID0, _ := c.setupIssuerBatch(ctx, cluster, o) + + var issuerBatchID1 string + if issuer != nil { + issuerBatchID1, err = issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-issuer-alt") + if err != nil { + return fmt.Errorf("issuer %s: alt batch id: %w", issuer.Name(), err) + } + } else { + issuerBatchID0 = node0BatchID + issuerBatchID1 = node1BatchID + } + + // API-stamp matrix (1-10): continue-on-error so later scenarios still run. + var matrixErrs error + runMatrix := func(name string, fn func() error) { + c.logger.Infof("--- %s ---", name) + if err := fn(); err != nil { + c.logger.Errorf("%s failed: %v", name, err) + matrixErrs = errors.Join(matrixErrs, fmt.Errorf("%s: %w", name, err)) + return + } + c.logger.Infof("%s passed", name) + } + + runMatrix("Scenario 1: Divergent SOC Tie-Break (Same Stamp)", func() error { + return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuer, batchSigner, o) + }) + runMatrix("Scenario 2: Timestamp Progression (Increasing Timestamp)", func() error { + return c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) + }) + runMatrix("Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence", func() error { + return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuerBatchID1, issuer, batchSigner, o) + }) + runMatrix("Scenario 4: Multi-Stamp Re-Offer Precedence", func() error { + return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuerBatchID1, issuer, batchSigner, o) + }) + runMatrix("Scenario 5: Multi-Batch Sibling Sum Refresh", func() error { + return c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, node1BatchID, o) + }) + runMatrix("Scenario 6: Identical Payload Checksum Matching", func() error { + return c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) + }) + runMatrix("Scenario 7: CAC vs CAC Stamp Index Collision", func() error { + return c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, node0BatchID, o) + }) + runMatrix("Scenario 8: CAC vs SOC Stamp Index Tie-Break", func() error { + return c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) + }) + runMatrix("Scenario 9: 3-Payload Sequenced Divergent SOC Chain", func() error { + return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuer, batchSigner, o) + }) + runMatrix("Scenario 10: Multi-Owner Independent SOC Isolation", func() error { + return c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, node0BatchID, o) + }) + + if matrixErrs != nil { + c.logger.Errorf("socmatrix: scenarios 1-10 finished with errors across %d full nodes: %v", len(fullNodeClients), matrixErrs) + } else { + c.logger.Infof("socmatrix: scenarios 1-10 completed successfully across %d full nodes", len(fullNodeClients)) + } + + n1, n2, err := pickClosestPair(ctx, fullNodeClients) + if err != nil { + return err + } + c.logger.Infof("precision neighborhood upload pair: %s and %s", n1.Name(), n2.Name()) + neighborhood := []*bee.Client{n1, n2} + + issuer, batchSigner, batchID, err := c.setupIssuerBatch(ctx, cluster, o) + if err != nil { + return err + } + c.logger.Infof("precision batch issuer=%s batch_id=%s", issuer.Name(), batchID) + + precision := []struct { + name string + run func() error + }{ + {"11 precision divergent soc identical stamp", func() error { + return c.scenarioDivergentSOC(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"12 precision soc same index higher timestamp", func() error { + return c.scenarioSOCSameIndexTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"13 precision soc different index higher timestamp", func() error { + return c.scenarioSOCDifferentIndexTimestamp(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"14 precision cac same index higher timestamp", func() error { + return c.scenarioCACSameIndexTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"15 precision cac same index equal timestamp", func() error { + return c.scenarioCACSameIndexEqualTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"16 precision soc stale stamp redelivery race", func() error { + return c.scenarioSOCStaleStampRedeliveryRace(ctx, issuer, batchSigner, neighborhood, batchID, o, o.SyncWait) + }}, + } + + var errs error + for _, s := range precision { + c.logger.Infof("--- Scenario %s ---", s.name) + if err := s.run(); err != nil { + c.logger.Errorf("%s failed: %v", s.name, err) + errs = errors.Join(errs, fmt.Errorf("%s: %w", s.name, err)) + continue + } + c.logger.Infof("%s passed", s.name) + } + errs = errors.Join(matrixErrs, errs) + if errs != nil { + return errs + } + + c.logger.Infof("socmatrix: all 16 scenarios completed successfully") + return nil +} + +func (c *Check) testDivergentSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + issuer *bee.Client, + batchSigner crypto.Signer, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s1-payload-alpha")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s1-payload-beta")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + if issuer != nil && batchSigner != nil { + env, err := issuer.CreateEnvelope(ctx, soc1.Address(), batchID) + if err == nil { + stampBytes, err := env.MarshalBinary() + if err == nil { + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stampBytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stampBytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + + // Fallback to API stamp upload if issuer/batchSigner not configured + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc1: %w", err) + } + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc2: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testTimestampProgression( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s2-initial-payload")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s2-updated-payload")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload initial soc1: %w", err) + } + + time.Sleep(1 * time.Second) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload updated soc2: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch2.Data(), o) +} + +func (c *Check) testCrossBatchStampHashPrecedence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + issuer *bee.Client, + batchSigner crypto.Signer, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s3-payload-one")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s3-payload-two")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + if issuer != nil && batchSigner != nil { + batch0Bytes, err0 := hex.DecodeString(batchID0) + batch1Bytes, err1 := hex.DecodeString(batchID1) + env0, err2 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID0) + env1, err3 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID1) + if err0 == nil && err1 == nil && err2 == nil && err3 == nil { + ts := binary.BigEndian.Uint64(env0.Timestamp()) + stamp0, errS0 := signStamp(batchSigner, batch0Bytes, env0.Index(), uint64ToBytes(ts), soc1.Address()) + stamp1, errS1 := signStamp(batchSigner, batch1Bytes, env1.Index(), uint64ToBytes(ts), soc1.Address()) + if errS0 == nil && errS1 == nil { + hash0, errH0 := stamp0.Hash() + hash1, errH1 := stamp1.Hash() + if errH0 == nil && errH1 == nil { + var expectedPayload []byte + if bytes.Compare(hash1, hash0) < 0 { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + stamp0Bytes, _ := stamp0.MarshalBinary() + stamp1Bytes, _ := stamp1.MarshalBinary() + + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp0: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stamp1Bytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + } + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiStampReofferPrecedence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + issuer *bee.Client, + batchSigner crypto.Signer, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s4-payload-alpha")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s4-payload-beta")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + if issuer != nil && batchSigner != nil { + batch0Bytes, err0 := hex.DecodeString(batchID0) + batch1Bytes, err1 := hex.DecodeString(batchID1) + env0, err2 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID0) + env1, err3 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID1) + if err0 == nil && err1 == nil && err2 == nil && err3 == nil { + ts := binary.BigEndian.Uint64(env0.Timestamp()) + stamp0, errS0 := signStamp(batchSigner, batch0Bytes, env0.Index(), uint64ToBytes(ts), soc1.Address()) + stamp1, errS1 := signStamp(batchSigner, batch1Bytes, env1.Index(), uint64ToBytes(ts), soc1.Address()) + if errS0 == nil && errS1 == nil { + hash0, errH0 := stamp0.Hash() + hash1, errH1 := stamp1.Hash() + if errH0 == nil && errH1 == nil { + var expectedPayload []byte + if bytes.Compare(hash1, hash0) < 0 { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + stamp0Bytes, _ := stamp0.MarshalBinary() + stamp1Bytes, _ := stamp1.MarshalBinary() + + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp0: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stamp1Bytes) + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o); err != nil { + return err + } + + // Re-offer soc1 under batchID0 with stamp0Bytes (same stamp timestamp) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + } + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o); err != nil { + return err + } + + // Re-offer soc1 under batchID0 + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiBatchSiblingSumRefresh( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s5-initial-multibatch")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s5-updated-multibatch")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID1) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch1.Data(), o); err != nil { + return err + } + + time.Sleep(1 * time.Second) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID0) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch2.Data(), o) +} + +func (c *Check) testIdenticalChecksumMatching( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch, err := cac.New([]byte("matrix-s6-identical-payload")) + if err != nil { + return err + } + sch, err := soc.New(idBytes, ch).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(sch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return err + } + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, sch.Address(), ch.Data(), o) +} + +func (c *Check) testCACIndexCollision( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + batchID string, + o Options, +) error { + ch1, err := cac.New([]byte("matrix-s7-cac-payload-one")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s7-cac-payload-two")) + if err != nil { + return err + } + + _, err = node0.UploadChunk(ctx, ch1.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return fmt.Errorf("upload cac1: %w", err) + } + _, err = node0.UploadChunk(ctx, ch2.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return fmt.Errorf("upload cac2: %w", err) + } + + if err := c.assertClusterConvergence(ctx, fullNodes, ch1.Address(), ch1.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, ch2.Address(), ch2.Data(), o) +} + +func (c *Check) testCACvsSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + chCAC, err := cac.New([]byte("matrix-s8-cac-content")) + if err != nil { + return err + } + socChunk, err := soc.New(idBytes, chCAC).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(socChunk.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadChunk(ctx, chCAC.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return err + } + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, chCAC.Data(), batchID) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, chCAC.Address(), chCAC.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, socChunk.Address(), chCAC.Data(), o) +} + +func (c *Check) testSequencedDivergentChain( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + issuer *bee.Client, + batchSigner crypto.Signer, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + p1 := []byte("matrix-s9-chain-payload-one") + p2 := []byte("matrix-s9-chain-payload-two") + p3 := []byte("matrix-s9-chain-payload-three") + + ch1, _ := cac.New(p1) + ch2, _ := cac.New(p2) + ch3, _ := cac.New(p3) + + soc1, _ := soc.New(idBytes, ch1).Sign(signer) + soc2, _ := soc.New(idBytes, ch2).Sign(signer) + soc3, _ := soc.New(idBytes, ch3).Sign(signer) + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig3Hex := hex.EncodeToString(soc3.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + // Determine winner among all three + win12, _ := divergentChunkWins(soc1, soc2) + var winner12 swarm.Chunk + if win12 { + winner12 = soc2 + } else { + winner12 = soc1 + } + winFinal, _ := divergentChunkWins(winner12, soc3) + var winningSOC swarm.Chunk + if winFinal { + winningSOC = soc3 + } else { + winningSOC = winner12 + } + + var expectedPayload []byte + if winningSOC.Address().Equal(soc1.Address()) && bytes.Equal(winningSOC.Data(), soc1.Data()) { + expectedPayload = ch1.Data() + } else if winningSOC.Address().Equal(soc2.Address()) && bytes.Equal(winningSOC.Data(), soc2.Data()) { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch3.Data() + } + + if issuer != nil { + env, err := issuer.CreateEnvelope(ctx, soc1.Address(), batchID) + if err == nil { + stampBytes, err := env.MarshalBinary() + if err == nil { + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stampBytes) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stampBytes) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), stampBytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), batchID) + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiOwnerSOCIsolation( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex1, ownerHex2 string, + signer1, signer2 crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, _ := cac.New([]byte("matrix-s10-owner1-payload")) + ch2, _ := cac.New([]byte("matrix-s10-owner2-payload")) + + soc1, _ := soc.New(idBytes, ch1).Sign(signer1) + soc2, _ := soc.New(idBytes, ch2).Sign(signer2) + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex1, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return err + } + _, err = node0.UploadSOC(ctx, ownerHex2, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch1.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, soc2.Address(), ch2.Data(), o) +} + +func (c *Check) assertClusterConvergence( + ctx context.Context, + nodes []*bee.Client, + address swarm.Address, + expectedPayload []byte, + o Options, +) error { + ticker := time.NewTicker(o.SyncRetryInterval) + defer ticker.Stop() + + // Bound wait by sync budget, not the parent check timeout (often 15–45m). + waitFor := 5 * o.SyncWait + if waitFor < 2*time.Minute { + waitFor = 2 * time.Minute + } + deadline := time.Now().Add(waitFor) + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) + case <-ticker.C: + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for cluster to converge on %s", address.String()) + } + allConverged := true + for _, node := range nodes { + retrieved, err := node.DownloadChunk(ctx, address, "", nil) + if err != nil { + allConverged = false + c.logger.Debugf("node %s: download %s error: %v", node.Name(), address.String(), err) + break + } + if !bytes.Contains(retrieved, expectedPayload) && !bytes.Equal(retrieved, expectedPayload) { + allConverged = false + c.logger.Debugf("node %s: data mismatch on %s", node.Name(), address.String()) + break + } + } + + if allConverged { + c.logger.Infof("all %d full nodes converged on address %s with expected payload!", len(nodes), address.String()) + return nil + } + } + } +} + +func newTestSigner() (crypto.Signer, string, error) { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", err + } + signer := crypto.NewDefaultSigner(privKey) + pubKey, err := signer.PublicKey() + if err != nil { + return nil, "", err + } + ownerBytes, err := crypto.NewEthereumAddress(*pubKey) + if err != nil { + return nil, "", err + } + return signer, hex.EncodeToString(ownerBytes), nil +} + +func randomID() ([]byte, error) { + key := make([]byte, 32) + _, err := crand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { + s1, err := soc.FromChunk(ch1) + if err != nil { + return false, fmt.Errorf("soc 1 from chunk: %w", err) + } + s2, err := soc.FromChunk(ch2) + if err != nil { + return false, fmt.Errorf("soc 2 from chunk: %w", err) + } + return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil +} + +// --------------------------------------------------------------------------- +// Precision scenarios (former chunk-convergence) and helpers +// --------------------------------------------------------------------------- + +func (c *Check) setupIssuerBatch(ctx context.Context, cluster orchestration.Cluster, o Options) (*bee.Client, crypto.Signer, string, error) { + nodes := cluster.Nodes() + var issuerNode orchestration.Node + var issuerName string + for name, n := range nodes { + if n.SwarmKey() != nil { + issuerNode = n + issuerName = name + break + } + } + if issuerNode == nil { + return nil, nil, "", fmt.Errorf("no node with swarm-key found; bootnode with configured swarm-key is required") + } + + privKey, err := issuerNode.SwarmKey().Decrypt(o.Password) + if err != nil { + return nil, nil, "", fmt.Errorf("decrypt swarm key for %s: %w", issuerName, err) + } + signer := crypto.NewDefaultSigner(privKey) + + clients, err := cluster.NodesClients(ctx) + if err != nil { + return nil, nil, "", fmt.Errorf("nodes clients: %w", err) + } + issuer, ok := clients[issuerName] + if !ok { + return nil, nil, "", fmt.Errorf("client for issuer node %s not found", issuerName) + } + + label := fmt.Sprintf("%s-%s", o.PostageLabel, hex.EncodeToString(randomBytes(4))) + batchID, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, label) + if err != nil { + return nil, nil, "", fmt.Errorf("create batch on %s: %w", issuerName, err) + } + return issuer, signer, batchID, nil +} + +func (c *Check) scenarioDivergentSOC(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + low, high, err := orderedSOCs(signer, id) + if err != nil { + return err + } + stamp, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope: %w", err) + } + stampBytes, err := stamp.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof("soc=%s wrapped_low=%s wrapped_high=%s", addr, mustWrapped(low), mustWrapped(high)) + // Identical stamp: losing upload may fail the wrapped-CAC tie-break. + // Require only the first (winning) upload to succeed. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, low, high, stampBytes, stampBytes, false); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, low.Data()) +} + +func (c *Check) scenarioSOCSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-same-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-same-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // CreateEnvelope yields a postage-valid index; reuse it with two timestamps. + env, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + stampOld, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), addr) + if err != nil { + return fmt.Errorf("sign older stamp: %w", err) + } + stampNew, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), addr) + if err != nil { + return fmt.Errorf("sign newer stamp: %w", err) + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s index=%s", addr, hex.EncodeToString(index)) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioSOCDifferentIndexTimestamp(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-diff-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-diff-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + stampOld, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope older: %w", err) + } + stampNew, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer: %w", err) + } + for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { + stampNew, err = issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer retry: %w", err) + } + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s idx_old=%s idx_new=%s", addr, hex.EncodeToString(stampOld.Index()), hex.EncodeToString(stampNew.Index())) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Valid index for this postage bucket (both CACs share the bucket). + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + // higher timestamp on high address chunk so timestamp rule (not address tie-break) decides + stampLow, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s", low.Address(), high.Address(), hex.EncodeToString(index)) + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], low, high); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing higher-timestamp CAC %s", n.Name(), high.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has lower-timestamp CAC %s", n.Name(), low.Address()) + } + } + return nil +} + +func (c *Check) scenarioCACSameIndexEqualTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Same batch, stamp index and timestamp; signatures differ (each stamp is + // bound to its CAC address). At equal timestamp the lower address wins. + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := uint64ToBytes(binary.BigEndian.Uint64(env.Timestamp())) + + stampLow, err := signStamp(batchSigner, batchBytes, index, ts, low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, ts, high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s equal_ts address tie-break", low.Address(), high.Address(), hex.EncodeToString(index)) + // Parallel uploads: loser (higher address) and winner (lower address). + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], high, low); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing lower-address CAC %s", n.Name(), low.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has higher-address CAC %s", n.Name(), high.Address()) + } + } + return nil +} + +// scenarioSOCStaleStampRedeliveryRace races three divergent SOCs at one address: +// +// SOC₁ under stamp S (batch A) → uploaded to Sam +// SOC₂ under stamp T (batch B) → uploaded to Sara +// SOC₃ reuses stamp S with payload P₃ whose wrapped CAC beats P₁ and P₂ → +// uploaded to both nodes in parallel with the SOC₁/SOC₂ uploads +// +// After sync, every neighborhood node must hold the same payload among P₁/P₂/P₃ +// (which one wins does not matter; agreement does). +func (c *Check) scenarioSOCStaleStampRedeliveryRace(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { + sam, sara := neighborhood[0], neighborhood[1] + + batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-stale-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch B: %w", err) + } + + signer, owner, id, addr, err := mineSOC(ctx, sam, sara) + if err != nil { + return err + } + soc1, soc2, soc3, err := orderedStaleRaceSOCs(signer, id) + if err != nil { + return err + } + + batchABytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + batchBBytes, err := hex.DecodeString(batchB) + if err != nil { + return err + } + + envS, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope stamp S: %w", err) + } + envT, err := issuer.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope stamp T: %w", err) + } + tsS := binary.BigEndian.Uint64(envS.Timestamp()) + tsT := binary.BigEndian.Uint64(envT.Timestamp()) + if tsS <= tsT { + tsS = tsT + 1 + } + stampS, err := signStamp(batchSigner, batchABytes, append([]byte{}, envS.Index()...), uint64ToBytes(tsS), addr) + if err != nil { + return fmt.Errorf("sign stamp S: %w", err) + } + stampT, err := signStamp(batchSigner, batchBBytes, append([]byte{}, envT.Index()...), uint64ToBytes(tsT), addr) + if err != nil { + return fmt.Errorf("sign stamp T: %w", err) + } + stampSBytes, err := stampS.MarshalBinary() + if err != nil { + return err + } + stampTBytes, err := stampT.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof( + "soc=%s stale-stamp race sam=%s sara=%s wrapped_p1=%s wrapped_p2=%s wrapped_p3=%s batchA=%s… batchB=%s… tsS=%d tsT=%d", + addr, sam.Name(), sara.Name(), mustWrapped(soc1), mustWrapped(soc2), mustWrapped(soc3), + batchID[:8], batchB[:8], tsS, tsT, + ) + + type uploadResult struct { + label string + err error + } + results := make(chan uploadResult, 4) + + go func() { + results <- uploadResult{"SOC1→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc1, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC2→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc2, stampTBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc3, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc3, stampSBytes)} + }() + + for range 4 { + r := <-results + if r.err != nil { + c.logger.Infof("upload %s returned: %v", r.label, r.err) + } else { + c.logger.Infof("upload %s ok", r.label) + } + } + c.logger.Infof("race uploads finished; waiting %s for neighborhood agreement on any of P1/P2/P3", syncWait) + + time.Sleep(syncWait) + + candidates := []struct { + label string + data []byte + }{ + {"P1", soc1.Data()}, + {"P2", soc2.Data()}, + {"P3", soc3.Data()}, + } + + agreed, err := assertNeighborhoodAgreesAmong(ctx, c.logger, neighborhood, addr, candidates) + if err != nil { + return fmt.Errorf("after stale-stamp race (want same payload on both): %w", err) + } + c.logger.Infof("neighborhood converged on %s", agreed) + return nil +} + +// orderedStaleRaceSOCs builds three SOCs at the same address where P₃'s wrapped +// CAC is lexicographically lower than both P₁ and P₂, so resolveDivergence would +// accept P₃ over either stored payload when the stale-stamp guard is absent. +func orderedStaleRaceSOCs(signer crypto.Signer, id []byte) (p1, p2, p3 swarm.Chunk, err error) { + for range 10_000 { + p1, err = buildSOC(signer, id, append([]byte("stale-p1-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p2, err = buildSOC(signer, id, append([]byte("stale-p2-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p3, err = buildSOC(signer, id, append([]byte("stale-p3-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + w1, w2, w3 := mustWrapped(p1).Bytes(), mustWrapped(p2).Bytes(), mustWrapped(p3).Bytes() + if bytes.Compare(w3, w1) < 0 && bytes.Compare(w3, w2) < 0 && + !bytes.Equal(w1, w2) && !bytes.Equal(w1, w3) && !bytes.Equal(w2, w3) { + return p1, p2, p3, nil + } + } + return nil, nil, nil, fmt.Errorf("could not mine P1/P2/P3 with wrapped(P3) lower than both") +} + +func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch swarm.Chunk, stamp []byte) error { + sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + + for attempt := 0; attempt < 5; attempt++ { + if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { + if strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { + time.Sleep(1 * time.Second) + continue + } + return fmt.Errorf("upload to %s: %w", n.Name(), err) + } + return nil + } + return fmt.Errorf("upload to %s: exceeded 5 retries on 503", n.Name()) +} + +// uploadSOCPair uploads ch1 to n1 and ch2 to n2 in parallel. +// If requireSecond is false, a failed second upload is ignored (tie-break loser). +func uploadSOCPair(ctx context.Context, n1, n2 *bee.Client, owner string, id []byte, ch1, ch2 swarm.Chunk, stamp1, stamp2 []byte, requireSecond bool) error { + type result struct { + which int // 1 or 2 + err error + } + results := make(chan result, 2) + + go func() { + results <- result{1, uploadSOC(ctx, n1, owner, id, ch1, stamp1)} + }() + go func() { + results <- result{2, uploadSOC(ctx, n2, owner, id, ch2, stamp2)} + }() + + var err1, err2 error + for range 2 { + r := <-results + switch r.which { + case 1: + err1 = r.err + case 2: + err2 = r.err + } + } + if err1 != nil { + return err1 + } + if err2 != nil && requireSecond { + return err2 + } + return nil +} + +// uploadChunkPair uploads ch1 to n1 and ch2 to n2 in parallel. +func uploadChunkPair(ctx context.Context, n1, n2 *bee.Client, ch1, ch2 swarm.Chunk) error { + st1, err := ch1.Stamp().MarshalBinary() + if err != nil { + return err + } + st2, err := ch2.Stamp().MarshalBinary() + if err != nil { + return err + } + + type result struct { + name string + err error + } + results := make(chan result, 2) + + go func() { + _, err := n1.UploadChunk(ctx, ch1.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st1), Direct: true}) + if err != nil { + err = fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + results <- result{n1.Name(), err} + }() + go func() { + _, err := n2.UploadChunk(ctx, ch2.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st2), Direct: true}) + if err != nil { + err = fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + results <- result{n2.Name(), err} + }() + + var errs error + for range 2 { + r := <-results + if r.err != nil { + errs = errors.Join(errs, r.err) + } + } + return errs +} + +func assertNeighborhoodPayload(ctx context.Context, logger logging.Logger, nodes []*bee.Client, addr swarm.Address, want []byte) error { + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + if !bytes.Equal(got, want) { + logger.Errorf("node %s payload mismatch want_len=%d got_len=%d", n.Name(), len(want), len(got)) + return fmt.Errorf("node %s: payload mismatch", n.Name()) + } + } + return nil +} + +// assertNeighborhoodAgreesAmong checks that every node holds the same body and +// that body matches one of the labeled candidates. Returns the winning label. +func assertNeighborhoodAgreesAmong( + ctx context.Context, + logger logging.Logger, + nodes []*bee.Client, + addr swarm.Address, + candidates []struct { + label string + data []byte + }, +) (string, error) { + if len(nodes) == 0 { + return "", fmt.Errorf("no nodes") + } + if len(candidates) == 0 { + return "", fmt.Errorf("no candidates") + } + + matchLabel := func(got []byte) string { + for _, c := range candidates { + if bytes.Equal(got, c.data) { + return c.label + } + } + return "" + } + + var agreed []byte + var agreedLabel string + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return "", fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + label := matchLabel(got) + if label == "" { + logger.Errorf("node %s holds unrecognized payload len=%d", n.Name(), len(got)) + return "", fmt.Errorf("node %s: payload not among candidates", n.Name()) + } + logger.Infof("%s holds %s", n.Name(), label) + if agreed == nil { + agreed = got + agreedLabel = label + continue + } + if !bytes.Equal(got, agreed) { + logger.Errorf("node %s holds %s, others hold %s", n.Name(), label, agreedLabel) + return "", fmt.Errorf("node %s: desync (%s vs %s)", n.Name(), label, agreedLabel) + } + } + return agreedLabel, nil +} + +func pickClosestPair(ctx context.Context, nodes orchestration.ClientList) (*bee.Client, *bee.Client, error) { + overlays := make([]swarm.Address, len(nodes)) + for i, n := range nodes { + a, err := n.Addresses(ctx) + if err != nil { + return nil, nil, fmt.Errorf("addresses for %s: %w", n.Name(), err) + } + overlays[i] = a.Overlay + } + bestI, bestJ, bestPO := 0, 1, -1 + for i := 0; i < len(nodes); i++ { + for j := i + 1; j < len(nodes); j++ { + p := int(swarm.Proximity(overlays[i].Bytes(), overlays[j].Bytes())) + if p > bestPO { + bestPO, bestI, bestJ = p, i, j + } + } + } + if bestPO < 0 { + return nil, nil, fmt.Errorf("could not find a node pair") + } + return nodes[bestI], nodes[bestJ], nil +} + +func mineSOC(ctx context.Context, n1, n2 *bee.Client) (crypto.Signer, string, []byte, swarm.Address, error) { + a1, err := n1.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + a2, err := n2.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + t1, t2 := a1.Overlay, a2.Overlay + minPO := swarm.Proximity(t1.Bytes(), t2.Bytes()) + if minPO > 8 { + minPO = 8 + } + + for { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + signer := crypto.NewDefaultSigner(privKey) + ownerBytes, err := signer.EthereumAddress() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + owner := hex.EncodeToString(ownerBytes.Bytes()) + for range 8192 { + id := make([]byte, swarm.HashSize) + if _, err := crand.Read(id); err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + tmp, err := cac.New([]byte("x")) + if err != nil { + continue + } + ch, err := soc.New(id, tmp).Sign(signer) + if err != nil { + continue + } + a := ch.Address() + if swarm.Proximity(a.Bytes(), t1.Bytes()) >= minPO && + swarm.Proximity(a.Bytes(), t2.Bytes()) >= minPO { + return signer, owner, id, a, nil + } + } + } +} + +func orderedSOCs(signer crypto.Signer, id []byte) (low, high swarm.Chunk, err error) { + a, err := buildSOC(signer, id, []byte("soc-conv-payload-aaa")) + if err != nil { + return nil, nil, err + } + b, err := buildSOC(signer, id, []byte("soc-conv-payload-bbb")) + if err != nil { + return nil, nil, err + } + wa, wb := mustWrapped(a), mustWrapped(b) + if bytes.Compare(wa.Bytes(), wb.Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func buildSOC(signer crypto.Signer, id, payload []byte) (swarm.Chunk, error) { + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + return soc.New(id, ch).Sign(signer) +} + +func orderedCACsInBucket(target swarm.Address) (low, high swarm.Chunk, err error) { + bucket := addressBucket(bucketDepth, target) + a, err := cacInBucket(bucket, append([]byte("cac-conv-aaa-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + b, err := cacInBucket(bucket, append([]byte("cac-conv-bbb-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + for a.Address().Equal(b.Address()) { + b, err = cacInBucket(bucket, randomBytes(32)) + if err != nil { + return nil, nil, err + } + } + if bytes.Compare(a.Address().Bytes(), b.Address().Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func cacInBucket(bucket uint32, seed []byte) (swarm.Chunk, error) { + for range 5_000_000 { + payload := append(append([]byte{}, seed...), randomBytes(16)...) + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + if addressBucket(bucketDepth, ch.Address()) == bucket { + return ch, nil + } + seed = randomBytes(16) + } + return nil, fmt.Errorf("could not mine CAC into bucket %d", bucket) +} + +func addressBucket(depth uint8, addr swarm.Address) uint32 { + return binary.BigEndian.Uint32(addr.Bytes()[:4]) >> (32 - depth) +} + +func signStamp(signer crypto.Signer, batchID, index, ts []byte, addr swarm.Address) (*postage.Stamp, error) { + digest, err := postage.ToSignDigest(addr.Bytes(), batchID, index, ts) + if err != nil { + return nil, err + } + sig, err := signer.Sign(digest) + if err != nil { + return nil, err + } + return postage.NewStamp(batchID, index, ts, sig), nil +} + +func mustWrapped(ch swarm.Chunk) swarm.Address { + s, err := soc.FromChunk(ch) + if err != nil { + return swarm.ZeroAddress + } + return s.WrappedChunk().Address() +} + +func uint64ToBytes(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + _, _ = crand.Read(b) + return b +} diff --git a/pkg/check/socmatrix/socmatrix_test.go b/pkg/check/socmatrix/socmatrix_test.go new file mode 100644 index 00000000..1fe938d8 --- /dev/null +++ b/pkg/check/socmatrix/socmatrix_test.go @@ -0,0 +1,40 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socmatrix_test + +import ( + "io" + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/check/socmatrix" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestNewDefaultOptions(t *testing.T) { + opts := socmatrix.NewDefaultOptions() + if opts.PostageTTL != 24*time.Hour { + t.Fatalf("expected PostageTTL 24h, got %v", opts.PostageTTL) + } + if opts.PostageDepth != 16 { + t.Fatalf("expected PostageDepth 16, got %d", opts.PostageDepth) + } + if opts.RequestTimeout != 10*time.Minute { + t.Fatalf("expected RequestTimeout 10m, got %v", opts.RequestTimeout) + } + if opts.SyncWait != 45*time.Second { + t.Fatalf("expected SyncWait 45s, got %v", opts.SyncWait) + } + if opts.Password != "beekeeper" { + t.Fatalf("expected Password beekeeper, got %q", opts.Password) + } +} + +func TestNewCheck(t *testing.T) { + chk := socmatrix.NewCheck(logging.New(io.Discard, 0)) + if chk == nil { + t.Fatal("expected non-nil check") + } +} diff --git a/pkg/config/check.go b/pkg/config/check.go index 541afb7e..63e5ee3a 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -34,6 +34,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/settlements" "github.com/ethersphere/beekeeper/pkg/check/smoke" "github.com/ethersphere/beekeeper/pkg/check/soc" + "github.com/ethersphere/beekeeper/pkg/check/socmatrix" "github.com/ethersphere/beekeeper/pkg/check/stake" "github.com/ethersphere/beekeeper/pkg/check/withdraw" "github.com/ethersphere/beekeeper/pkg/logging" @@ -60,6 +61,26 @@ type CheckGlobalConfig struct { GethURL string } +func newSOCMatrixOptions(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` + SyncWait *time.Duration `yaml:"sync-wait"` + Password *string `yaml:"password"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := socmatrix.NewDefaultOptions() + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + return opts, nil +} + // Checks represents all available check types var Checks = map[string]CheckType{ "act": { @@ -528,6 +549,10 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "socmatrix": { + NewAction: socmatrix.NewCheck, + NewOptions: newSOCMatrixOptions, + }, "postage": { NewAction: postage.NewCheck, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { diff --git a/pkg/orchestration/key.go b/pkg/orchestration/key.go index 22c12d5a..056ab768 100644 --- a/pkg/orchestration/key.go +++ b/pkg/orchestration/key.go @@ -1,6 +1,7 @@ package orchestration import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" @@ -13,6 +14,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/google/uuid" "golang.org/x/crypto/scrypt" + "golang.org/x/crypto/sha3" "gopkg.in/yaml.v3" ) @@ -69,6 +71,50 @@ func (k *EncryptedKey) StringJSON() (string, error) { return string(data), nil } +// Decrypt decrypts the encrypted Swarm key with the given password and returns +// the secp256k1 private key. The key file format matches Ethereum JSON v3. +func (k *EncryptedKey) Decrypt(password string) (*ecdsa.PrivateKey, error) { + if k == nil { + return nil, fmt.Errorf("nil encrypted key") + } + salt, err := hex.DecodeString(k.Crypto.KDFParams.Salt) + if err != nil { + return nil, fmt.Errorf("decode salt: %w", err) + } + derivedKey, err := scrypt.Key([]byte(password), salt, k.Crypto.KDFParams.N, k.Crypto.KDFParams.R, k.Crypto.KDFParams.P, k.Crypto.KDFParams.DKLen) + if err != nil { + return nil, fmt.Errorf("scrypt: %w", err) + } + cipherText, err := hex.DecodeString(k.Crypto.CipherText) + if err != nil { + return nil, fmt.Errorf("decode ciphertext: %w", err) + } + wantMAC, err := hex.DecodeString(k.Crypto.MAC) + if err != nil { + return nil, fmt.Errorf("decode mac: %w", err) + } + // Bee keystore MAC is SHA3-256; Ethereum V3 keyfiles use Keccak-256. + calculatedMAC := sha3.Sum256(append(derivedKey[16:32], cipherText...)) + if !bytes.Equal(calculatedMAC[:], wantMAC) { + calculatedMACEth, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) + if err != nil { + return nil, fmt.Errorf("mac: %w", err) + } + if !bytes.Equal(calculatedMACEth, wantMAC) { + return nil, fmt.Errorf("invalid password or corrupted key") + } + } + iv, err := hex.DecodeString(k.Crypto.CipherParams.IV) + if err != nil { + return nil, fmt.Errorf("decode iv: %w", err) + } + plain, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, fmt.Errorf("decrypt: %w", err) + } + return crypto.DecodeSecp256k1PrivateKey(plain) +} + type keyCripto struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"`