diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index 939d82b4f97..0cb7c41fe2f 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -54,7 +54,7 @@ jobs: cache: false go-version-file: go.mod - name: Cache Go Modules - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cache/go-build @@ -84,7 +84,7 @@ jobs: beekeeper version --log-verbosity 0 mv ~/.beekeeper.yaml .beekeeper.yaml mv ~/.beekeeper/local.yaml local.yaml - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: temp-artifacts include-hidden-files: true @@ -105,13 +105,13 @@ jobs: needs: [init] steps: - name: Cache - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | /tmp/k3s-${{ env.K3S_VERSION }} key: k3s-${{ env.K3S_VERSION }} - name: "Download Artifact" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: temp-artifacts - name: Unpack artifacts @@ -220,7 +220,7 @@ jobs: echo "Connect to github actions node using" echo "sh <(curl -sSf https://lets.tunshell.com/init.sh) L $(echo $KEYS | jq -r .peer2_key) \${TUNSHELL_SECRET} eu.relay.tunshell.com" curl -sSf https://lets.tunshell.com/init.sh | sh /dev/stdin T $(echo $KEYS | jq -r .peer1_key) ${{ secrets.TUNSHELL_SECRET }} eu.relay.tunshell.com - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: failure() with: name: debug-dump @@ -233,7 +233,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Download Artifact" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: temp-artifacts - name: Unpack artifacts @@ -255,7 +255,7 @@ jobs: echo RUN_TYPE="MERGE RUN" >> $GITHUB_ENV - name: Trigger Bee Factory latest build if: github.ref == 'refs/heads/master' && github.event.action != 'beekeeper' && success() - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 with: token: ${{ secrets.GHA_PAT_BASIC }} repository: ethersphere/bee-factory diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index b9dbaa12f0e..9ba3d4bfd59 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -61,6 +61,7 @@ const ( optionNameRedistributionAddress = "redistribution-address" optionNameStakingAddress = "staking-address" optionNameBlockTime = "block-time" + optionNameBlockSyncInterval = "block-sync-interval" optionWarmUpTime = "warmup-time" optionNameMainNet = "mainnet" optionNameRetrievalCaching = "cache-retrieval" @@ -88,6 +89,8 @@ const ( optionAutoTLSDomain = "autotls-domain" optionAutoTLSRegistrationEndpoint = "autotls-registration-endpoint" optionAutoTLSCAEndpoint = "autotls-ca-endpoint" + optionNamePubsubBrokerMode = "pubsub-broker-mode" + optionNamePubsubMaxConnections = "pubsub-max-connections" // blockchain-rpc optionNameBlockchainRpcEndpoint = "blockchain-rpc-endpoint" @@ -312,6 +315,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().String(optionNameRedistributionAddress, "", "redistribution contract address") cmd.Flags().String(optionNameStakingAddress, "", "staking contract address") cmd.Flags().Uint64(optionNameBlockTime, 5, "chain block time") + cmd.Flags().Uint64(optionNameBlockSyncInterval, 10, "block number cache sync interval in blocks") cmd.Flags().Duration(optionWarmUpTime, time.Minute*5, "maximum node warmup duration; proceeds when stable or after this time") cmd.Flags().Bool(optionNameMainNet, true, "triggers connect to main net bootnodes.") cmd.Flags().Bool(optionNameRetrievalCaching, true, "enable forwarded content caching") @@ -337,6 +341,8 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().String(optionAutoTLSDomain, p2pforge.DefaultForgeDomain, "autotls domain") cmd.Flags().String(optionAutoTLSRegistrationEndpoint, p2pforge.DefaultForgeEndpoint, "autotls registration endpoint") cmd.Flags().String(optionAutoTLSCAEndpoint, p2pforge.DefaultCAEndpoint, "autotls certificate authority endpoint") + cmd.Flags().Bool(optionNamePubsubBrokerMode, true, "enable pubsub broker mode") + cmd.Flags().Int(optionNamePubsubMaxConnections, 0, "max pubsub connections per topic (0 = unlimited)") } // preRun must be called from every command's PreRunE, after which c.logger is diff --git a/cmd/bee/cmd/configurateoptions.go b/cmd/bee/cmd/configurateoptions.go index b6433d06877..759b82f9dd7 100644 --- a/cmd/bee/cmd/configurateoptions.go +++ b/cmd/bee/cmd/configurateoptions.go @@ -14,7 +14,6 @@ import ( ) func (c *command) initConfigurateOptionsCmd() (err error) { - cmd := &cobra.Command{ Use: "printconfig", Short: "Print default or provided configuration in yaml format", diff --git a/cmd/bee/cmd/db.go b/cmd/bee/cmd/db.go index 8bca2ed31cc..5ca0b58a762 100644 --- a/cmd/bee/cmd/db.go +++ b/cmd/bee/cmd/db.go @@ -440,7 +440,7 @@ func dbExportReserveCmd(cmd *cobra.Command) { hdr := &tar.Header{ Name: chunk.Address().String(), Size: int64(len(b)), - Mode: 0600, + Mode: 0o600, } if err := tw.WriteHeader(hdr); err != nil { return true, fmt.Errorf("writing header: %w", err) @@ -533,7 +533,7 @@ func dbExportPinningCmd(cmd *cobra.Command) { err = tw.WriteHeader(&tar.Header{ Name: root.String() + "/" + addr.String(), Size: int64(len(b)), - Mode: 0600, + Mode: 0o600, }) if err != nil { return true, fmt.Errorf("error writing header: %w", err) @@ -867,7 +867,8 @@ func dbNukeCmd(cmd *cobra.Command) { } return nil - }} + }, + } c.Flags().String(optionNameDataDir, "", "data directory") c.Flags().String(optionNameVerbosity, "trace", "verbosity level") c.Flags().Duration(optionNameSleepAfter, time.Duration(0), "time to sleep after the operation finished") diff --git a/cmd/bee/cmd/deploy.go b/cmd/bee/cmd/deploy.go index 11faac956b5..874dcc38a8b 100644 --- a/cmd/bee/cmd/deploy.go +++ b/cmd/bee/cmd/deploy.go @@ -59,6 +59,7 @@ func (c *command) initDeployCmd() error { IdleTimeout: c.config.GetDuration(configKeyBlockchainRpcIdleTimeout), Keepalive: c.config.GetDuration(configKeyBlockchainRpcKeepalive), }, + c.config.GetUint64(optionNameBlockSyncInterval), ) if err != nil { return err diff --git a/cmd/bee/cmd/split.go b/cmd/bee/cmd/split.go index 112b32ed2fa..92f86fadbe2 100644 --- a/cmd/bee/cmd/split.go +++ b/cmd/bee/cmd/split.go @@ -28,6 +28,7 @@ type putter struct { func (s *putter) Put(_ context.Context, chunk swarm.Chunk) error { return s.cb(chunk) } + func newPutter(cb func(ch swarm.Chunk) error) *putter { return &putter{ cb: cb, @@ -103,7 +104,7 @@ func splitRefs(cmd *cobra.Command) { refs = append(refs, ch.Address().String()) return nil }) - writer, err := os.OpenFile(outputFileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + writer, err := os.OpenFile(outputFileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open output file: %w", err) } @@ -191,7 +192,7 @@ func splitChunks(cmd *cobra.Command) { var chunksCount atomic.Int64 store := newPutter(func(chunk swarm.Chunk) error { filePath := filepath.Join(outputDir, chunk.Address().String()) - err := os.WriteFile(filePath, chunk.Data(), 0644) + err := os.WriteFile(filePath, chunk.Data(), 0o644) if err != nil { return err } diff --git a/cmd/bee/cmd/split_test.go b/cmd/bee/cmd/split_test.go index 4e819d6f46e..34a2b6f9a00 100644 --- a/cmd/bee/cmd/split_test.go +++ b/cmd/bee/cmd/split_test.go @@ -36,7 +36,7 @@ func TestDBSplitRefs(t *testing.T) { } inputFileName := path.Join(t.TempDir(), "input") - err = os.WriteFile(inputFileName, buf, 0644) + err = os.WriteFile(inputFileName, buf, 0o644) if err != nil { t.Fatal(err) } @@ -81,7 +81,7 @@ func TestDBSplitChunks(t *testing.T) { } inputFileName := path.Join(t.TempDir(), "input") - err = os.WriteFile(inputFileName, buf, 0644) + err = os.WriteFile(inputFileName, buf, 0o644) if err != nil { t.Fatal(err) } diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 469eb207f8f..7a09256c3fe 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -280,6 +280,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo BlockchainRpcKeepalive: c.config.GetDuration(configKeyBlockchainRpcKeepalive), BlockProfile: c.config.GetBool(optionNamePProfBlock), BlockTime: networkConfig.blockTime, + BlockSyncInterval: c.config.GetUint64(optionNameBlockSyncInterval), BootnodeMode: bootNode, Bootnodes: networkConfig.bootNodes, CacheCapacity: c.config.GetUint64(optionNameCacheCapacity), @@ -331,6 +332,8 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo WarmupTime: c.config.GetDuration(optionWarmUpTime), WelcomeMessage: c.config.GetString(optionWelcomeMessage), WhitelistedWithdrawalAddress: c.config.GetStringSlice(optionNameWhitelistedWithdrawalAddress), + PubsubBrokerMode: c.config.GetBool(optionNamePubsubBrokerMode), + PubsubMaxConnections: c.config.GetInt(optionNamePubsubMaxConnections), }) return b, err diff --git a/cmd/bee/cmd/start_dev.go b/cmd/bee/cmd/start_dev.go index 9c82d11be60..97f1f5fc9b3 100644 --- a/cmd/bee/cmd/start_dev.go +++ b/cmd/bee/cmd/start_dev.go @@ -18,7 +18,6 @@ import ( ) func (c *command) initStartDevCmd() (err error) { - cmd := &cobra.Command{ Use: "dev", Short: "Start in dev mode. WARNING: This command will be deprecated soon.", diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index 6bf0e89fc3c..4fe586ad8cc 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -2428,3 +2428,57 @@ paths: $ref: "SwarmCommon.yaml#/components/responses/400" default: description: Default response. + + "/pubsub/{topic}": + get: + summary: Connect to a pubsub topic via WebSocket + description: | + Opens a WebSocket connection to a pubsub topic. The connection acts as either a publisher (read+write) + or subscriber (read-only) depending on the presence of GSOC headers. + + **WebSocket protocol:** + - Inbound (client → node, publisher only): raw SOC payload `[sig:65B][span:8B][payload:N B]` + - Outbound (node → client): raw SOC payload `[sig:65B][span:8B][payload:N B]` + tags: + - Pubsub + parameters: + - in: path + name: topic + schema: + type: string + required: true + description: Topic identifier (hex-encoded address or arbitrary string to be hashed) + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPubsubPeer" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPubsubGsocEthAddress" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmPubsubGsocTopic" + - in: header + name: swarm-keep-alive + schema: + type: integer + required: false + description: WebSocket ping period in seconds (default: 60) + responses: + "101": + description: WebSocket upgrade successful + "400": + $ref: "SwarmCommon.yaml#/components/responses/400" + "500": + $ref: "SwarmCommon.yaml#/components/responses/500" + + "/pubsub/": + get: + summary: List all pubsub topics + description: Returns a list of all active pubsub topics this node is participating in (as broker or subscriber) + tags: + - Pubsub + responses: + "200": + description: List of pubsub topics + content: + application/json: + schema: + $ref: "SwarmCommon.yaml#/components/schemas/PubsubTopicListResponse" + "400": + $ref: "SwarmCommon.yaml#/components/responses/400" + "500": + $ref: "SwarmCommon.yaml#/components/responses/500" diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index ab39fa48555..d1fd4293642 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1076,6 +1076,31 @@ components: required: false description: "Indicates which feed version was resolved (v1 or v2)" + PubsubTopicInfo: + type: object + properties: + topicAddress: + type: string + description: "Hex-encoded topic address" + mode: + type: integer + description: "Pubsub mode identifier" + role: + type: string + description: "Role of this node: 'broker' or 'subscriber'" + connections: + type: array + items: + type: string + description: "List of connected peer overlays" + + PubsubTopicListResponse: + type: object + properties: + topics: + type: array + items: + $ref: "#/components/schemas/PubsubTopicInfo" parameters: GasPriceParameter: @@ -1279,6 +1304,30 @@ components: required: false description: "ACT history Unix timestamp" + SwarmPubsubPeer: + in: header + name: swarm-pubsub-peer + schema: + type: string + required: true + description: "Multiaddress of the broker peer to connect to for pubsub" + + SwarmPubsubGsocEthAddress: + in: header + name: swarm-pubsub-gsoc-eth-address + schema: + $ref: "#/components/schemas/HexString" + required: false + description: "GSOC owner Ethereum address (20 bytes, hex-encoded) for publisher role. Required together with swarm-pubsub-gsoc-topic to upgrade to publisher." + + SwarmPubsubGsocTopic: + in: header + name: swarm-pubsub-gsoc-topic + schema: + $ref: "#/components/schemas/HexString" + required: false + description: "GSOC topic identifier (hex) for publisher role. Required together with swarm-pubsub-gsoc-eth-address to upgrade to publisher." + responses: "200": description: Success diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 81f0f1548b4..ef04fb716c6 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -6,6 +6,8 @@ # api-addr: 127.0.0.1:1633 ## chain block time # block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 47d4fa7337c..65ad0b526cb 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -11,6 +11,7 @@ services: - BEE_AUTOTLS_DOMAIN - BEE_AUTOTLS_REGISTRATION_ENDPOINT - BEE_BLOCK_TIME + - BEE_BLOCK_SYNC_INTERVAL - BEE_BLOCKCHAIN_RPC_DIAL_TIMEOUT - BEE_BLOCKCHAIN_RPC_ENDPOINT - BEE_BLOCKCHAIN_RPC_IDLE_TIMEOUT diff --git a/packaging/docker/env b/packaging/docker/env index 96c3186728b..0a400437b3c 100644 --- a/packaging/docker/env +++ b/packaging/docker/env @@ -15,6 +15,8 @@ # BEE_AUTOTLS_REGISTRATION_ENDPOINT= ## chain block time (default 5) # BEE_BLOCK_TIME=5 +## sets how many estimated blocks Bee can trust local block-number extrapolation before forcing a fresh HeaderByNumber(nil) RPC sync (default 10) +# BEE_BLOCK_SYNC_INTERVAL=10 ## blockchain rpc TCP dial timeout (default 30s) # BEE_BLOCKCHAIN_RPC_DIAL_TIMEOUT=30s ## rpc blockchain endpoint (default empty) diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 4c44a5cb0f2..2aac12016fe 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -6,6 +6,8 @@ # api-addr: 127.0.0.1:1633 ## chain block time # block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index e61836ceefa..f812f395f07 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -6,6 +6,8 @@ # api-addr: 127.0.0.1:1633 ## chain block time # block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index 0e152ef19b4..f44aaf0e0d0 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -6,6 +6,8 @@ # api-addr: 127.0.0.1:1633 ## chain block time # block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" diff --git a/pkg/accesscontrol/kvs/mock/kvs.go b/pkg/accesscontrol/kvs/mock/kvs.go index a3d2ec3f56c..f33becf6e36 100644 --- a/pkg/accesscontrol/kvs/mock/kvs.go +++ b/pkg/accesscontrol/kvs/mock/kvs.go @@ -14,8 +14,10 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var lock = &sync.Mutex{} -var lockGetPut = &sync.Mutex{} +var ( + lock = &sync.Mutex{} + lockGetPut = &sync.Mutex{} +) type single struct { memoryMock map[string]map[string][]byte diff --git a/pkg/accesscontrol/session_test.go b/pkg/accesscontrol/session_test.go index a8dc4628f83..1abfca118c6 100644 --- a/pkg/accesscontrol/session_test.go +++ b/pkg/accesscontrol/session_test.go @@ -107,13 +107,11 @@ func TestSessionKeyFromKeystore(t *testing.T) { assertNoError(t, "", err) if !exists { assert.FailNow(t, "Key1 should exist") - } key1, created, err := ks.Key(tag1, password1, crypto.EDGSecp256_K1) assertNoError(t, "", err) if created { assert.FailNow(t, "Key1 should not be created") - } si2 := mock.NewFromKeystore(ks, tag2, password2, mockKeyFunc) diff --git a/pkg/accounting/accounting.go b/pkg/accounting/accounting.go index 12ed5a2676b..4780daa7962 100644 --- a/pkg/accounting/accounting.go +++ b/pkg/accounting/accounting.go @@ -216,7 +216,6 @@ func NewAccounting( lightFactor int64, p2pService p2p.Service, ) (*Accounting, error) { - lightPaymentThreshold := new(big.Int).Div(PaymentThreshold, big.NewInt(lightFactor)) lightRefreshRate := new(big.Int).Div(refreshRate, big.NewInt(lightFactor)) return &Accounting{ @@ -269,7 +268,6 @@ func (a *Accounting) getIncreasedExpectedDebt(peer swarm.Address, accountingPeer } func (a *Accounting) PrepareCredit(ctx context.Context, peer swarm.Address, price uint64, originated bool) (Action, error) { - accountingPeer := a.getAccountingPeer(peer) if err := accountingPeer.lock.TryLock(ctx); err != nil { @@ -522,7 +520,6 @@ func (a *Accounting) settle(peer swarm.Address, balance *accountingPeer) error { func (a *Accounting) Balance(peer swarm.Address) (balance *big.Int, err error) { var w bigint.BigInt err = a.store.Get(peerBalanceKey(peer), &w) - if err != nil { if errors.Is(err, storage.ErrNotFound) { return big.NewInt(0), ErrPeerNoBalance @@ -537,7 +534,6 @@ func (a *Accounting) Balance(peer swarm.Address) (balance *big.Int, err error) { func (a *Accounting) OriginatedBalance(peer swarm.Address) (balance *big.Int, err error) { var w bigint.BigInt err = a.store.Get(originatedBalanceKey(peer), &w) - if err != nil { if errors.Is(err, storage.ErrNotFound) { return big.NewInt(0), ErrPeerNoBalance @@ -552,7 +548,6 @@ func (a *Accounting) OriginatedBalance(peer swarm.Address) (balance *big.Int, er func (a *Accounting) SurplusBalance(peer swarm.Address) (balance *big.Int, err error) { var w bigint.BigInt err = a.store.Get(peerSurplusBalanceKey(peer), &w) - if err != nil { if errors.Is(err, storage.ErrNotFound) { return big.NewInt(0), nil @@ -638,7 +633,6 @@ func (a *Accounting) getAccountingPeer(peer swarm.Address) *accountingPeer { // to set the next checkpoint and increase the payment threshold given by 1 * refreshment rate // must be called under accountingPeer lock func (a *Accounting) notifyPaymentThresholdUpgrade(peer swarm.Address, accountingPeer *accountingPeer) { - // get appropriate linear growth limit based on whether the peer is a full node or a light node thresholdGrowChange := new(big.Int).Set(a.thresholdGrowChange) if !accountingPeer.fullNode { @@ -697,7 +691,6 @@ func (a *Accounting) Balances() (map[string]*big.Int, error) { return false, nil }) - if err != nil { return nil, err } @@ -801,7 +794,6 @@ func (a *Accounting) CompensatedBalances() (map[string]*big.Int, error) { return false, nil }) - if err != nil { return nil, err } @@ -822,7 +814,6 @@ func (a *Accounting) CompensatedBalances() (map[string]*big.Int, error) { return false, nil }) - if err != nil { return nil, err } @@ -893,7 +884,6 @@ func (a *Accounting) PeerDebt(peer swarm.Address) (*big.Int, error) { // peerLatentDebt returns the sum of the positive part of the outstanding balance, shadow reserve and the ghost balance func (a *Accounting) peerLatentDebt(peer swarm.Address) (*big.Int, error) { - accountingPeer := a.getAccountingPeer(peer) var wl bigint.BigInt @@ -1006,7 +996,6 @@ func (a *Accounting) NotifyPaymentSent(peer swarm.Address, amount *big.Int, rece if err != nil { a.logger.Warning("notify payment sent; failed to decrease originated balance", "error", err) } - } // NotifyPaymentThreshold should be called to notify accounting of changes in the payment threshold @@ -1186,7 +1175,6 @@ func (a *Accounting) NotifyRefreshmentSent(peer swarm.Address, attemptedAmount, if err != nil { a.logger.Warning("accounting: notifyrefreshmentsent failed to decrease originated balance", "error", err) } - } // NotifyRefreshmentReceived is called by pseudosettle when we receive a time based settlement. @@ -1398,7 +1386,6 @@ func (d *debitAction) Cleanup() { } func (a *Accounting) blocklistUntil(peer swarm.Address, multiplier int64) (int64, error) { - debt, err := a.peerLatentDebt(peer) if err != nil { return 0, err diff --git a/pkg/accounting/accounting_test.go b/pkg/accounting/accounting_test.go index ec72235534e..b30f46636d3 100644 --- a/pkg/accounting/accounting_test.go +++ b/pkg/accounting/accounting_test.go @@ -868,7 +868,6 @@ func TestAccountingCallSettlementMonetary(t *testing.T) { case <-time.After(1 * time.Second): t.Fatal("timeout waiting for payment") } - } func TestAccountingCallSettlementTooSoon(t *testing.T) { @@ -1105,7 +1104,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if err != nil { t.Fatal("Unexpected overflow from doable NotifyPayment") } - //sanity check surplus balance + // sanity check surplus balance val, err := acc.SurplusBalance(peer1Addr) if err != nil { t.Fatal("Error checking Surplusbalance") @@ -1113,7 +1112,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if val.Int64() != 2 { t.Fatal("Not expected surplus balance") } - //sanity check balance + // sanity check balance val, err = acc.Balance(peer1Addr) if err != nil { t.Fatal("Error checking Balance") @@ -1126,7 +1125,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if err != nil { t.Fatal("Unexpected error from NotifyPayment") } - //sanity check surplus balance + // sanity check surplus balance val, err = acc.SurplusBalance(peer1Addr) if err != nil { t.Fatal("Error checking Surplusbalance") @@ -1134,7 +1133,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if val.Int64() != testPaymentThreshold.Int64()+2 { t.Fatal("Unexpected surplus balance") } - //sanity check balance + // sanity check balance val, err = acc.Balance(peer1Addr) if err != nil { t.Fatal("Error checking Balance") @@ -1160,7 +1159,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if val.Int64() != 2 { t.Fatal("Unexpected surplus balance") } - //sanity check balance + // sanity check balance val, err = acc.Balance(peer1Addr) if err != nil { t.Fatal("Error checking Balance") @@ -1186,7 +1185,7 @@ func TestAccountingSurplusBalance(t *testing.T) { if val.Int64() != 0 { t.Fatal("Unexpected surplus balance") } - //sanity check balance + // sanity check balance val, err = acc.Balance(peer1Addr) if err != nil { t.Fatal("Error checking Balance") @@ -1730,7 +1729,6 @@ func TestAccountingReconnectBeforeAllowed(t *testing.T) { if blocklistTime != int64(4*paymentThresholdInRefreshmentSeconds) { t.Fatalf("unexpected blocklisting time, got %v expected %v", blocklistTime, 4*paymentThresholdInRefreshmentSeconds) } - } func TestAccountingResetBalanceAfterReconnect(t *testing.T) { @@ -1825,7 +1823,6 @@ func TestAccountingResetBalanceAfterReconnect(t *testing.T) { if surplusBalance.Int64() != 0 { t.Fatalf("surplus balance for peer %v not as expected got %d, wanted 0", peer.String(), balance) } - } func testAccountingSettlementGrowingThresholds(t *testing.T, settleFunc func(t *testing.T, acc *accounting.Accounting, peer1Addr swarm.Address, debitRefresh int64), fullNode bool, testPayThreshold *big.Int, testGrowth int64) { @@ -1938,14 +1935,12 @@ func testAccountingSettlementGrowingThresholds(t *testing.T, settleFunc func(t * if pricing.paymentThreshold.Cmp(checkPaymentThreshold) != 0 { t.Fatalf("expected threshold %v got %v", checkPaymentThreshold, pricing.paymentThreshold) } - } func TestAccountingRefreshGrowingThresholds(t *testing.T) { t.Parallel() testAccountingSettlementGrowingThresholds(t, debitAndRefresh, true, testPaymentThreshold, testRefreshRate) - } func TestAccountingRefreshGrowingThresholdsLight(t *testing.T) { @@ -1955,14 +1950,12 @@ func TestAccountingRefreshGrowingThresholdsLight(t *testing.T) { lightRefreshRate := testRefreshRate / testLightFactor testAccountingSettlementGrowingThresholds(t, debitAndRefresh, false, lightPaymentThresholdDefault, lightRefreshRate) - } func TestAccountingSwapGrowingThresholds(t *testing.T) { t.Parallel() testAccountingSettlementGrowingThresholds(t, debitAndReceivePayment, true, testPaymentThreshold, testRefreshRate) - } func TestAccountingSwapGrowingThresholdsLight(t *testing.T) { @@ -1972,7 +1965,6 @@ func TestAccountingSwapGrowingThresholdsLight(t *testing.T) { lightRefreshRate := testRefreshRate / testLightFactor testAccountingSettlementGrowingThresholds(t, debitAndReceivePayment, false, lightPaymentThresholdDefault, lightRefreshRate) - } func debitAndRefresh(t *testing.T, acc *accounting.Accounting, peer1Addr swarm.Address, debitRefresh int64) { @@ -1994,7 +1986,6 @@ func debitAndRefresh(t *testing.T, acc *accounting.Accounting, peer1Addr swarm.A if err != nil { t.Fatalf("unexpected error from NotifyRefreshmentReceived: %v", err) } - } func debitAndReceivePayment(t *testing.T, acc *accounting.Accounting, peer1Addr swarm.Address, debitRefresh int64) { @@ -2016,5 +2007,4 @@ func debitAndReceivePayment(t *testing.T, acc *accounting.Accounting, peer1Addr if err != nil { t.Fatalf("unexpected error from NotifyRefreshmentReceived: %v", err) } - } diff --git a/pkg/accounting/mock/accounting.go b/pkg/accounting/mock/accounting.go index cbf838f9ba3..731b6fcc6b9 100644 --- a/pkg/accounting/mock/accounting.go +++ b/pkg/accounting/mock/accounting.go @@ -232,11 +232,9 @@ func (s *Service) PeerAccounting() (map[string]accounting.PeerInfo, error) { } func (s *Service) Connect(peer swarm.Address, full bool) { - } func (s *Service) Disconnect(peer swarm.Address) { - } func (s *Service) SurplusBalance(peer swarm.Address) (*big.Int, error) { diff --git a/pkg/api/accesscontrol_test.go b/pkg/api/accesscontrol_test.go index 91dcfcc6efb..737a47ebc11 100644 --- a/pkg/api/accesscontrol_test.go +++ b/pkg/api/accesscontrol_test.go @@ -838,7 +838,6 @@ func TestAccessLogicPublisher(t *testing.T) { }), jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), ) - }) t.Run("download-w/o-publisher", func(t *testing.T) { diff --git a/pkg/api/accounting_test.go b/pkg/api/accounting_test.go index c2db87d2cdb..901ca8f2883 100644 --- a/pkg/api/accounting_test.go +++ b/pkg/api/accounting_test.go @@ -100,7 +100,6 @@ func TestAccountingInfo(t *testing.T) { if !reflect.DeepEqual(got, expected) { t.Errorf("got accounting: %v, expected: %v", got, expected) } - } func TestAccountingInfoError(t *testing.T) { diff --git a/pkg/api/api.go b/pkg/api/api.go index 93595168cc3..748ccc2500b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -41,6 +41,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/postage" "github.com/ethersphere/bee/v2/pkg/postage/postagecontract" "github.com/ethersphere/bee/v2/pkg/pss" + "github.com/ethersphere/bee/v2/pkg/pubsub" "github.com/ethersphere/bee/v2/pkg/resolver" "github.com/ethersphere/bee/v2/pkg/resolver/client/ens" "github.com/ethersphere/bee/v2/pkg/resolver/multiresolver" @@ -94,6 +95,9 @@ const ( SwarmActTimestampHeader = "Swarm-Act-Timestamp" SwarmActPublisherHeader = "Swarm-Act-Publisher" SwarmActHistoryAddressHeader = "Swarm-Act-History-Address" + SwarmPubsubPeerHeader = "Swarm-Pubsub-Peer" + SwarmPubsubGsocEthAddressHeader = "Swarm-Pubsub-Gsoc-Eth-Address" + SwarmPubsubGsocTopicHeader = "Swarm-Pubsub-Gsoc-Topic" ImmutableHeader = "Immutable" GasPriceHeader = "Gas-Price" @@ -187,6 +191,7 @@ type Service struct { topologyDriver topology.Driver p2p p2p.DebugService + pubsubSvc *pubsub.Service accounting accounting.Interface chequebook chequebook.Service pseudosettle settlement.Interface @@ -270,6 +275,7 @@ type ExtraOptions struct { SyncStatus func() (bool, error) NodeStatus *status.Service PinIntegrity PinIntegrity + PubsubService *pubsub.Service } func New( @@ -361,6 +367,7 @@ func (s *Service) Configure(signer crypto.Signer, tracer *tracing.Tracer, o Opti s.lightNodes = e.LightNodes s.pseudosettle = e.Pseudosettle s.blockTime = e.BlockTime + s.pubsubSvc = e.PubsubService s.statusSem = semaphore.NewWeighted(1) s.postageSem = semaphore.NewWeighted(1) @@ -589,6 +596,7 @@ func (s *Service) corsHandler(h http.Handler) http.Handler { SwarmRedundancyStrategyHeader, SwarmRedundancyFallbackModeHeader, SwarmChunkRetrievalTimeoutHeader, SwarmLookAheadBufferSizeHeader, SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader, SwarmOnlyRootChunk, GasPriceHeader, GasLimitHeader, ImmutableHeader, SwarmActHeader, SwarmActTimestampHeader, SwarmActPublisherHeader, SwarmActHistoryAddressHeader, + SwarmPubsubPeerHeader, SwarmPubsubGsocEthAddressHeader, SwarmPubsubGsocTopicHeader, } allowedHeadersStr := strings.Join(allowedHeaders, ", ") diff --git a/pkg/api/debugstorage_test.go b/pkg/api/debugstorage_test.go index 14a669f6b10..dc13f8d81bf 100644 --- a/pkg/api/debugstorage_test.go +++ b/pkg/api/debugstorage_test.go @@ -42,5 +42,4 @@ func TestDebugStorage(t *testing.T) { jsonhttptest.WithExpectedJSONResponse(want), ) }) - } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 4f27142dfab..aedb7b3aadf 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -84,7 +84,6 @@ func newMetrics() metrics { } func toFileSizeBucket(bytes int64) int64 { - for _, s := range fileSizeBucketsKBytes { if (s * bytesInKB) >= bytes { return s * bytesInKB diff --git a/pkg/api/peer.go b/pkg/api/peer.go index 1c918b98fa9..543fae42219 100644 --- a/pkg/api/peer.go +++ b/pkg/api/peer.go @@ -32,7 +32,6 @@ func (s *Service) peerConnectHandler(w http.ResponseWriter, r *http.Request) { } bzzAddr, err := s.p2p.Connect(r.Context(), []multiaddr.Multiaddr{paths.MultiAddress}) - if err != nil { logger.Debug("p2p connect failed", "addresses", paths.MultiAddress, "error", err) logger.Error(nil, "p2p connect failed", "addresses", paths.MultiAddress) diff --git a/pkg/api/probe.go b/pkg/api/probe.go index 89d5083f964..a4369afc976 100644 --- a/pkg/api/probe.go +++ b/pkg/api/probe.go @@ -58,7 +58,6 @@ func (p *Probe) Healthy() ProbeStatus { return ProbeStatusNOK } return p.healthy.get() - } // SetHealthy updates the value of the healthy status. diff --git a/pkg/api/pubsub.go b/pkg/api/pubsub.go new file mode 100644 index 00000000000..317204882bf --- /dev/null +++ b/pkg/api/pubsub.go @@ -0,0 +1,154 @@ +// 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 api + +import ( + "context" + "encoding/hex" + "net/http" + "time" + + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/pubsub" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + ma "github.com/multiformats/go-multiaddr" +) + +func (s *Service) pubsubWsHandler(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("pubsub").Build() + + paths := struct { + Topic string `map:"topic" validate:"required"` + }{} + if response := s.mapStructure(mux.Vars(r), &paths); response != nil { + response("invalid path params", logger, w) + return + } + + var topicAddr [32]byte + if decoded, err := hex.DecodeString(paths.Topic); err == nil && len(decoded) == swarm.HashSize { + copy(topicAddr[:], decoded) + } else { + h := swarm.NewHasher() + _, _ = h.Write([]byte(paths.Topic)) + copy(topicAddr[:], h.Sum(nil)) + } + + // Required: underlay multiaddr — accept from header or query param (browsers cannot set WS headers) + peerHeader := r.Header.Get(SwarmPubsubPeerHeader) + if peerHeader == "" { + peerHeader = r.URL.Query().Get("swarm-pubsub-peer") + } + if peerHeader == "" { + jsonhttp.BadRequest(w, "missing Swarm-Pubsub-Peer header") + return + } + underlay, err := ma.NewMultiaddr(peerHeader) + if err != nil { + logger.Info("invalid peer multiaddr", "value", peerHeader, "error", err) + jsonhttp.BadRequest(w, "invalid Swarm-Pubsub-Peer header") + return + } + + // Optional: GSOC fields for Publisher upgrade — accept from header or query param + var connectOpts pubsub.ConnectOptions + + gsocEthAddrHex := r.Header.Get(SwarmPubsubGsocEthAddressHeader) + if gsocEthAddrHex == "" { + gsocEthAddrHex = r.URL.Query().Get("swarm-pubsub-gsoc-eth-address") + } + gsocTopicHex := r.Header.Get(SwarmPubsubGsocTopicHeader) + if gsocTopicHex == "" { + gsocTopicHex = r.URL.Query().Get("swarm-pubsub-gsoc-topic") + } + if gsocEthAddrHex != "" && gsocTopicHex != "" { + gsocOwner, err := hex.DecodeString(gsocEthAddrHex) + if err != nil { + jsonhttp.BadRequest(w, "invalid Swarm-Pubsub-Gsoc-Eth-Address header") + return + } + gsocID, err := hex.DecodeString(gsocTopicHex) + if err != nil { + jsonhttp.BadRequest(w, "invalid Swarm-Pubsub-Gsoc-Topic header") + return + } + connectOpts.GsocOwner = gsocOwner + connectOpts.GsocID = gsocID + connectOpts.ReadWrite = true + } + + headers := struct { + KeepAlive time.Duration `map:"Swarm-Keep-Alive"` + }{} + if response := s.mapStructure(r.Header, &headers); response != nil { + response("invalid header params", logger, w) + return + } + + if s.beeMode == DevMode { + logger.Warning("pubsub endpoint is disabled in dev mode") + jsonhttp.BadRequest(w, errUnsupportedDevNodeOperation) + return + } + + // Connect to broker peer + ctx, cancel := context.WithCancel(context.Background()) + mode, err := s.pubsubSvc.Connect(ctx, underlay, topicAddr, pubsub.ModeGSOCEphemeral, connectOpts) + if err != nil { + cancel() + logger.Info("pubsub connect failed", "error", err) + jsonhttp.InternalServerError(w, "pubsub connect failed") + return + } + // Upgrade to WebSocket + upgrader := websocket.Upgrader{ + ReadBufferSize: swarm.ChunkWithSpanSize, + WriteBufferSize: swarm.ChunkWithSpanSize, + CheckOrigin: s.checkOrigin, + } + + logger.Info("upgrading to websocket") + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + // Upgrade() hijacks the connection before returning an error, + // so do NOT write an HTTP response here. + cancel() + logger.Info("websocket upgrade failed", "error", err) + logger.Error(nil, "websocket upgrade failed") + return + } + logger.Info("websocket upgrade successful") + + pingPeriod := headers.KeepAlive * time.Second + if pingPeriod == 0 { + pingPeriod = time.Minute + } + + isPublisher := connectOpts.ReadWrite + + s.wsWg.Add(1) + go func() { + pubsub.ListeningWs(ctx, conn, pubsub.WsOptions{PingPeriod: pingPeriod, Cancel: cancel}, logger, mode, isPublisher) + cancel() + _ = conn.Close() + s.wsWg.Done() + }() +} + +func (s *Service) pubsubListHandler(w http.ResponseWriter, r *http.Request) { + if s.pubsubSvc == nil { + jsonhttp.NotFound(w, "pubsub service not available") + return + } + + topics := s.pubsubSvc.Topics() + jsonhttp.OK(w, struct { + Topics []pubsub.TopicInfo `json:"topics"` + }{ + Topics: topics, + }) +} diff --git a/pkg/api/router.go b/pkg/api/router.go index 776def9457d..3eeba8b2bc5 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -44,7 +44,15 @@ func (s *Service) Mount() { s.Handler = web.ChainHandlers( httpaccess.NewHTTPAccessLogHandler(s.logger, s.tracer, "api access"), - handlers.CompressHandler, + func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + h.ServeHTTP(w, r) + return + } + handlers.CompressHandler(h).ServeHTTP(w, r) + }) + }, s.corsHandler, web.NoCacheHeadersHandler, web.FinalHandler(router), @@ -74,6 +82,16 @@ func (s *Service) EnableFullAPI() { } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip compression for WebSocket upgrade requests. + // CompressHandler wraps the ResponseWriter with a gzip writer; when + // the WebSocket upgrader hijacks the connection and the handler returns, + // the gzip writer tries to flush/close and writes to the hijacked + // connection, causing "response.Write on hijacked connection" errors. + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + h.ServeHTTP(w, r) + return + } + // Skip compression for GET requests on download endpoints. // This is done in order to preserve Content-Length header in response, // because CompressHandler is always removing it. @@ -364,6 +382,14 @@ func (s *Service) mountAPI() { ), }) + handle("/pubsub/{topic}", http.HandlerFunc(s.pubsubWsHandler)) + + handle("/pubsub/", web.ChainHandlers( + web.FinalHandler(jsonhttp.MethodHandler{ + "GET": http.HandlerFunc(s.pubsubListHandler), + }), + )) + handle("/pss/subscribe/{topic}", http.HandlerFunc(s.pssWsHandler)) handle("/gsoc/subscribe/{address}", web.ChainHandlers( diff --git a/pkg/api/status_test.go b/pkg/api/status_test.go index c6e92bc7ddb..d7a33083534 100644 --- a/pkg/api/status_test.go +++ b/pkg/api/status_test.go @@ -110,6 +110,7 @@ func (m *topologyPeersIterNoopMock) EachConnectedPeer(_ topology.EachPeerFunc, _ func (m *topologyPeersIterNoopMock) EachConnectedPeerRev(_ topology.EachPeerFunc, _ topology.Select) error { return nil } + func (m *topologyPeersIterNoopMock) IsReachable() bool { return true } @@ -137,6 +138,7 @@ func (m *statusSnapshotMock) GetChainState() *postage.ChainState { return m.chai func (m *statusSnapshotMock) ReserveSizeWithinRadius() uint64 { return m.reserveSizeWithinRadius } + func (m *statusSnapshotMock) NeighborhoodsStat(ctx context.Context) ([]*storer.NeighborhoodStat, error) { return m.neighborhoods, nil } diff --git a/pkg/api/wallet.go b/pkg/api/wallet.go index 5cdd0de087d..75674e1f48b 100644 --- a/pkg/api/wallet.go +++ b/pkg/api/wallet.go @@ -7,9 +7,8 @@ package api import ( "math/big" "net/http" - "strings" - "slices" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/bigint" diff --git a/pkg/api/welcome_message_test.go b/pkg/api/welcome_message_test.go index 6eb6670e8ab..4127f8e3e43 100644 --- a/pkg/api/welcome_message_test.go +++ b/pkg/api/welcome_message_test.go @@ -25,7 +25,8 @@ func TestGetWelcomeMessage(t *testing.T) { srv, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithGetWelcomeMessageFunc(func() string { return DefaultTestWelcomeMessage - }))}) + })), + }) jsonhttptest.Request(t, srv, http.MethodGet, "/welcome-message", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(api.WelcomeMessageResponse{ diff --git a/pkg/bzz/utilities_test.go b/pkg/bzz/utilities_test.go index 6a9fd9a97dd..3f0b88c54a9 100644 --- a/pkg/bzz/utilities_test.go +++ b/pkg/bzz/utilities_test.go @@ -45,6 +45,7 @@ func Test_ContainsAddress(t *testing.T) { } } } + func makeAddreses(t *testing.T, count int) []bzz.Address { t.Helper() diff --git a/pkg/crypto/dh_test.go b/pkg/crypto/dh_test.go index a1dfbff6487..a092e6f9b22 100644 --- a/pkg/crypto/dh_test.go +++ b/pkg/crypto/dh_test.go @@ -89,5 +89,4 @@ func TestSharedKey(t *testing.T) { if !bytes.Equal(sk, expectedSK) { t.Fatalf("incorrect shared key: expected %v, got %x", expectedSK, sk) } - } diff --git a/pkg/crypto/edg.go b/pkg/crypto/edg.go index 577c6993202..d00220c8da7 100644 --- a/pkg/crypto/edg.go +++ b/pkg/crypto/edg.go @@ -16,9 +16,11 @@ var EDGSecp256_K1 = new(edgSecp256_k1) func (s *edgSecp256_k1) Generate() (*ecdsa.PrivateKey, error) { return GenerateSecp256k1Key() } + func (s *edgSecp256_k1) Encode(k *ecdsa.PrivateKey) ([]byte, error) { return EncodeSecp256k1PrivateKey(k) } + func (s *edgSecp256_k1) Decode(data []byte) (*ecdsa.PrivateKey, error) { return DecodeSecp256k1PrivateKey(data) } @@ -31,9 +33,11 @@ var EDGSecp256_R1 = new(edgSecp256_r1) func (s *edgSecp256_r1) Generate() (*ecdsa.PrivateKey, error) { return GenerateSecp256r1Key() } + func (s *edgSecp256_r1) Encode(k *ecdsa.PrivateKey) ([]byte, error) { return EncodeSecp256r1PrivateKey(k) } + func (s *edgSecp256_r1) Decode(data []byte) (*ecdsa.PrivateKey, error) { return DecodeSecp256r1PrivateKey(data) } diff --git a/pkg/crypto/eip712/typeddata.go b/pkg/crypto/eip712/typeddata.go index 14d62dd549a..5230146f335 100644 --- a/pkg/crypto/eip712/typeddata.go +++ b/pkg/crypto/eip712/typeddata.go @@ -11,11 +11,13 @@ import ( ) // type aliases to avoid importing "core" everywhere -type TypedData = apitypes.TypedData -type TypedDataDomain = apitypes.TypedDataDomain -type Types = apitypes.Types -type Type = apitypes.Type -type TypedDataMessage = apitypes.TypedDataMessage +type ( + TypedData = apitypes.TypedData + TypedDataDomain = apitypes.TypedDataDomain + Types = apitypes.Types + Type = apitypes.Type + TypedDataMessage = apitypes.TypedDataMessage +) // EncodeForSigning encodes the hash that will be signed for the given EIP712 data func EncodeForSigning(typedData *TypedData) ([]byte, error) { diff --git a/pkg/crypto/signer.go b/pkg/crypto/signer.go index 586f93535ba..16d88f16b0f 100644 --- a/pkg/crypto/signer.go +++ b/pkg/crypto/signer.go @@ -17,9 +17,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/crypto/eip712" ) -var ( - ErrInvalidLength = errors.New("invalid signature length") -) +var ErrInvalidLength = errors.New("invalid signature length") type Signer interface { // Sign signs data with ethereum prefix (eip191 type 0x45). diff --git a/pkg/discovery/mock/mock.go b/pkg/discovery/mock/mock.go index 8fc9b861589..edab539ea6c 100644 --- a/pkg/discovery/mock/mock.go +++ b/pkg/discovery/mock/mock.go @@ -13,7 +13,7 @@ import ( type Discovery struct { mtx sync.Mutex - ctr int //how many ops + ctr int // how many ops records map[string][]swarm.Address broadcastFunc func(context.Context, swarm.Address, ...swarm.Address) error } diff --git a/pkg/encryption/elgamal/encryption_test.go b/pkg/encryption/elgamal/encryption_test.go index ea14c77bc9f..46ee31d2fe2 100644 --- a/pkg/encryption/elgamal/encryption_test.go +++ b/pkg/encryption/elgamal/encryption_test.go @@ -57,5 +57,4 @@ func TestElgamalCorrect(t *testing.T) { if !bytes.Equal(plaintext, expected) { t.Fatalf("original and encrypted-decrypted plaintexts do no match: expected %x, got %x", expected, plaintext) } - } diff --git a/pkg/encryption/encryption_test.go b/pkg/encryption/encryption_test.go index 01d7c70d687..e58743f2495 100644 --- a/pkg/encryption/encryption_test.go +++ b/pkg/encryption/encryption_test.go @@ -29,8 +29,10 @@ import ( var expectedTransformedHex = "352187af3a843decc63ceca6cb01ea39dbcf77caf0a8f705f5c30d557044ceec9392b94a79376f1e5c10cd0c0f2a98e5353bf22b3ea4fdac6677ee553dec192e3db64e179d0474e96088fb4abd2babd67de123fb398bdf84d818f7bda2c1ab60b3ea0e0569ae54aa969658eb4844e6960d2ff44d7c087ee3aaffa1c0ee5df7e50b615f7ad90190f022934ad5300c7d1809bfe71a11cc04cece5274eb97a5f20350630522c1dbb7cebaf4f97f84e03f5cfd88f2b48880b25d12f4d5e75c150f704ef6b46c72e07db2b705ac3644569dccd22fd8f964f6ef787fda63c46759af334e6f665f70eac775a7017acea49f3c7696151cb1b9434fa4ac27fb803921ffb5ec58dafa168098d7d5b97e384be3384cf5bc235c3d887fef89fe76c0065f9b8d6ad837b442340d9e797b46ef5709ea3358bc415df11e4830de986ef0f1c418ffdcc80e9a3cda9bea0ab5676c0d4240465c43ba527e3b4ea50b4f6255b510e5d25774a75449b0bd71e56c537ade4fcf0f4d63c99ae1dbb5a844971e2c19941b8facfcfc8ee3056e7cb3c7114c5357e845b52f7103cb6e00d2308c37b12baa5b769e1cc7b00fc06f2d16e70cc27a82cb9c1a4e40cb0d43907f73df2c9db44f1b51a6b0bc6d09f77ac3be14041fae3f9df2da42df43ae110904f9ecee278030185254d7c6e918a5512024d047f77a992088cb3190a6587aa54d0c7231c1cd2e455e0d4c07f74bece68e29cd8ba0190c0bcfb26d24634af5d91a81ef5d4dd3d614836ce942ddbf7bb1399317f4c03faa675f325f18324bf9433844bfe5c4cc04130c8d5c329562b7cd66e72f7355de8f5375a72202971613c32bd7f3fcdcd51080758cd1d0a46dbe8f0374381dbc359f5864250c63dde8131cbd7c98ae2b0147d6ea4bf65d1443d511b18e6d608bbb46ac036353b4c51df306a10a6f6939c38629a5c18aaf89cac04bd3ad5156e6b92011c88341cb08551bab0a89e6a46538f5af33b86121dba17e3a434c273f385cd2e8cb90bdd32747d8425d929ccbd9b0815c73325988855549a8489dfd047daf777aaa3099e54cf997175a5d9e1edfe363e3b68c70e02f6bf4fcde6a0f3f7d0e7e98bde1a72ae8b6cd27b32990680cc4a04fc467f41c5adcaddabfc71928a3f6872c360c1d765260690dd28b269864c8e380d9c92ef6b89b0094c8f9bb22608b4156381b19b920e9583c9616ce5693b4d2a6c689f02e6a91584a8e501e107403d2689dd0045269dd9946c0e969fb656a3b39d84a798831f5f9290f163eb2f97d3ae25071324e95e2256d9c1e56eb83c26397855323edc202d56ad05894333b7f0ed3c1e4734782eb8bd5477242fd80d7a89b12866f85cfae476322f032465d6b1253993033fccd4723530630ab97a1566460af9c90c9da843c229406e65f3fa578bd6bf04dee9b6153807ddadb8ceefc5c601a8ab26023c67b1ab1e8e0f29ce94c78c308005a781853e7a2e0e51738939a657c987b5e611f32f47b5ff461c52e63e0ea390515a8e1f5393dae54ea526934b5f310b76e3fa050e40718cb4c8a20e58946d6ee1879f08c52764422fe542b3240e75eccb7aa75b1f8a651e37a3bc56b0932cdae0e985948468db1f98eb4b77b82081ea25d8a762db00f7898864984bd80e2f3f35f236bf57291dec28f550769943bcfb6f884b7687589b673642ef7fe5d7d5a87d3eca5017f83ccb9a3310520474479464cb3f433440e7e2f1e28c0aef700a45848573409e7ab66e0cfd4fe5d2147ace81bc65fd8891f6245cd69246bbf5c27830e5ab882dd1d02aba34ff6ca9af88df00fd602892f02fedbdc65dedec203faf3f8ff4a97314e0ddb58b9ab756a61a562597f4088b445fcc3b28a708ca7b1485dcd791b779fbf2b3ef1ec5c6205f595fbe45a02105034147e5a146089c200a49dae33ae051a08ea5f974a21540aaeffa7f9d9e3d35478016fb27b871036eb27217a5b834b461f535752fb5f1c8dded3ae14ce3a2ef6639e2fe41939e3509e46e347a95d50b2080f1ba42c804b290ddc912c952d1cec3f2661369f738feacc0dbf1ea27429c644e45f9e26f30c341acd34c7519b2a1663e334621691e810767e9918c2c547b2e23cce915f97d26aac8d0d2fcd3edb7986ad4e2b8a852edebad534cb6c0e9f0797d3563e5409d7e068e48356c67ce519246cd9c560e881453df97cbba562018811e6cf8c327f399d1d1253ab47a19f4a0ccc7c6d86a9603e0551da310ea595d71305c4aad96819120a92cdbaf1f77ec8df9cc7c838c0d4de1e8692dd81da38268d1d71324bcffdafbe5122e4b81828e021e936d83ae8021eac592aa52cd296b5ce392c7173d622f8e07d18f59bb1b08ba15211af6703463b09b593af3c37735296816d9f2e7a369354a5374ea3955e14ca8ac56d5bfe4aef7a21bd825d6ae85530bee5d2aaaa4914981b3dfdb2e92ec2a27c83d74b59e84ff5c056f7d8945745f2efc3dcf28f288c6cd8383700fb2312f7001f24dd40015e436ae23e052fe9070ea9535b9c989898a9bda3d5382cf10e432fae6ccf0c825b3e6436edd3a9f8846e5606f8563931b5f29ba407c5236e5730225dda211a8504ec1817bc935e1fd9a532b648c502df302ed2063aed008fd5676131ac9e95998e9447b02bd29d77e38fcfd2959f2de929b31970335eb2a74348cc6918bc35b9bf749eab0fe304c946cd9e1ca284e6853c42646e60b6b39e0d3fb3c260abfc5c1b4ca3c3770f344118ca7c7f5c1ad1f123f8f369cd60afc3cdb3e9e81968c5c9fa7c8b014ffe0508dd4f0a2a976d5d1ca8fc9ad7a237d92cfe7b41413d934d6e142824b252699397e48e4bac4e91ebc10602720684bd0863773c548f9a2f9724245e47b129ecf65afd7252aac48c8a8d6fd3d888af592a01fb02dc71ed7538a700d3d16243e4621e0fcf9f8ed2b4e11c9fa9a95338bb1dac74a7d9bc4eb8cbf900b634a2a56469c00f5994e4f0934bdb947640e6d67e47d0b621aacd632bfd3c800bd7d93bd329f494a90e06ed51535831bd6e07ac1b4b11434ef3918fa9511813a002913f33f836454798b8d1787fea9a4c4743ba091ed192ed92f4d33e43a226bf9503e1a83a16dd340b3cbbf38af6db0d99201da8de529b4225f3d2fa2aad6621afc6c79ef3537720591edfc681ae6d00ede53ed724fc71b23b90d2e9b7158aaee98d626a4fe029107df2cb5f90147e07ebe423b1519d848af18af365c71bfd0665db46be493bbe99b79a188de0cf3594aef2299f0324075bdce9eb0b87bc29d62401ba4fd6ae48b1ba33261b5b845279becf38ee03e3dc5c45303321c5fac96fd02a3ad8c9e3b02127b320501333c9e6360440d1ad5e64a6239501502dde1a49c9abe33b66098458eee3d611bb06ffcd234a1b9aef4af5021cd61f0de6789f822ee116b5078aae8c129e8391d8987500d322b58edd1595dc570b57341f2df221b94a96ab7fbcf32a8ca9684196455694024623d7ed49f7d66e8dd453c0bae50e0d8b34377b22d0ece059e2c385dfc70b9089fcd27577c51f4d870b5738ee2b68c361a67809c105c7848b68860a829f29930857a9f9d40b14fd2384ac43bafdf43c0661103794c4bd07d1cfdd4681b6aeaefad53d4c1473359bcc5a83b09189352e5bb9a7498dd0effb89c35aad26954551f8b0621374b449bf515630bd3974dca982279733470fdd059aa9c3df403d8f22b38c4709c82d8f12b888e22990350490e16179caf406293cc9e65f116bafcbe96af132f679877061107a2f690a82a8cb46eea57a90abd23798c5937c6fe6b17be3f9bfa01ce117d2c268181b9095bf49f395fea07ca03838de0588c5e2db633e836d64488c1421e653ea52d810d096048c092d0da6e02fa6613890219f51a76148c8588c2487b171a28f17b7a299204874af0131725d793481333be5f08e86ca837a226850b0c1060891603bfecf9e55cddd22c0dbb28d495342d9cc3de8409f72e52a0115141cffe755c74f061c1a770428ccb0ae59536ee6fc074fbfc6cacb51a549d327527e20f8407477e60355863f1153f9ce95641198663c968874e7fdb29407bd771d94fdda8180cbb0358f5874738db705924b8cbe0cd5e1484aeb64542fe8f38667b7c34baf818c63b1e18440e9fba575254d063fd49f24ef26432f4eb323f3836972dca87473e3e9bb26dc3be236c3aae6bc8a6da567442309da0e8450e242fc9db836e2964f2c76a3b80a2c677979882dda7d7ebf62c93664018bcf4ec431fe6b403d49b3b36618b9c07c2d0d4569cb8d52223903debc72ec113955b206c34f1ae5300990ccfc0180f47d91afdb542b6312d12aeff7e19c645dc0b9fe6e3288e9539f6d5870f99882df187bfa6d24d179dfd1dac22212c8b5339f7171a3efc15b760fed8f68538bc5cbd845c2d1ab41f3a6c692820653eaef7930c02fbe6061d93805d73decdbb945572a7c44ed0241982a6e4d2d730898f82b3d9877cb7bca41cc6dcee67aa0c3d6db76f0b0a708ace0031113e48429de5d886c10e9200f68f32263a2fbf44a5992c2459fda7b8796ba796e3a0804fc25992ed2c9a5fe0580a6b809200ecde6caa0364b58be11564dcb9a616766dd7906db5636ee708b0204f38d309466d8d4a162965dd727e29f5a6c133e9b4ed5bafe803e479f9b2a7640c942c4a40b14ac7dc9828546052761a070f6404008f1ec3605836339c3da95a00b4fd81b2cabf88b51d2087d5b83e8c5b69bf96d8c72cbd278dad3bbb42b404b436f84ad688a22948adf60a81090f1e904291503c16e9f54b05fc76c881a5f95f0e732949e95d3f1bae2d3652a14fe0dda2d68879604657171856ef72637def2a96ac47d7b3fe86eb3198f5e0e626f06be86232305f2ae79ffcd2725e48208f9d8d63523f81915acc957563ab627cd6bc68c2a37d59fb0ed77a90aa9d085d6914a8ebada22a2c2d471b5163aeddd799d90fbb10ed6851ace2c4af504b7d572686700a59d6db46d5e42bb83f8e0c0ffe1dfa6582cc0b34c921ff6e85e83188d24906d5c08bb90069639e713051b3102b53e6f703e8210017878add5df68e6f2b108de279c5490e9eef5590185c4a1c744d4e00d244e1245a8805bd30407b1bc488db44870ccfd75a8af104df78efa2fb7ba31f048a263efdb3b63271fff4922bece9a71187108f65744a24f4947dc556b7440cb4fa45d296bb7f724588d1f245125b21ea063500029bd49650237f53899daf1312809552c81c5827341263cc807a29fe84746170cdfa1ff3838399a5645319bcaff674bb70efccdd88b3d3bb2f2d98111413585dc5d5bd5168f43b3f55e58972a5b2b9b3733febf02f931bd436648cb617c3794841aab961fe41277ab07812e1d3bc4ff6f4350a3e615bfba08c3b9480ef57904d3a16f7e916345202e3f93d11f7a7305170cb8c4eb9ac88ace8bbd1f377bdd5855d3162d6723d4435e84ce529b8f276a8927915ac759a0d04e5ca4a9d3da6291f0333b475df527e99fe38f7a4082662e8125936640c26dd1d17cf284ce6e2b17777a05aa0574f7793a6a062cc6f7263f7ab126b4528a17becfdec49ac0f7d8705aa1704af97fb861faa8a466161b2b5c08a5bacc79fe8500b913d65c8d3c52d1fd52d2ab2c9f52196e712455619c1cd3e0f391b274487944240e2ed8858dd0823c801094310024ae3fe4dd1cf5a2b6487b42cc5937bbafb193ee331d87e378258963d49b9da90899bbb4b88e79f78e866b0213f4719f67da7bcc2fce073c01e87c62ea3cdbcd589cfc41281f2f4c757c742d6d1e" -var hashFunc = sha3.NewLegacyKeccak256 -var testKey encryption.Key +var ( + hashFunc = sha3.NewLegacyKeccak256 + testKey encryption.Key +) // nolint:gochecknoinits func init() { diff --git a/pkg/feeds/epochs/finder.go b/pkg/feeds/epochs/finder.go index d5133fdaa57..fedce775cf5 100644 --- a/pkg/feeds/epochs/finder.go +++ b/pkg/feeds/epochs/finder.go @@ -13,8 +13,10 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var _ feeds.Lookup = (*finder)(nil) -var _ feeds.Lookup = (*asyncFinder)(nil) +var ( + _ feeds.Lookup = (*finder)(nil) + _ feeds.Lookup = (*asyncFinder)(nil) +) // finder encapsulates a chunk store getter and a feed and provides // non-concurrent lookup methods @@ -156,6 +158,7 @@ func (f *asyncFinder) at(ctx context.Context, at int64, p *path, e *epoch, c cha } } } + func (f *asyncFinder) At(ctx context.Context, at int64, after uint64) (swarm.Chunk, feeds.Index, feeds.Index, error) { // TODO: current and next index return values need to be implemented ch, err := f.asyncAt(ctx, at, after) diff --git a/pkg/file/joiner/joiner.go b/pkg/file/joiner/joiner.go index cc89f921397..cadd4d61604 100644 --- a/pkg/file/joiner/joiner.go +++ b/pkg/file/joiner/joiner.go @@ -339,8 +339,10 @@ func (j *joiner) subtrieSection(startIdx, payloadSize, parities int, subtrieSize return branchSize } -var errWhence = errors.New("seek: invalid whence") -var errOffset = errors.New("seek: invalid offset") +var ( + errWhence = errors.New("seek: invalid whence") + errOffset = errors.New("seek: invalid offset") +) func (j *joiner) Seek(offset int64, whence int) (int64, error) { switch whence { @@ -366,7 +368,6 @@ func (j *joiner) Seek(offset int64, whence int) (int64, error) { } j.off = offset return offset, nil - } func (j *joiner) IterateChunkAddresses(fn swarm.AddressIterFunc) error { diff --git a/pkg/file/loadsave/loadsave_test.go b/pkg/file/loadsave/loadsave_test.go index da268e15a87..a5828402d04 100644 --- a/pkg/file/loadsave/loadsave_test.go +++ b/pkg/file/loadsave/loadsave_test.go @@ -31,7 +31,6 @@ func TestLoadSave(t *testing.T) { store := inmemchunkstore.New() ls := loadsave.New(store, store, pipelineFn(store), redundancy.DefaultLevel) ref, err := ls.Save(context.Background(), data) - if err != nil { t.Fatal(err) } diff --git a/pkg/file/pipeline/bmt/bmt.go b/pkg/file/pipeline/bmt/bmt.go index 669b20b8252..a882697c554 100644 --- a/pkg/file/pipeline/bmt/bmt.go +++ b/pkg/file/pipeline/bmt/bmt.go @@ -12,9 +12,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var ( - errInvalidData = errors.New("bmt: invalid data") -) +var errInvalidData = errors.New("bmt: invalid data") type bmtWriter struct { next pipeline.ChainWriter diff --git a/pkg/file/pipeline/feeder/feeder.go b/pkg/file/pipeline/feeder/feeder.go index 6ba722796f2..c7c878e3f88 100644 --- a/pkg/file/pipeline/feeder/feeder.go +++ b/pkg/file/pipeline/feeder/feeder.go @@ -51,7 +51,7 @@ func (f *chunkFeeder) Write(b []byte) (int, error) { d := make([]byte, f.size+span) var sp int // span of current write - //copy from existing buffer to this one + // copy from existing buffer to this one sp = copy(d[span:], f.buffer[:f.bufferIdx]) // don't account what was already in the buffer when returning diff --git a/pkg/file/pipeline/mock/writer.go b/pkg/file/pipeline/mock/writer.go index 19f505123b2..d62140fe0fa 100644 --- a/pkg/file/pipeline/mock/writer.go +++ b/pkg/file/pipeline/mock/writer.go @@ -26,6 +26,7 @@ func (c *MockChainWriter) ChainWrite(_ *pipeline.PipeWriteArgs) error { c.chainWriteCalls++ return nil } + func (c *MockChainWriter) Sum() ([]byte, error) { c.Lock() defer c.Unlock() diff --git a/pkg/file/redundancy/getter/getter.go b/pkg/file/redundancy/getter/getter.go index 30ec23c00b3..8244142ad9b 100644 --- a/pkg/file/redundancy/getter/getter.go +++ b/pkg/file/redundancy/getter/getter.go @@ -101,7 +101,6 @@ func (g *decoder) Get(ctx context.Context, addr swarm.Address) (swarm.Chunk, err // If the fetch fails and waiting for the recovery is allowed, the function will wait // for either a good or bad recovery signal. func (g *decoder) fetch(ctx context.Context, i int, waitForRecovery bool) (err error) { - waitRecovery := func(err error) error { if !waitForRecovery { return err @@ -171,7 +170,6 @@ func (g *decoder) fetch(ctx context.Context, i int, waitForRecovery bool) (err e } func (g *decoder) prefetch() { - var err error defer func() { if err != nil { @@ -205,11 +203,9 @@ func (g *decoder) prefetch() { if err == nil && s > DATA { g.logger.Debug("successful recovery", "strategy", s) } - } func (g *decoder) runStrategy(s Strategy) error { - // across the different strategies, the common goal is to fetch at least as many chunks // as the number of data shards. // DATA strategy has a max error tolerance of zero. diff --git a/pkg/file/redundancy/level.go b/pkg/file/redundancy/level.go index c4eeacddc9f..8a3b30a82ff 100644 --- a/pkg/file/redundancy/level.go +++ b/pkg/file/redundancy/level.go @@ -114,6 +114,7 @@ var mediumEt = newErasureTable( []int{95, 69, 47, 29, 15, 6, 2, 1}, []int{9, 8, 7, 6, 5, 4, 3, 2}, ) + var encMediumEt = newErasureTable( []int{47, 34, 23, 14, 7, 3, 1}, []int{9, 8, 7, 6, 5, 4, 3}, @@ -123,6 +124,7 @@ var strongEt = newErasureTable( []int{105, 96, 87, 78, 70, 62, 54, 47, 40, 33, 27, 21, 16, 11, 7, 4, 2, 1}, []int{21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4}, ) + var encStrongEt = newErasureTable( []int{52, 48, 43, 39, 35, 31, 27, 23, 20, 16, 13, 10, 8, 5, 3, 2, 1}, []int{21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5}, @@ -132,6 +134,7 @@ var insaneEt = newErasureTable( []int{93, 88, 83, 78, 74, 69, 64, 60, 55, 51, 46, 42, 38, 34, 30, 27, 23, 20, 17, 14, 11, 9, 6, 4, 3, 2, 1}, []int{31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5}, ) + var encInsaneEt = newErasureTable( []int{46, 44, 41, 39, 37, 34, 32, 30, 27, 25, 23, 21, 19, 17, 15, 13, 11, 10, 8, 7, 5, 4, 3, 2, 1}, []int{31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 6}, @@ -147,6 +150,7 @@ var paranoidEt = newErasureTable( 56, 54, 52, 50, 48, 47, 45, 43, 40, 38, 36, 34, 31, 29, 26, 23, 19, }, ) + var encParanoidEt = newErasureTable( []int{ 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, diff --git a/pkg/file/redundancy/redundancy_test.go b/pkg/file/redundancy/redundancy_test.go index e6ad81a16a7..e5d35963687 100644 --- a/pkg/file/redundancy/redundancy_test.go +++ b/pkg/file/redundancy/redundancy_test.go @@ -67,6 +67,7 @@ func (c *ParityChainWriter) ChainWrite(args *pipeline.PipeWriteArgs) error { c.validCalls = append(c.validCalls, valid) return nil } + func (c *ParityChainWriter) Sum() ([]byte, error) { c.Lock() defer c.Unlock() diff --git a/pkg/file/utils.go b/pkg/file/utils.go index b0e3071f98d..0d304ba2141 100644 --- a/pkg/file/utils.go +++ b/pkg/file/utils.go @@ -12,9 +12,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var ( - zeroAddress = [32]byte{} -) +var zeroAddress = [32]byte{} // ChunkPayloadSize returns the effective byte length of an intermediate chunk // assumes data is always chunk size (without span) diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index 464681258e2..41e4f54ac2c 100644 --- a/pkg/gsoc/gsoc.go +++ b/pkg/gsoc/gsoc.go @@ -90,7 +90,7 @@ func (l *listener) Close() error { l.handlersMu.Lock() defer l.handlersMu.Unlock() - l.handlers = make(map[string][]*Handler) //unset handlers on shutdown + l.handlers = make(map[string][]*Handler) // unset handlers on shutdown return nil } diff --git a/pkg/hive/export_test.go b/pkg/hive/export_test.go index ec3ab70ec29..5b37a21d27d 100644 --- a/pkg/hive/export_test.go +++ b/pkg/hive/export_test.go @@ -4,5 +4,7 @@ package hive -var MaxBatchSize = maxBatchSize -var LimitBurst = limitBurst +var ( + MaxBatchSize = maxBatchSize + LimitBurst = limitBurst +) diff --git a/pkg/hive/hive_test.go b/pkg/hive/hive_test.go index dbad90dac04..fccba251429 100644 --- a/pkg/hive/hive_test.go +++ b/pkg/hive/hive_test.go @@ -112,6 +112,7 @@ func TestHandlerRateLimit(t *testing.T) { t.Fatal("want nil error") } } + func TestBroadcastPeers(t *testing.T) { t.Parallel() diff --git a/pkg/jsonhttp/jsonhttp.go b/pkg/jsonhttp/jsonhttp.go index e555223e110..0b200de42aa 100644 --- a/pkg/jsonhttp/jsonhttp.go +++ b/pkg/jsonhttp/jsonhttp.go @@ -65,9 +65,7 @@ func Respond(w http.ResponseWriter, statusCode int, response any) { Message: message.Error(), Code: statusCode, } - case interface { - String() string - }: + case interface{ String() string }: response = &StatusResponse{ Message: message.String(), Code: statusCode, diff --git a/pkg/keystore/file/service.go b/pkg/keystore/file/service.go index 01a5db72aeb..8198af18646 100644 --- a/pkg/keystore/file/service.go +++ b/pkg/keystore/file/service.go @@ -53,11 +53,11 @@ func (s *Service) SetKey(name, password string, edg keystore.EDG) (*ecdsa.Privat filename := s.keyFilename(name) - if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil { return nil, err } - if err := os.WriteFile(filename, d, 0600); err != nil { + if err := os.WriteFile(filename, d, 0o600); err != nil { return nil, err } diff --git a/pkg/keystore/mem/service.go b/pkg/keystore/mem/service.go index 46868957d1e..ecd7129a491 100644 --- a/pkg/keystore/mem/service.go +++ b/pkg/keystore/mem/service.go @@ -36,7 +36,6 @@ func (s *Service) Exists(name string) (bool, error) { defer s.mu.RUnlock() _, ok := s.m[name] return ok, nil - } func (s *Service) SetKey(name, password string, edg keystore.EDG) (*ecdsa.PrivateKey, error) { diff --git a/pkg/log/formatter_test.go b/pkg/log/formatter_test.go index 4425cafa185..d06e662b6e3 100644 --- a/pkg/log/formatter_test.go +++ b/pkg/log/formatter_test.go @@ -39,9 +39,11 @@ type marshalerTest struct{ val string } func (marshalerTest) MarshalLog() any { return struct{ Inner string }{"I am a log.Marshaler"} } + func (marshalerTest) String() string { return "String(): you should not see this" } + func (marshalerTest) Error() string { return "Error(): you should not see this" } @@ -61,6 +63,7 @@ type stringerTest struct{ val string } func (stringerTest) String() string { return "I am a fmt.Stringer" } + func (stringerTest) Error() string { return "Error(): you should not see this" } diff --git a/pkg/log/httpaccess/http_access.go b/pkg/log/httpaccess/http_access.go index fbc350dba60..b30803e78b3 100644 --- a/pkg/log/httpaccess/http_access.go +++ b/pkg/log/httpaccess/http_access.go @@ -92,12 +92,16 @@ type responseRecorder struct { http.ResponseWriter // Metrics. - status int - size int + status int + size int + hijacked bool } // Write implements http.ResponseWriter. func (rr *responseRecorder) Write(b []byte) (int, error) { + if rr.hijacked { + return 0, http.ErrHijacked + } size, err := rr.ResponseWriter.Write(b) rr.size += size return size, err @@ -105,6 +109,9 @@ func (rr *responseRecorder) Write(b []byte) (int, error) { // WriteHeader implements http.ResponseWriter. func (rr *responseRecorder) WriteHeader(s int) { + if rr.hijacked { + return + } rr.ResponseWriter.WriteHeader(s) if rr.status == 0 { rr.status = s @@ -118,13 +125,27 @@ func (rr *responseRecorder) CloseNotify() <-chan bool { return rr.ResponseWriter.(http.CloseNotifier).CloseNotify() } -// Hijack implements http.Hijacker. +// Hijack implements http.Hijacker so that WebSocket upgrades pass through +// correctly. Without this, the underlying connection is hijacked by the +// upgrader but the recorder still holds a reference to the (now-hijacked) +// ResponseWriter, causing "response.Write on hijacked connection" errors. func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return rr.ResponseWriter.(http.Hijacker).Hijack() + h, ok := rr.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, http.ErrNotSupported + } + conn, brw, err := h.Hijack() + if err == nil { + rr.hijacked = true + } + return conn, brw, err } // Flush implements http.Flusher. func (rr *responseRecorder) Flush() { + if rr.hijacked { + return + } rr.ResponseWriter.(http.Flusher).Flush() } diff --git a/pkg/manifest/mantaray/marshal.go b/pkg/manifest/mantaray/marshal.go index d95282135ad..8d510ceb57f 100644 --- a/pkg/manifest/mantaray/marshal.go +++ b/pkg/manifest/mantaray/marshal.go @@ -136,7 +136,7 @@ func (n *Node) MarshalBinary() (bytes []byte, err error) { indexBytes := make([]byte, 32) - var index = &bitsForBytes{} + index := &bitsForBytes{} for k := range n.forks { index.set(k) } diff --git a/pkg/manifest/mantaray/node.go b/pkg/manifest/mantaray/node.go index 865a89a29eb..461ac3b8a84 100644 --- a/pkg/manifest/mantaray/node.go +++ b/pkg/manifest/mantaray/node.go @@ -15,9 +15,7 @@ const ( PathSeparator = '/' // path separator ) -var ( - ZeroObfuscationKey = make([]byte, 32) -) +var ZeroObfuscationKey = make([]byte, 32) // Error used when lookup path does not match var ( diff --git a/pkg/manifest/mantaray/persist.go b/pkg/manifest/mantaray/persist.go index 259785d803c..b59e58fed70 100644 --- a/pkg/manifest/mantaray/persist.go +++ b/pkg/manifest/mantaray/persist.go @@ -7,6 +7,7 @@ package mantaray import ( "context" "errors" + "golang.org/x/sync/errgroup" ) diff --git a/pkg/manifest/mantaray/persist_test.go b/pkg/manifest/mantaray/persist_test.go index 619989d3803..0025a0d5f64 100644 --- a/pkg/manifest/mantaray/persist_test.go +++ b/pkg/manifest/mantaray/persist_test.go @@ -174,11 +174,13 @@ func TestPersistRemove(t *testing.T) { } } -type addr [32]byte -type mockLoadSaver struct { - mtx sync.Mutex - store map[addr][]byte -} +type ( + addr [32]byte + mockLoadSaver struct { + mtx sync.Mutex + store map[addr][]byte + } +) func newMockLoadSaver() *mockLoadSaver { return &mockLoadSaver{ diff --git a/pkg/node/chain.go b/pkg/node/chain.go index b33ec01d523..ef1a63c9881 100644 --- a/pkg/node/chain.go +++ b/pkg/node/chain.go @@ -63,6 +63,7 @@ func InitChain( minimumGasTipCap uint64, fallbackGasLimit uint64, rpcCfg BlockchainRPCConfig, + blockSyncInterval uint64, ) (transaction.Backend, common.Address, int64, transaction.Monitor, transaction.Service, error) { backend := backendnoop.New(chainID) @@ -97,7 +98,7 @@ func InitChain( logger.Info("connected to blockchain backend", "version", versionString) - backend = wrapped.NewBackend(ethclient.NewClient(rpcClient), minimumGasTipCap) + backend = wrapped.NewBackend(ethclient.NewClient(rpcClient), minimumGasTipCap, pollingInterval, blockSyncInterval) } backendChainID, err := backend.ChainID(ctx) diff --git a/pkg/node/metrics.go b/pkg/node/metrics.go index a161c39ffe7..3ec2f19d3bb 100644 --- a/pkg/node/metrics.go +++ b/pkg/node/metrics.go @@ -37,9 +37,11 @@ func newMetrics() nodeMetrics { Name: "full_sync_duration_minutes", Help: "Duration in minutes for node full sync to complete", // middle range should be more frequent - Buckets: []float64{80, 90, 100, 110, + Buckets: []float64{ + 80, 90, 100, 110, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, // 2-3 hours range - 190, 200, 210, 220, 230, 240}, + 190, 200, 210, 220, 230, 240, + }, }, ), } diff --git a/pkg/node/node.go b/pkg/node/node.go index 38107628149..72c88c0927c 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -48,6 +48,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/pricer" "github.com/ethersphere/bee/v2/pkg/pricing" "github.com/ethersphere/bee/v2/pkg/pss" + "github.com/ethersphere/bee/v2/pkg/pubsub" "github.com/ethersphere/bee/v2/pkg/puller" "github.com/ethersphere/bee/v2/pkg/pullsync" "github.com/ethersphere/bee/v2/pkg/pusher" @@ -141,6 +142,7 @@ type Options struct { BlockchainRpcKeepalive time.Duration BlockProfile bool BlockTime time.Duration + BlockSyncInterval uint64 BootnodeMode bool Bootnodes []string CacheCapacity uint64 @@ -192,6 +194,8 @@ type Options struct { WarmupTime time.Duration WelcomeMessage string WhitelistedWithdrawalAddress []string + PubsubBrokerMode bool + PubsubMaxConnections int } const ( @@ -424,6 +428,7 @@ func NewBee( IdleTimeout: o.BlockchainRpcIdleTimeout, Keepalive: o.BlockchainRpcKeepalive, }, + o.BlockSyncInterval, ) if err != nil { return nil, fmt.Errorf("init chain: %w", err) @@ -665,6 +670,7 @@ func NewBee( Nonce: nonce, ValidateOverlay: chainEnabled, Registry: registry, + PubsubReservedStreamSlots: o.PubsubMaxConnections, }) if err != nil { return nil, fmt.Errorf("p2p service: %w", err) @@ -737,6 +743,11 @@ func NewBee( return nil, fmt.Errorf("init batch service: %w", err) } + pubsubSvc := pubsub.New(p2ps, logger, o.PubsubBrokerMode, o.PubsubMaxConnections) + if err = p2ps.AddProtocol(pubsubSvc.Protocol()); err != nil { + return nil, fmt.Errorf("pubsub protocol: %w", err) + } + // Construct protocols. pingPong := pingpong.New(p2ps, logger, tracer) @@ -1132,7 +1143,6 @@ func NewBee( minStake := big.NewInt(0).Mul(big.NewInt(1< 0 && stake.Cmp(minStake) < 0 { logger.Warning("staked amount does not sufficiently cover the additional reserve capacity. On-chain height update will be skipped. Node will start, but storage incentives may not function for this capacity.", "missing_stake", new(big.Int).Sub(minStake, stake)) - } else { // make sure that the staking contract has the up to date height tx, updated, err := stakingContract.UpdateHeight(ctx) @@ -1266,6 +1276,7 @@ func NewBee( SyncStatus: syncStatusFn, NodeStatus: nodeStatus, PinIntegrity: localStore.PinIntegrity(), + PubsubService: pubsubSvc, } if o.APIAddr != "" { diff --git a/pkg/node/snapshot.go b/pkg/node/snapshot.go index ef190c0e26c..b235b31e11f 100644 --- a/pkg/node/snapshot.go +++ b/pkg/node/snapshot.go @@ -11,11 +11,10 @@ import ( "encoding/json" "fmt" "io" + "slices" "sort" "sync" - "slices" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/core/types" archive "github.com/ethersphere/batch-archive" diff --git a/pkg/node/snapshot_test.go b/pkg/node/snapshot_test.go index 2fbf0323ccd..abdb7b9e4e0 100644 --- a/pkg/node/snapshot_test.go +++ b/pkg/node/snapshot_test.go @@ -31,6 +31,7 @@ type mockSnapshotGetter struct { func newMockSnapshotGetter(data []byte) mockSnapshotGetter { return mockSnapshotGetter{data} } + func (m mockSnapshotGetter) GetBatchSnapshot() []byte { return m.data } diff --git a/pkg/node/statestore.go b/pkg/node/statestore.go index a526b924985..a2b21129ca9 100644 --- a/pkg/node/statestore.go +++ b/pkg/node/statestore.go @@ -66,7 +66,6 @@ const ( // checkOverlay checks the overlay is the same as stored in the statestore func checkOverlay(storer storage.StateStorer, overlay swarm.Address) error { - var storedOverlay swarm.Address err := storer.Get(noncedOverlayKey, &storedOverlay) if err != nil { diff --git a/pkg/p2p/libp2p/internal/handshake/handshake_test.go b/pkg/p2p/libp2p/internal/handshake/handshake_test.go index d455e6e8520..96de7493549 100644 --- a/pkg/p2p/libp2p/internal/handshake/handshake_test.go +++ b/pkg/p2p/libp2p/internal/handshake/handshake_test.go @@ -439,13 +439,11 @@ func TestHandshake(t *testing.T) { res, err := handshakeService.Handshake(context.Background(), stream1, node2mas) if !errors.Is(err, testError) { t.Fatalf("expected error %v got %v", testError, err) - } if res != nil { t.Fatal("expected nil res") } - }) t.Run("Handle - OK", func(t *testing.T) { diff --git a/pkg/p2p/libp2p/internal/handshake/mock/stream.go b/pkg/p2p/libp2p/internal/handshake/mock/stream.go index bebe17fe30a..a408827690e 100644 --- a/pkg/p2p/libp2p/internal/handshake/mock/stream.go +++ b/pkg/p2p/libp2p/internal/handshake/mock/stream.go @@ -24,6 +24,7 @@ type Stream struct { func NewStream(readBuffer, writeBuffer *bytes.Buffer) *Stream { return &Stream{readBuffer: readBuffer, writeBuffer: writeBuffer} } + func (s *Stream) SetReadErr(err error, checkmark int) { s.readError = err s.readErrCheckmark = checkmark diff --git a/pkg/p2p/libp2p/internal/reacher/reacher_test.go b/pkg/p2p/libp2p/internal/reacher/reacher_test.go index dd590e2a70d..db49df6b447 100644 --- a/pkg/p2p/libp2p/internal/reacher/reacher_test.go +++ b/pkg/p2p/libp2p/internal/reacher/reacher_test.go @@ -9,9 +9,8 @@ import ( "errors" "sync" "testing" - "time" - "testing/synctest" + "time" "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/p2p" diff --git a/pkg/p2p/libp2p/libp2p.go b/pkg/p2p/libp2p/libp2p.go index 316cbd6171f..2c4d672c8a9 100644 --- a/pkg/p2p/libp2p/libp2p.go +++ b/pkg/p2p/libp2p/libp2p.go @@ -149,6 +149,7 @@ type Options struct { HeadersRWTimeout time.Duration Registry *prometheus.Registry autoTLSCertManager autoTLSCertManager + PubsubReservedStreamSlots int } func New(ctx context.Context, signer beecrypto.Signer, networkID uint64, overlay swarm.Address, addr string, ab addressbook.Putter, storer storage.StateStorer, lightNodes *lightnode.Container, logger log.Logger, tracer *tracing.Tracer, o Options) (s *Service, returnErr error) { @@ -209,11 +210,16 @@ func New(ctx context.Context, signer beecrypto.Signer, networkID uint64, overlay } // Tweak certain settings + inboundLimit := rcmgr.LimitVal(IncomingStreamCountLimit - o.PubsubReservedStreamSlots) + if inboundLimit < 0 { + inboundLimit = 0 + } + cfg := rcmgr.PartialLimitConfig{ System: rcmgr.ResourceLimits{ - Streams: IncomingStreamCountLimit + OutgoingStreamCountLimit, + Streams: inboundLimit + OutgoingStreamCountLimit, StreamsOutbound: OutgoingStreamCountLimit, - StreamsInbound: IncomingStreamCountLimit, + StreamsInbound: inboundLimit, }, } @@ -587,7 +593,7 @@ func (s *Service) handleIncoming(stream network.Stream) { peerAddrs, err := s.peerMultiaddrs(s.ctx, peerID) if err != nil { s.logger.Debug("stream handler: handshake: build remote multiaddrs", "peer_id", peerID, "error", err) - s.logger.Error(nil, "stream handler: handshake: build remote multiaddrs", "peer_id", peerID) + s.logger.Error(err, "stream handler: handshake: build remote multiaddrs", "peer_id", peerID) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(peerID) return @@ -602,7 +608,7 @@ func (s *Service) handleIncoming(stream network.Stream) { observedAddrs, err = buildFullMAs([]ma.Multiaddr{stream.Conn().RemoteMultiaddr()}, peerID) if err != nil { s.logger.Debug("stream handler: handshake: build remote multiaddrs fallback", "peer_id", peerID, "error", err) - s.logger.Error(nil, "stream handler: handshake: build remote multiaddrs fallback", "peer_id", peerID) + s.logger.Error(err, "stream handler: handshake: build remote multiaddrs fallback", "peer_id", peerID) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(peerID) return @@ -619,7 +625,7 @@ func (s *Service) handleIncoming(stream network.Stream) { ) if err != nil { s.logger.Debug("stream handler: handshake: handle failed", "peer_id", peerID, "error", err) - s.logger.Error(nil, "stream handler: handshake: handle failed", "peer_id", peerID) + s.logger.Error(err, "stream handler: handshake: handle failed", "peer_id", peerID) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(peerID) return @@ -630,14 +636,14 @@ func (s *Service) handleIncoming(stream network.Stream) { blocked, err := s.blocklist.Exists(overlay) if err != nil { s.logger.Debug("stream handler: blocklisting: exists failed", "peer_address", overlay, "error", err) - s.logger.Error(nil, "stream handler: internal error while connecting with peer", "peer_address", overlay) + s.logger.Error(err, "stream handler: internal error while connecting with peer", "peer_address", overlay) _ = handshakeStream.Reset() _ = stream.Conn().Close() return } if blocked { - s.logger.Error(nil, "stream handler: blocked connection from blocklisted peer", "peer_address", overlay) + s.logger.Error(err, "stream handler: blocked connection from blocklisted peer", "peer_address", overlay) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(peerID) return @@ -647,7 +653,7 @@ func (s *Service) handleIncoming(stream network.Stream) { s.logger.Debug("stream handler: peer already exists", "peer_address", overlay) if err = handshakeStream.FullClose(); err != nil { s.logger.Debug("stream handler: could not close stream", "peer_address", overlay, "error", err) - s.logger.Error(nil, "stream handler: unable to handshake with peer", "peer_address", overlay) + s.logger.Error(err, "stream handler: unable to handshake with peer", "peer_address", overlay) _ = stream.Conn().Close() } return @@ -655,7 +661,7 @@ func (s *Service) handleIncoming(stream network.Stream) { if err = handshakeStream.FullClose(); err != nil { s.logger.Debug("stream handler: could not close stream", "peer_address", overlay, "error", err) - s.logger.Error(nil, "stream handler: unable to handshake with peer", "peer_address", overlay) + s.logger.Error(err, "stream handler: unable to handshake with peer", "peer_address", overlay) _ = s.Disconnect(overlay, "could not fully close stream on handshake") return } @@ -665,7 +671,7 @@ func (s *Service) handleIncoming(stream network.Stream) { err = s.addressbook.Put(i.BzzAddress.Overlay, *i.BzzAddress) if err != nil { s.logger.Debug("stream handler: addressbook put error", "peer_id", peerID, "error", err) - s.logger.Error(nil, "stream handler: unable to persist peer", "peer_id", peerID) + s.logger.Error(err, "stream handler: unable to persist peer", "peer_id", peerID) _ = s.Disconnect(i.BzzAddress.Overlay, "unable to persist peer in addressbook") return } @@ -868,7 +874,7 @@ func (s *Service) AddProtocol(p p2p.ProtocolSpec) (err error) { _ = stream.Reset() if err := s.Blocklist(overlay, bpe.Duration(), bpe.Error()); err != nil { logger.Debug("blocklist: could not blocklist peer", "peer_id", peerID, "error", err) - logger.Error(nil, "unable to blocklist peer", "peer_id", peerID) + logger.Error(err, "unable to blocklist peer", "peer_id", peerID) } loggerV1.Debug("handler: peer blocklisted", "protocol", p.Name, "peer_address", overlay) } @@ -959,7 +965,18 @@ func buildHostAddress(peerID libp2ppeer.ID) (ma.Multiaddr, error) { return ma.NewMultiaddr(fmt.Sprintf("/p2p/%s", peerID.String())) } -func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *bzz.Address, err error) { +func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (*bzz.Address, error) { + return s.connect(ctx, addrs, false) +} + +// ConnectAllowLight behaves like Connect but does not reject the peer +// if it identifies itself as a light node. Intended for protocols such +// as pubsub where broker peers may operate in light-node mode. +func (s *Service) ConnectAllowLight(ctx context.Context, addrs []ma.Multiaddr) (*bzz.Address, error) { + return s.connect(ctx, addrs, true) +} + +func (s *Service) connect(ctx context.Context, addrs []ma.Multiaddr, allowLight bool) (address *bzz.Address, err error) { loggerV1 := s.logger.V(1).Register() defer func() { @@ -1011,6 +1028,13 @@ func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *b } connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + // Clear any stale libp2p swarm dial backoff for this peer so that + // an explicit Connect call always attempts a real TCP dial rather + // than failing immediately with a cached backoff error (which would + // still count against the Bee connection breaker). + if sw, ok := s.host.Network().(*lp2pswarm.Swarm); ok { + sw.Backoff().Clear(info.ID) + } err = s.connectionBreaker.Execute(func() error { return s.host.Connect(connectCtx, *info) }) cancel() @@ -1086,7 +1110,7 @@ func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *b return nil, fmt.Errorf("handshake: %w", err) } - if !i.FullNode { + if !i.FullNode && !allowLight { _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(info.ID) return nil, p2p.ErrDialLightNode @@ -1097,14 +1121,14 @@ func (s *Service) Connect(ctx context.Context, addrs []ma.Multiaddr) (address *b blocked, err := s.blocklist.Exists(overlay) if err != nil { s.logger.Debug("blocklisting: exists failed", "peer_id", info.ID, "error", err) - s.logger.Error(nil, "internal error while connecting with peer", "peer_id", info.ID) + s.logger.Error(err, "internal error while connecting with peer", "peer_id", info.ID) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(info.ID) return nil, err } if blocked { - s.logger.Error(nil, "blocked connection to blocklisted peer", "peer_id", info.ID) + s.logger.Error(err, "blocked connection to blocklisted peer", "peer_id", info.ID) _ = handshakeStream.Reset() _ = s.host.Network().ClosePeer(info.ID) return nil, p2p.ErrPeerBlocklisted diff --git a/pkg/p2p/libp2p/libp2p_test.go b/pkg/p2p/libp2p/libp2p_test.go index ede9cbbcb79..873d074f1e5 100644 --- a/pkg/p2p/libp2p/libp2p_test.go +++ b/pkg/p2p/libp2p/libp2p_test.go @@ -136,7 +136,6 @@ func expectPeersEventually(t *testing.T, s *libp2p.Service, addrs ...swarm.Addre err := spinlock.Wait(5*time.Second, func() bool { peers = s.Peers() return len(peers) == len(addrs) - }) if err != nil { t.Fatalf("timed out waiting for peers, got %v, want %v", len(peers), len(addrs)) diff --git a/pkg/p2p/libp2p/mock/mock_certmagic.go b/pkg/p2p/libp2p/mock/mock_certmagic.go index 8337f26e8ab..7263438a453 100644 --- a/pkg/p2p/libp2p/mock/mock_certmagic.go +++ b/pkg/p2p/libp2p/mock/mock_certmagic.go @@ -120,6 +120,7 @@ func (m *MockP2PForgeCertMgr) Start() error { } return nil } + func (m *MockP2PForgeCertMgr) Stop() { m.mu.Lock() defer m.mu.Unlock() diff --git a/pkg/p2p/libp2p/peer.go b/pkg/p2p/libp2p/peer.go index 232fbf9b103..c5c90a102df 100644 --- a/pkg/p2p/libp2p/peer.go +++ b/pkg/p2p/libp2p/peer.go @@ -86,7 +86,6 @@ func (r *peerRegistry) Disconnected(_ network.Network, c network.Conn) { delete(r.bee260Compatibility, peerID) r.mu.Unlock() r.disconnecter.disconnected(overlay) - } func (r *peerRegistry) addStream(peerID libp2ppeer.ID, stream network.Stream, cancel context.CancelFunc) { @@ -155,7 +154,6 @@ func (r *peerRegistry) addIfNotExists(c network.Conn, overlay swarm.Address, ful r.overlays[peerID] = overlay r.full[peerID] = full return false - } func (r *peerRegistry) peerID(overlay swarm.Address) (peerID libp2ppeer.ID, found bool) { diff --git a/pkg/p2p/libp2p/protocols_test.go b/pkg/p2p/libp2p/protocols_test.go index 0bec5ba49af..8b98bc54880 100644 --- a/pkg/p2p/libp2p/protocols_test.go +++ b/pkg/p2p/libp2p/protocols_test.go @@ -388,7 +388,6 @@ func TestConnectDisconnectEvents(t *testing.T) { expectCounter(t, &coutCount, 0, &countMU) expectCounter(t, &dinCount, 1, &countMU) expectCounter(t, &doutCount, 1, &countMU) - } func TestPing(t *testing.T) { diff --git a/pkg/p2p/libp2p/stream.go b/pkg/p2p/libp2p/stream.go index 93e7ce6181a..0a3efa0f209 100644 --- a/pkg/p2p/libp2p/stream.go +++ b/pkg/p2p/libp2p/stream.go @@ -29,6 +29,7 @@ type stream struct { func newStream(s network.Stream, metrics metrics) *stream { return &stream{Stream: s, metrics: metrics} } + func (s *stream) Headers() p2p.Headers { return s.headers } diff --git a/pkg/p2p/libp2p/tracing_test.go b/pkg/p2p/libp2p/tracing_test.go index 533a49088ec..66ebd22aa29 100644 --- a/pkg/p2p/libp2p/tracing_test.go +++ b/pkg/p2p/libp2p/tracing_test.go @@ -45,7 +45,6 @@ func TestTracing(t *testing.T) { var handledTracingSpan string handled := make(chan struct{}) if err := s1.AddProtocol(newTestProtocol(func(ctx context.Context, _ p2p.Peer, _ p2p.Stream) error { - span, _, _ := tracer1.StartSpanFromContext(ctx, "test-p2p-handler", nil) defer span.Finish() diff --git a/pkg/p2p/libp2p/welcome_message_test.go b/pkg/p2p/libp2p/welcome_message_test.go index 5e6ac7a2f2d..866a404e4ae 100644 --- a/pkg/p2p/libp2p/welcome_message_test.go +++ b/pkg/p2p/libp2p/welcome_message_test.go @@ -54,6 +54,5 @@ func TestDynamicWelcomeMessage(t *testing.T) { t.Fatalf("wrong error: want %v, got %v", want, got) } }) - }) } diff --git a/pkg/p2p/streamtest/streamtest_test.go b/pkg/p2p/streamtest/streamtest_test.go index 51b0d903ef2..91b80ae5b69 100644 --- a/pkg/p2p/streamtest/streamtest_test.go +++ b/pkg/p2p/streamtest/streamtest_test.go @@ -24,7 +24,7 @@ import ( func TestRecorder(t *testing.T) { t.Parallel() - var answers = map[string]string{ + answers := map[string]string{ "What is your name?": "Sir Lancelot of Camelot", "What is your quest?": "To seek the Holy Grail.", "What is your favorite color?": "Blue.", diff --git a/pkg/postage/batchservice/batchservice.go b/pkg/postage/batchservice/batchservice.go index cf3382c552d..1726b1c88dd 100644 --- a/pkg/postage/batchservice/batchservice.go +++ b/pkg/postage/batchservice/batchservice.go @@ -245,10 +245,12 @@ func (svc *batchService) UpdateBlockNumber(blockNumber uint64) error { svc.logger.Debug("block height updated", "new_block", blockNumber) return nil } + func (svc *batchService) TransactionStart() error { svc.pendingChainState = svc.storer.GetChainState() return svc.stateStore.Put(dirtyDBKey, true) } + func (svc *batchService) TransactionEnd() error { if svc.pendingChainState != nil { if err := svc.storer.PutChainState(svc.pendingChainState); err != nil { diff --git a/pkg/postage/batchservice/batchservice_test.go b/pkg/postage/batchservice/batchservice_test.go index 2a9ef622c10..57a7e5aef82 100644 --- a/pkg/postage/batchservice/batchservice_test.go +++ b/pkg/postage/batchservice/batchservice_test.go @@ -29,8 +29,7 @@ var ( testTxHash = common.BytesToHash(make([]byte, 32)) ) -type mockListener struct { -} +type mockListener struct{} func (*mockListener) Listen(ctx context.Context, from uint64, updater postage.EventUpdater) <-chan error { c := make(chan error, 1) @@ -238,7 +237,6 @@ func TestBatchServiceCreate(t *testing.T) { validateNoBatch(t, testBatch, batchStore) }) - } func TestBatchServiceTopUp(t *testing.T) { @@ -484,6 +482,7 @@ func TestBatchServiceUpdatePrice(t *testing.T) { } }) } + func TestBatchServiceUpdateBlockNumber(t *testing.T) { t.Parallel() diff --git a/pkg/postage/batchstore/store.go b/pkg/postage/batchstore/store.go index 3c665feaa64..4b6b994655e 100644 --- a/pkg/postage/batchstore/store.go +++ b/pkg/postage/batchstore/store.go @@ -30,8 +30,10 @@ const ( ) // ErrNotFound signals that the element was not found. -var ErrNotFound = errors.New("batchstore: not found") -var ErrStorageRadiusExceeds = errors.New("batchstore: storage radius must not exceed reserve radius") +var ( + ErrNotFound = errors.New("batchstore: not found") + ErrStorageRadiusExceeds = errors.New("batchstore: storage radius must not exceed reserve radius") +) type evictFn func(batchID []byte) error @@ -105,7 +107,6 @@ func (s *store) Get(id []byte) (*postage.Batch, error) { } func (s *store) get(id []byte) (*postage.Batch, error) { - b := &postage.Batch{} err := s.store.Get(batchKey(id), b) if err != nil { @@ -233,7 +234,6 @@ func (s *store) Commitment() (uint64, error) { var totalCommitment int err := s.store.Iterate(batchKeyPrefix, func(key, value []byte) (bool, error) { - b := &postage.Batch{} if err := b.UnmarshalBinary(value); err != nil { return false, err @@ -276,7 +276,6 @@ func (s *store) Reset() error { // expired batches, and computing a new radius. // Must be called under lock. func (s *store) saveBatch(b *postage.Batch) error { - if err := s.store.Put(valueKey(b.Value, b.ID), nil); err != nil { return fmt.Errorf("batchstore: allocate batch %x: %w", b.ID, err) } @@ -297,11 +296,9 @@ func (s *store) saveBatch(b *postage.Batch) error { // cleanup evicts and removes expired batch. // Must be called under lock. func (s *store) cleanup() error { - var evictions []*postage.Batch err := s.store.Iterate(valueKeyPrefix, func(key, value []byte) (stop bool, err error) { - b, err := s.get(valueKeyToID(key)) if err != nil { return false, err @@ -349,11 +346,9 @@ func (s *store) cleanup() error { // and the node capacity using the formula totalCommitment/node_capacity = 2^R. // Must be called under lock. func (s *store) computeRadius() error { - var totalCommitment int err := s.store.Iterate(batchKeyPrefix, func(key, value []byte) (bool, error) { - b := &postage.Batch{} if err := b.UnmarshalBinary(value); err != nil { return false, err diff --git a/pkg/postage/batchstore/store_test.go b/pkg/postage/batchstore/store_test.go index 975c93f3e18..d50beff47cb 100644 --- a/pkg/postage/batchstore/store_test.go +++ b/pkg/postage/batchstore/store_test.go @@ -78,7 +78,7 @@ func TestBatchStore_IterateStopsEarly(t *testing.T) { stateStorePut(t, stateStore, key1, testBatch1) stateStorePut(t, stateStore, key2, testBatch2) - var iterations = 0 + iterations := 0 err := batchStore.Iterate(func(b *postage.Batch) (bool, error) { iterations++ return false, nil @@ -127,7 +127,7 @@ func TestBatchStore_SaveAndUpdate(t *testing.T) { t.Fatalf("storer.Save(...): unexpected error: %v", err) } - //get test batch after save call + // get test batch after save call stateStoreGet(t, stateStore, key, testBatch) var have postage.Batch diff --git a/pkg/postage/listener/listener.go b/pkg/postage/listener/listener.go index 53feb3a5299..349b6a05a14 100644 --- a/pkg/postage/listener/listener.go +++ b/pkg/postage/listener/listener.go @@ -34,10 +34,8 @@ const ( defaultBatchFactor = uint64(5) // minimal number of blocks to sync at once ) -var ( - // for testing, set externally - batchFactorOverridePublic = "5" -) +// for testing, set externally +var batchFactorOverridePublic = "5" var ( ErrPostageSyncingStalled = errors.New("postage syncing stalled") diff --git a/pkg/postage/postagecontract/contract.go b/pkg/postage/postagecontract/contract.go index 4d0d38b3505..cae599db166 100644 --- a/pkg/postage/postagecontract/contract.go +++ b/pkg/postage/postagecontract/contract.go @@ -233,7 +233,6 @@ func (c *postageContract) sendTransaction(ctx context.Context, callData []byte, } func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner common.Address, initialBalance *big.Int, depth uint8, nonce common.Hash, immutable bool) (*types.Receipt, error) { - callData, err := c.postageStampContractABI.Pack("createBatch", owner, initialBalance, depth, BucketDepth, nonce, immutable) if err != nil { return nil, err @@ -248,7 +247,6 @@ func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner } func (c *postageContract) sendTopUpBatchTransaction(ctx context.Context, batchID []byte, topUpAmount *big.Int) (*types.Receipt, error) { - callData, err := c.postageStampContractABI.Pack("topUp", common.BytesToHash(batchID), topUpAmount) if err != nil { return nil, err @@ -263,7 +261,6 @@ func (c *postageContract) sendTopUpBatchTransaction(ctx context.Context, batchID } func (c *postageContract) sendDiluteTransaction(ctx context.Context, batchID []byte, newDepth uint8) (*types.Receipt, error) { - callData, err := c.postageStampContractABI.Pack("increaseDepth", common.BytesToHash(batchID), newDepth) if err != nil { return nil, err @@ -390,7 +387,6 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I if ev.Address == c.postageStampContractAddress && len(ev.Topics) > 0 && ev.Topics[0] == c.batchCreatedTopic { var createdEvent batchCreatedEvent err = transaction.ParseEvent(&c.postageStampContractABI, "BatchCreated", &createdEvent, *ev) - if err != nil { return } @@ -406,7 +402,6 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I ev.BlockNumber, createdEvent.ImmutableFlag, )) - if err != nil { return } @@ -419,7 +414,6 @@ func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.I } func (c *postageContract) TopUpBatch(ctx context.Context, batchID []byte, topupBalance *big.Int) (txHash common.Hash, err error) { - batch, err := c.postageStorer.Get(batchID) if err != nil { return @@ -458,7 +452,6 @@ func (c *postageContract) TopUpBatch(ctx context.Context, batchID []byte, topupB } func (c *postageContract) DiluteBatch(ctx context.Context, batchID []byte, newDepth uint8) (txHash common.Hash, err error) { - batch, err := c.postageStorer.Get(batchID) if err != nil { return @@ -530,9 +523,11 @@ type noOpPostageContract struct{} func (m *noOpPostageContract) CreateBatch(context.Context, *big.Int, uint8, bool, string) (common.Hash, []byte, error) { return common.Hash{}, nil, nil } + func (m *noOpPostageContract) TopUpBatch(context.Context, []byte, *big.Int) (common.Hash, error) { return common.Hash{}, ErrChainDisabled } + func (m *noOpPostageContract) DiluteBatch(context.Context, []byte, uint8) (common.Hash, error) { return common.Hash{}, ErrChainDisabled } diff --git a/pkg/postage/postagecontract/contract_test.go b/pkg/postage/postagecontract/contract_test.go index 680eb7d773f..92020efbe9a 100644 --- a/pkg/postage/postagecontract/contract_test.go +++ b/pkg/postage/postagecontract/contract_test.go @@ -40,7 +40,6 @@ func TestCreateBatch(t *testing.T) { initialBalance := big.NewInt(100) t.Run("ok", func(t *testing.T) { - depth := uint8(10) totalAmount := big.NewInt(102400) txHashApprove := common.HexToHash("abb0") @@ -257,7 +256,6 @@ func TestCreateBatch(t *testing.T) { t.Fatalf("expected error %v. got %v", postagecontract.ErrInsufficientValidity, err) } }) - } func newCreateEvent(postageContractAddress common.Address, batchId common.Hash) *types.Log { @@ -292,7 +290,6 @@ func TestTopUpBatch(t *testing.T) { topupBalance := big.NewInt(100) t.Run("ok", func(t *testing.T) { - totalAmount := big.NewInt(102400) txHashApprove := common.HexToHash("abb0") txHashTopup := common.HexToHash("c3a7") @@ -457,7 +454,6 @@ func TestDiluteBatch(t *testing.T) { ctx := context.Background() t.Run("ok", func(t *testing.T) { - txHashDilute := common.HexToHash("c3a7") batch := postagetesting.MustNewBatch(postagetesting.WithOwner(owner.Bytes())) batch.Depth = uint8(10) @@ -685,7 +681,6 @@ func TestBatchExpirer(t *testing.T) { if err != nil { t.Fatal(err) } - }) t.Run("wrong call data for expired batches exist", func(t *testing.T) { @@ -719,7 +714,6 @@ func TestBatchExpirer(t *testing.T) { if err == nil { t.Fatal("expected error") } - }) t.Run("wrong call data for expired limited batches", func(t *testing.T) { diff --git a/pkg/postage/service.go b/pkg/postage/service.go index 12404ef3006..e6991bdcfa3 100644 --- a/pkg/postage/service.go +++ b/pkg/postage/service.go @@ -278,7 +278,6 @@ func (ps *service) Close() error { // HandleStampExpiry handles stamp expiry for a given id. func (ps *service) HandleStampExpiry(ctx context.Context, id []byte) error { - exists, err := ps.removeIssuer(id) if err != nil { return err @@ -293,7 +292,6 @@ func (ps *service) HandleStampExpiry(ctx context.Context, id []byte) error { // removeStampItems func (ps *service) removeStampItems(ctx context.Context, batchID []byte) error { - ps.logger.Debug("removing expired stamp items", "batchID", hex.EncodeToString(batchID)) deleteItemC := make(chan *StampItem) diff --git a/pkg/postage/stamper.go b/pkg/postage/stamper.go index bd9ce86a390..342aa226b21 100644 --- a/pkg/postage/stamper.go +++ b/pkg/postage/stamper.go @@ -14,10 +14,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var ( - // ErrBucketFull is the error when a collision bucket is full. - ErrBucketFull = errors.New("bucket full") -) +// ErrBucketFull is the error when a collision bucket is full. +var ErrBucketFull = errors.New("bucket full") // Stamper can issue stamps from the given address of chunk. type Stamper interface { diff --git a/pkg/pricer/headerutils/utilities.go b/pkg/pricer/headerutils/utilities.go index a85e56990b4..93616909f41 100644 --- a/pkg/pricer/headerutils/utilities.go +++ b/pkg/pricer/headerutils/utilities.go @@ -7,6 +7,7 @@ package headerutils import ( "encoding/binary" "errors" + "github.com/ethersphere/bee/v2/pkg/p2p" "github.com/ethersphere/bee/v2/pkg/swarm" ) @@ -31,7 +32,6 @@ var ( // Headers, utility functions func MakePricingHeaders(chunkPrice uint64, addr swarm.Address) (p2p.Headers, error) { - chunkPriceInBytes := make([]byte, 8) binary.BigEndian.PutUint64(chunkPriceInBytes, chunkPrice) @@ -45,7 +45,6 @@ func MakePricingHeaders(chunkPrice uint64, addr swarm.Address) (p2p.Headers, err } func MakePricingResponseHeaders(chunkPrice uint64, addr swarm.Address, index uint8) (p2p.Headers, error) { - chunkPriceInBytes := make([]byte, 8) chunkIndexInBytes := make([]byte, 1) @@ -64,7 +63,6 @@ func MakePricingResponseHeaders(chunkPrice uint64, addr swarm.Address, index uin // ParsePricingHeaders used by responder to read address and price from stream headers // Returns an error if no target field attached or the contents of it are not readable func ParsePricingHeaders(receivedHeaders p2p.Headers) (swarm.Address, uint64, error) { - target, err := ParseTargetHeader(receivedHeaders) if err != nil { return swarm.ZeroAddress, 0, err diff --git a/pkg/pricer/headerutils/utilities_test.go b/pkg/pricer/headerutils/utilities_test.go index b2c62ee68d1..434967c506d 100644 --- a/pkg/pricer/headerutils/utilities_test.go +++ b/pkg/pricer/headerutils/utilities_test.go @@ -21,7 +21,6 @@ func TestMakePricingHeaders(t *testing.T) { makeHeaders, err := headerutils.MakePricingHeaders(uint64(5348), addr) if err != nil { t.Fatal(err) - } expectedHeaders := p2p.Headers{ @@ -32,7 +31,6 @@ func TestMakePricingHeaders(t *testing.T) { if !reflect.DeepEqual(makeHeaders, expectedHeaders) { t.Fatalf("Made headers not as expected, got %+v, want %+v", makeHeaders, expectedHeaders) } - } func TestMakePricingResponseHeaders(t *testing.T) { @@ -54,7 +52,6 @@ func TestMakePricingResponseHeaders(t *testing.T) { if !reflect.DeepEqual(makeHeaders, expectedHeaders) { t.Fatalf("Made headers not as expected, got %+v, want %+v", makeHeaders, expectedHeaders) } - } func TestParsePricingHeaders(t *testing.T) { @@ -125,7 +122,6 @@ func TestParseIndexHeader(t *testing.T) { if parsedIndex != uint8(11) { t.Fatalf("Index mismatch, got %v, want %v", parsedIndex, 11) } - } func TestParseTargetHeader(t *testing.T) { @@ -145,7 +141,6 @@ func TestParseTargetHeader(t *testing.T) { if !parsedTarget.Equal(addr) { t.Fatalf("Target mismatch, got %v, want %v", parsedTarget, addr) } - } func TestParsePriceHeader(t *testing.T) { @@ -163,7 +158,6 @@ func TestParsePriceHeader(t *testing.T) { if parsedPrice != uint64(5348) { t.Fatalf("Index mismatch, got %v, want %v", parsedPrice, 5348) } - } func TestReadMalformedHeaders(t *testing.T) { @@ -196,5 +190,4 @@ func TestReadMalformedHeaders(t *testing.T) { if err == nil { t.Fatal("Expected error caused by bad length of fields") } - } diff --git a/pkg/pricing/pricing.go b/pkg/pricing/pricing.go index 32ec1515936..9666f392840 100644 --- a/pkg/pricing/pricing.go +++ b/pkg/pricing/pricing.go @@ -27,10 +27,8 @@ const ( streamName = "pricing" ) -var ( - // ErrThresholdTooLow says that the proposed payment threshold is too low for even a single reserve. - ErrThresholdTooLow = errors.New("threshold too low") -) +// ErrThresholdTooLow says that the proposed payment threshold is too low for even a single reserve. +var ErrThresholdTooLow = errors.New("threshold too low") var _ Interface = (*Service)(nil) @@ -111,7 +109,6 @@ func (s *Service) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) (e } func (s *Service) init(ctx context.Context, p p2p.Peer) error { - threshold := s.paymentThreshold if !p.FullNode { threshold = s.lightPaymentThreshold diff --git a/pkg/pss/export_test.go b/pkg/pss/export_test.go index c1be883aa35..3fc0ed15b4e 100644 --- a/pkg/pss/export_test.go +++ b/pkg/pss/export_test.go @@ -4,6 +4,4 @@ package pss -var ( - Contains = contains -) +var Contains = contains diff --git a/pkg/pss/mining_test.go b/pkg/pss/mining_test.go index 94e469bf6ca..1e368f56045 100644 --- a/pkg/pss/mining_test.go +++ b/pkg/pss/mining_test.go @@ -58,5 +58,4 @@ func BenchmarkWrap(b *testing.B) { } }) } - } diff --git a/pkg/pss/pss.go b/pkg/pss/pss.go index 28319e9a615..7202b37e792 100644 --- a/pkg/pss/pss.go +++ b/pkg/pss/pss.go @@ -74,7 +74,7 @@ func (ps *pss) Close() error { ps.handlersMu.Lock() defer ps.handlersMu.Unlock() - ps.handlers = make(map[Topic][]*Handler) //unset handlers on shutdown + ps.handlers = make(map[Topic][]*Handler) // unset handlers on shutdown return nil } diff --git a/pkg/pubsub/mode.go b/pkg/pubsub/mode.go new file mode 100644 index 00000000000..05c43b5ca31 --- /dev/null +++ b/pkg/pubsub/mode.go @@ -0,0 +1,432 @@ +// 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 pubsub + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "sync" + + "github.com/ethersphere/bee/v2/pkg/log" + "github.com/ethersphere/bee/v2/pkg/p2p" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +var ErrInvalidSignature = errors.New("pubsub: invalid SOC signature") + +const ( + // P2P headers + HeaderGsocOwner = "pubsub-gsoc-owner" + HeaderGsocID = "pubsub-gsoc-id" +) + +// ModeID identifies a pubsub mode. +type ModeID uint8 + +// Mode defines mode-specific behavior for the pubsub protocol. +// Each mode determines its own roles, wire format, and message handling. +type Mode interface { + ID() ModeID + TopicAddress() swarm.Address + + // Subscriber side - outbound connection to broker + Connect(ctx context.Context, p p2p.Streamer, overlay swarm.Address, opts ConnectOptions) (p2p.Stream, error) + CreateSubscriberConn(stream p2p.Stream, overlay swarm.Address) *SubscriberConn + GetSubscriberConn() *SubscriberConn + RemoveSubscriberConn(conn *SubscriberConn) + ReadBrokerMessage(stream p2p.Stream) ([]byte, error) + + // Broker side - handles incoming streams (publisher and subscriber) + HandleBroker(ctx context.Context, peer p2p.Peer, stream p2p.Stream, headers p2p.Headers) error + SubscriberCount() int + SubscriberOverlays() []string +} + +// --- GSOC Ephemeral Mode (mode 1) --- + +const ( + // Mode-specific message types (Broker → Subscriber); 0x01 is reserved for service-level ping. + MsgTypeHandshake byte = 0x02 + MsgTypeData byte = 0x03 +) + +// GSOCEphemeralMode implements Mode for GSOC ephemeral messaging. +type GSOCEphemeralMode struct { + mu sync.RWMutex + topicAddress swarm.Address + gsocOwner []byte + gsocID []byte + logger log.Logger + subscribers map[string]*brokerSubscriber + subscriberConn *SubscriberConn +} + +var _ Mode = (*GSOCEphemeralMode)(nil) + +func NewGSOCEphemeralMode(topicAddress []byte, logger log.Logger) *GSOCEphemeralMode { + return &GSOCEphemeralMode{ + topicAddress: swarm.NewAddress(topicAddress), + logger: logger, + subscribers: make(map[string]*brokerSubscriber), + } +} + +func (m *GSOCEphemeralMode) ID() ModeID { return ModeGSOCEphemeral } + +func (m *GSOCEphemeralMode) TopicAddress() swarm.Address { return m.topicAddress.Clone() } + +func (m *GSOCEphemeralMode) Connect(ctx context.Context, p p2p.Streamer, overlay swarm.Address, opts ConnectOptions) (p2p.Stream, error) { + var rw byte + if opts.ReadWrite { + rw = 1 + } + headers := p2p.Headers{ + HeaderTopicAddress: m.topicAddress.Bytes(), + HeaderMode: {byte(m.ID())}, + HeaderReadWrite: {rw}, + } + if len(opts.GsocOwner) > 0 { + headers[HeaderGsocOwner] = opts.GsocOwner + } + if len(opts.GsocID) > 0 { + headers[HeaderGsocID] = opts.GsocID + } + return p.NewStream(ctx, overlay, headers, protocolName, protocolVersion, streamName) +} + +// validatePublisher sets SOC parameters on the broker side so it can validate the messages. +func (m *GSOCEphemeralMode) validatePublisher(headers p2p.Headers) error { + gsocOwner := headers[HeaderGsocOwner] + gsocID := headers[HeaderGsocID] + + m.mu.Lock() + m.setGsocParams(gsocOwner, gsocID) + set := m.gsocID != nil + m.mu.Unlock() + + if !set { + return ErrWrongHeaders + } + return nil +} + +// FormatBroadcast formats a raw publisher message for delivery to a subscriber. +// First delivery to each subscriber includes a handshake with SOC identity; subsequent are data-only. +func (m *GSOCEphemeralMode) formatBroadcast(sub *brokerSubscriber, rawMsg []byte) []byte { + if !sub.handshakeHappened { + // Handshake: [1B type=0x02][32B SOC ID][20B owner][65B sig][8B span][NB payload] + msg := make([]byte, 1+IDSize+OwnerSize+len(rawMsg)) + msg[0] = MsgTypeHandshake + copy(msg[1:1+IDSize], m.gsocID) + copy(msg[1+IDSize:1+IDSize+OwnerSize], m.gsocOwner) + copy(msg[1+IDSize+OwnerSize:], rawMsg) + sub.handshakeHappened = true + return msg + } + + // Data: [1B type=0x03][65B sig][8B span][NB payload] + msg := make([]byte, 1+len(rawMsg)) + msg[0] = MsgTypeData + copy(msg[1:], rawMsg) + return msg +} + +// ReadPublisherMessage reads [65B sig][8B span][NB payload (max 4KB)] from the stream, +// constructs and validates the SOC chunk and returns that. +func (m *GSOCEphemeralMode) ReadPublisherMessage(stream p2p.Stream) ([]byte, error) { + sig := make([]byte, SigSize) + if _, err := io.ReadFull(stream, sig); err != nil { + return nil, err + } + spanBytes := make([]byte, SpanSize) + if _, err := io.ReadFull(stream, spanBytes); err != nil { + return nil, err + } + span := min(binary.LittleEndian.Uint64(spanBytes), MaxPayload) + + payload := make([]byte, span) + if _, err := io.ReadFull(stream, payload); err != nil { + return nil, err + } + + // Construct SOC chunk: [ID (32B)][sig (65B)][span (8B)][payload] + socData := make([]byte, IDSize+SigSize+SpanSize+int(span)) + copy(socData, m.gsocID) + copy(socData[IDSize:], sig) + copy(socData[IDSize+SigSize:], spanBytes) + copy(socData[IDSize+SigSize+SpanSize:], payload) + + if !soc.Valid(swarm.NewChunk(m.topicAddress, socData)) { + m.logger.Debug("soc validation failed", "topicAddress", m.topicAddress, "socData", socData) + return nil, ErrInvalidSignature + } + + return socData[IDSize:], nil +} + +// ReadBrokerMessage reads one broker→subscriber message and verifies it +func (m *GSOCEphemeralMode) ReadBrokerMessage(stream p2p.Stream) ([]byte, error) { + typeBuf := make([]byte, 1) + if _, err := io.ReadFull(stream, typeBuf); err != nil { + return nil, err + } + + if handled, err := readServiceMessage(typeBuf[0]); err != nil { + return nil, err + } else if handled { + return nil, nil + } + + switch typeBuf[0] { + case MsgTypeHandshake: + socID := make([]byte, IDSize) + if _, err := io.ReadFull(stream, socID); err != nil { + return nil, fmt.Errorf("read SOC ID: %w", err) + } + ownerAddr := make([]byte, OwnerSize) + if _, err := io.ReadFull(stream, ownerAddr); err != nil { + return nil, fmt.Errorf("read owner addr: %w", err) + } + m.setGsocParams(ownerAddr, socID) + + return m.ReadPublisherMessage(stream) // same as publisher message at this point + + case MsgTypeData: + if m.gsocID == nil { + return nil, fmt.Errorf("pubsub: data message before handshake") + } + return m.ReadPublisherMessage(stream) + + default: + return nil, fmt.Errorf("pubsub: unknown message type: 0x%02x", typeBuf[0]) + } +} + +// SubscriberCount returns the number of active subscribers. +func (m *GSOCEphemeralMode) SubscriberCount() int { + m.mu.RLock() + defer m.mu.RUnlock() + return len(m.subscribers) +} + +// SubscriberOverlays returns the overlay addresses of all active subscribers. +func (m *GSOCEphemeralMode) SubscriberOverlays() []string { + m.mu.RLock() + defer m.mu.RUnlock() + overlays := make([]string, 0, len(m.subscribers)) + for _, sub := range m.subscribers { + overlays = append(overlays, sub.overlay.String()) + } + return overlays +} + +// broadcast sends a message to all subscribers. +func (m *GSOCEphemeralMode) broadcast(rawMsg []byte) { + m.mu.Lock() + defer m.mu.Unlock() + + m.logger.Info("broadcasting to subscribers", "count", len(m.subscribers), "size", len(rawMsg)) + for _, sub := range m.subscribers { + msg := m.formatBroadcast(sub, rawMsg) + + select { + case sub.outCh <- msg: + m.logger.Info("message enqueued for subscriber", "peer", sub.overlay, "size", len(msg)) + default: + m.logger.Warning("subscriber message queue full, dropping message", "peer", sub.overlay) + } + } +} + +// HandleBroker handles an incoming broker-side stream, dispatching to publisher or subscriber handling. +func (m *GSOCEphemeralMode) HandleBroker(ctx context.Context, peer p2p.Peer, stream p2p.Stream, headers p2p.Headers) error { + rwBytes := headers[HeaderReadWrite] + m.logger.Info("broker stream opened", "peer", peer.Address, "topic", m.TopicAddress(), "rw", rwBytes) + if len(rwBytes) != 1 { + _ = stream.Reset() + return ErrWrongHeaders + } + if rwBytes[0] == 1 { + return m.handlePublisher(ctx, peer, stream, headers) + } + m.logger.Info("handling as subscriber", "peer", peer.Address) + return m.handleSubscriber(ctx, peer, stream) +} + +func (m *GSOCEphemeralMode) handleSubscriber(ctx context.Context, peer p2p.Peer, stream p2p.Stream) error { + subCtx, cancel, unregister := m.registerSubscriber(ctx, peer.Address, stream) + defer cancel() + defer unregister() + + m.logger.Info("subscriber connected", "peer", peer.Address, "topic", m.TopicAddress()) + + <-subCtx.Done() + if errors.Is(subCtx.Err(), context.Canceled) { + return nil + } + return subCtx.Err() +} + +func (m *GSOCEphemeralMode) handlePublisher(ctx context.Context, peer p2p.Peer, stream p2p.Stream, headers p2p.Headers) error { + m.logger.Info("publisher handler entered", "peer", peer.Address, "topic", m.TopicAddress()) + + if err := m.validatePublisher(headers); err != nil { + m.logger.Info("publisher validation failed", "peer", peer.Address, "error", err) + _ = stream.Reset() + return err + } + m.logger.Info("publisher validated", "peer", peer.Address, "gsoc_id", fmt.Sprintf("%x", m.gsocID), "gsoc_owner", fmt.Sprintf("%x", m.gsocOwner)) + + partCtx, cancel, unregister := m.registerSubscriber(ctx, peer.Address, stream) + defer cancel() + defer unregister() + + m.logger.Info("publisher connected, starting read loop", "peer", peer.Address, "topic", m.TopicAddress(), "subscribers", m.SubscriberCount()) + + for { + select { + case <-partCtx.Done(): + if errors.Is(partCtx.Err(), context.Canceled) { + return nil + } + return partCtx.Err() + default: + } + + m.logger.Info("waiting for publisher message", "peer", peer.Address) + rawMsg, err := m.ReadPublisherMessage(stream) + if err != nil { + if errors.Is(err, io.EOF) { + m.logger.Info("publisher stream EOF", "peer", peer.Address) + return nil + } + m.logger.Info("publisher read error", "peer", peer.Address, "error", err) + return fmt.Errorf("read publisher message: %w", err) + } + + m.logger.Info("publisher message received", "peer", peer.Address, "size", len(rawMsg)) + m.broadcast(rawMsg) + } +} + +// registerSubscriber adds a peer as a subscriber and starts a write goroutine for it. +func (m *GSOCEphemeralMode) registerSubscriber(ctx context.Context, overlay swarm.Address, stream p2p.Stream) (context.Context, context.CancelFunc, func()) { + connCtx, cancel := context.WithCancel(ctx) + + sub := &brokerSubscriber{ + overlay: overlay, + stream: stream, + outCh: make(chan []byte, 256), + cancel: cancel, + } + + overlayKey := overlay.String() + m.mu.Lock() + m.subscribers[overlayKey] = sub + m.mu.Unlock() + + startBrokerWriter(connCtx, cancel, stream, sub.outCh, overlay, m.logger) + + unregister := func() { + m.mu.Lock() + if m.subscribers[overlayKey] == sub { + delete(m.subscribers, overlayKey) + } + m.mu.Unlock() + } + + return connCtx, cancel, unregister +} + +// CreateSubscriberConn returns the existing SubscriberConn for this topic if one is active, +// incrementing its ref count so the shared stream stays open. When no conn exists yet, +// a new one is created and a single mux goroutine is started to fan out broker messages. +func (m *GSOCEphemeralMode) CreateSubscriberConn(stream p2p.Stream, overlay swarm.Address) *SubscriberConn { + m.mu.Lock() + defer m.mu.Unlock() + + if m.subscriberConn != nil { + m.subscriberConn.refs++ + return m.subscriberConn + } + + sc := &SubscriberConn{ + Stream: stream, + Overlay: overlay, + refs: 1, + subs: make(map[uint64]chan []byte), + logger: m.logger, + } + m.subscriberConn = sc + go m.runMux(stream) + return sc +} + +// runMux reads broker messages from the shared p2p stream and broadcasts each to all +// registered WS sessions. It exits when the stream closes or returns an error. +// On exit it immediately clears m.subscriberConn so new Connect calls open a fresh stream. +func (m *GSOCEphemeralMode) runMux(stream p2p.Stream) { + defer func() { + m.subscriberConn.closeAll() + m.mu.Lock() + m.subscriberConn = nil + m.mu.Unlock() + }() + for { + msg, err := m.ReadBrokerMessage(stream) + if err != nil { + m.logger.Debug("pubsub mux: stream error, stopping", "error", err) + return + } + if msg == nil { + continue + } + m.subscriberConn.fanOut(msg) + } +} + +// GetSubscriberConn returns the subscriber-side connection, or nil. +func (m *GSOCEphemeralMode) GetSubscriberConn() *SubscriberConn { + m.mu.RLock() + defer m.mu.RUnlock() + return m.subscriberConn +} + +// RemoveSubscriberConn decrements the ref count for conn. +// When the last WS session exits it closes the stream, stopping the mux goroutine. +// If the mux already died and cleared m.subscriberConn, refs are still tracked on conn +// so the stream is closed exactly once when refs reach zero. +func (m *GSOCEphemeralMode) RemoveSubscriberConn(conn *SubscriberConn) { + m.mu.Lock() + defer m.mu.Unlock() + conn.refs-- + if conn.refs <= 0 { + m.subscriberConn = nil + _ = conn.Stream.FullClose() + } +} + +// setGsocParams sets the GSOC recurring parameters so that messages don't need to include them. +func (m *GSOCEphemeralMode) setGsocParams(gsocOwner, gsocID []byte) { + if m.gsocOwner != nil { + return + } + // Verify got socId and address match with topicaddress + addr, err := soc.CreateAddress(gsocID, gsocOwner) + if err != nil || !bytes.Equal(addr.Bytes(), m.topicAddress.Bytes()) { + m.logger.Debug("gsoc params verification failed", "err", err, "addr", addr, "topicAddress", m.topicAddress) + return + } + + m.gsocOwner = make([]byte, OwnerSize) + copy(m.gsocOwner, gsocOwner) + m.gsocID = make([]byte, IDSize) + copy(m.gsocID, gsocID) +} diff --git a/pkg/pubsub/pubsub.go b/pkg/pubsub/pubsub.go new file mode 100644 index 00000000000..308bd84d55b --- /dev/null +++ b/pkg/pubsub/pubsub.go @@ -0,0 +1,381 @@ +// 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 pubsub + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/ethersphere/bee/v2/pkg/bzz" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/log" + "github.com/ethersphere/bee/v2/pkg/p2p" + "github.com/ethersphere/bee/v2/pkg/swarm" + ma "github.com/multiformats/go-multiaddr" +) + +const ( + loggerName = "pubsub" + protocolName = "pubsub" + protocolVersion = "1.0.0" + streamName = "msg" + + // p2p stream header keys + HeaderTopicAddress = "pubsub-topic-address" + HeaderMode = "pubsub-mode" + HeaderReadWrite = "pubsub-readwrite" // 1 = read+write (publisher), 0 = read-only (subscriber) + + // Mode constants + ModeGSOCEphemeral ModeID = 1 + + // Service-level broker message types. + // 0x01 is reserved for ping across all modes; mode-specific types start at 0x02. + MsgTypePing byte = 0x01 + + // streamPingInterval is how often the broker sends a keepalive ping to each subscriber. + streamPingInterval = 30 * time.Second + + // Wire format sizes + SpanSize = swarm.SpanSize // pubsub span: 8-byte little-endian uint64 (matches bee-js Span.LENGTH) + MaxPayload = swarm.ChunkSize + SigSize = swarm.SocSignatureSize + IDSize = swarm.HashSize + OwnerSize = crypto.AddressSize +) + +var ( + ErrBrokerDisabled = errors.New("pubsub: broker mode is disabled") + ErrMaxConnections = errors.New("pubsub: max connections reached") + ErrInvalidHandshake = errors.New("pubsub: handshake verification failed") + ErrWrongHeaders = errors.New("pubsub: wrong required headers") + ErrTopicMismatch = errors.New("pubsub: topic address mismatch") +) + +// startBrokerWriter starts a goroutine that drains outCh to stream and sends +// keepalive pings on every streamPingInterval tick. +func startBrokerWriter(ctx context.Context, cancel context.CancelFunc, stream p2p.Stream, outCh <-chan []byte, overlay swarm.Address, logger log.Logger) { + go func() { + ticker := time.NewTicker(streamPingInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case msg := <-outCh: + if err := writeRaw(stream, msg); err != nil { + logger.Info("broker write to subscriber failed", "peer", overlay, "error", err) + cancel() + return + } + logger.Info("broker wrote to subscriber", "peer", overlay, "size", len(msg)) + case <-ticker.C: + if err := writeRaw(stream, []byte{MsgTypePing}); err != nil { + cancel() + return + } + } + } + }() +} + +// readServiceMessage handles broker wire messages that are common to all modes. +// Returns (handled, err). The caller should return (nil, err) when handled is true. +func readServiceMessage(typeBuf byte) (handled bool, err error) { + switch typeBuf { + case MsgTypePing: + return true, nil + } + return false, nil +} + +func newMode(topicAddr [32]byte, modeID ModeID, logger log.Logger) (Mode, error) { + switch modeID { + case ModeGSOCEphemeral: + return NewGSOCEphemeralMode(topicAddr[:], logger), nil + default: + return nil, fmt.Errorf("pubsub: unknown mode: %d", modeID) + } +} + +// ConnectOptions carries optional mode-specific parameters for Connect. +type ConnectOptions struct { + ReadWrite bool // true = publisher (read+write), false = subscriber (read-only) + GsocOwner []byte + GsocID []byte +} + +// TopicInfo describes a topic for the list endpoint. +type TopicInfo struct { + TopicAddress string `json:"topicAddress"` + Mode ModeID `json:"mode"` + Role string `json:"role"` + Connections []string `json:"connections"` +} + +// TopicModeKey is a composite key for identifying a mode instance per topic. +type TopicModeKey struct { + TopicAddr [32]byte + ModeID ModeID +} + +// P2P groups the p2p capabilities needed by the pubsub service. +type P2P interface { + p2p.Service + p2p.Streamer + // ConnectAllowLight dials a peer, accepting it even if it identifies + // itself as a light node (broker peers may run in light-node mode). + ConnectAllowLight(ctx context.Context, addrs []ma.Multiaddr) (*bzz.Address, error) +} + +// Service is the pubsub protocol service. +type Service struct { + mu sync.RWMutex + p2p P2P + logger log.Logger + brokerMode bool + maxConns int + modes map[TopicModeKey]Mode // (topic, mode) -> mode instance +} + +func New(p2p P2P, logger log.Logger, brokerMode bool, maxConns int) *Service { + s := &Service{ + p2p: p2p, + logger: logger.WithName(loggerName).Register(), + brokerMode: brokerMode, + maxConns: maxConns, + modes: make(map[TopicModeKey]Mode), + } + return s +} + +// Protocol returns the p2p protocol spec. +func (s *Service) Protocol() p2p.ProtocolSpec { + return p2p.ProtocolSpec{ + Name: protocolName, + Version: protocolVersion, + StreamSpecs: []p2p.StreamSpec{ + { + Name: streamName, + Handler: s.brokerHandler, + }, + }, + } +} + +// Connect establishes a subscriber connection to a broker peer. +func (s *Service) Connect(ctx context.Context, underlay ma.Multiaddr, topicAddr [32]byte, modeID ModeID, opts ConnectOptions) (Mode, error) { + key := TopicModeKey{TopicAddr: topicAddr, ModeID: modeID} + m, err := s.getOrCreateMode(key) + if err != nil { + return nil, err + } + + s.logger.Info("connecting to broker peer", "underlay", underlay) + bzzAddr, err := s.p2p.ConnectAllowLight(ctx, []ma.Multiaddr{underlay}) + if err != nil && !errors.Is(err, p2p.ErrAlreadyConnected) { + return nil, fmt.Errorf("connect to peer: %w", err) + } + s.logger.Info("connected to broker peer", "overlay", bzzAddr.Overlay) + + var sc *SubscriberConn + if existing := m.GetSubscriberConn(); existing != nil { + // Reuse the existing p2p stream — no new broker-side stream, just bump the ref count. + sc = m.CreateSubscriberConn(existing.Stream, bzzAddr.Overlay) + } else { + stream, err := m.Connect(ctx, s.p2p, bzzAddr.Overlay, opts) + if err != nil { + s.logger.Error(err, "open stream failed") + return nil, fmt.Errorf("open stream: %w", err) + } + sc = m.CreateSubscriberConn(stream, bzzAddr.Overlay) + if sc.Stream != stream { + // Race: another goroutine created the conn between our check and create. + _ = stream.FullClose() + } + } + + go func() { + <-ctx.Done() + m.RemoveSubscriberConn(sc) + }() + + return m, nil +} + +// Topics returns info about all active topics. +func (s *Service) Topics() []TopicInfo { + s.mu.RLock() + defer s.mu.RUnlock() + + var topics []TopicInfo + + for key, m := range s.modes { + info := TopicInfo{ + TopicAddress: fmt.Sprintf("%x", key.TopicAddr), + Mode: m.ID(), + Connections: m.SubscriberOverlays(), + } + sc := m.GetSubscriberConn() + switch { + case m.SubscriberCount() > 0 && sc != nil: + info.Role = "broker+subscriber" + info.Connections = append(info.Connections, sc.Overlay.String()) + case m.SubscriberCount() > 0: + info.Role = "broker" + case sc != nil: + info.Role = "subscriber" + info.Connections = []string{sc.Overlay.String()} + default: + continue + } + topics = append(topics, info) + } + + return topics +} + +// brokerHandler handles incoming streams on the broker side. +func (s *Service) brokerHandler(ctx context.Context, peer p2p.Peer, stream p2p.Stream) error { + s.logger.Info("broker handler invoked", "peer", peer.Address) + if !s.brokerMode { + _ = stream.Reset() + return ErrBrokerDisabled + } + + headers := stream.Headers() + + topicAddrBytes := headers[HeaderTopicAddress] + if len(topicAddrBytes) != IDSize { + _ = stream.Reset() + return ErrWrongHeaders + } + var topicAddr [32]byte + copy(topicAddr[:], topicAddrBytes) + + modeBytes := headers[HeaderMode] + if len(modeBytes) != 1 { + _ = stream.Reset() + return ErrWrongHeaders + } + key := TopicModeKey{TopicAddr: topicAddr, ModeID: ModeID(modeBytes[0])} + m, err := s.getOrCreateMode(key) + if err != nil { + _ = stream.Reset() + return err + } + + if s.maxConns > 0 && m.SubscriberCount() >= s.maxConns { + _ = stream.Reset() + return ErrMaxConnections + } + + return m.HandleBroker(ctx, peer, stream, headers) +} + +func (s *Service) getOrCreateMode(key TopicModeKey) (Mode, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if m, ok := s.modes[key]; ok { + return m, nil + } + + m, err := newMode(key.TopicAddr, key.ModeID, s.logger) + if err != nil { + return nil, err + } + + s.modes[key] = m + return m, nil +} + +// writeRaw writes raw bytes to the stream. +func writeRaw(stream p2p.Stream, data []byte) error { + c := 0 + for c < len(data) { + n, err := stream.Write(data[c:]) + if err != nil { + return err + } + c += n + } + return nil +} + +// brokerSubscriber holds a subscriber's stream and outgoing message channel. +type brokerSubscriber struct { + overlay swarm.Address + stream p2p.Stream + outCh chan []byte + cancel context.CancelFunc + handshakeHappened bool +} + +// SubscriberConn represents the shared subscriber-side p2p stream to a broker. +// Multiple WebSocket sessions can attach to one SubscriberConn via the mux. +type SubscriberConn struct { + Stream p2p.Stream + Overlay swarm.Address + + refs int // number of active WS sessions; protected by the owning mode's mu + writeMu sync.Mutex + subsMu sync.Mutex + subs map[uint64]chan []byte + nextID uint64 + logger log.Logger +} + +// Subscribe registers a new WS session and returns its per-session message channel. +func (sc *SubscriberConn) Subscribe() (uint64, <-chan []byte) { + sc.subsMu.Lock() + defer sc.subsMu.Unlock() + id := sc.nextID + sc.nextID++ + ch := make(chan []byte, 16) + sc.subs[id] = ch + return id, ch +} + +// Unsubscribe removes the WS session channel and closes it. +func (sc *SubscriberConn) Unsubscribe(id uint64) { + sc.subsMu.Lock() + defer sc.subsMu.Unlock() + if ch, ok := sc.subs[id]; ok { + close(ch) + delete(sc.subs, id) + } +} + +// fanOut broadcasts a message to all registered WS session channels. +func (sc *SubscriberConn) fanOut(msg []byte) { + sc.subsMu.Lock() + defer sc.subsMu.Unlock() + for _, ch := range sc.subs { + select { + case ch <- msg: + default: + sc.logger.Warning("pubsub: subscriber ws channel full, dropping message") + } + } +} + +func (sc *SubscriberConn) closeAll() { + sc.subsMu.Lock() + defer sc.subsMu.Unlock() + for id, ch := range sc.subs { + close(ch) + delete(sc.subs, id) + } +} + +// WriteToStream serializes concurrent writes from multiple WS sessions. +func (sc *SubscriberConn) WriteToStream(data []byte) error { + sc.writeMu.Lock() + defer sc.writeMu.Unlock() + return writeRaw(sc.Stream, data) +} diff --git a/pkg/pubsub/ws.go b/pkg/pubsub/ws.go new file mode 100644 index 00000000000..1cc65793ac4 --- /dev/null +++ b/pkg/pubsub/ws.go @@ -0,0 +1,126 @@ +// 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 pubsub + +import ( + "context" + "time" + + "github.com/ethersphere/bee/v2/pkg/log" + "github.com/gorilla/websocket" +) + +type WsOptions struct { + PingPeriod time.Duration + Cancel context.CancelFunc +} + +// ListeningWs bridges a subscriber's p2p stream to a WebSocket connection. +// The Mode handles all wire-format details: reading broker messages, +// verifying them, and returning the payload to forward to the WebSocket. +// If the subscriber is a Publisher, it also reads from the WebSocket +// and writes raw messages to the p2p stream. +func ListeningWs(ctx context.Context, conn *websocket.Conn, options WsOptions, logger log.Logger, mode Mode, isPublisher bool) { + sc := mode.GetSubscriberConn() + subID, msgCh := sc.Subscribe() + var ( + ticker = time.NewTicker(options.PingPeriod) + writeDeadline = options.PingPeriod + time.Second + readDeadline = options.PingPeriod + time.Second + ) + + logger.Info("pubsub ws: starting", "topic", mode.TopicAddress(), "isPublisher", isPublisher, "pingPeriod", options.PingPeriod) + + conn.SetCloseHandler(func(code int, text string) error { + logger.Info("pubsub ws: client gone", "topic", mode.TopicAddress(), "code", code, "message", text) + options.Cancel() + return nil + }) + + // Reset read deadline on every pong so idle subscribers don't time out. + conn.SetPongHandler(func(appData string) error { + if err := conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { + return err + } + return conn.SetWriteDeadline(time.Now().Add(writeDeadline)) + }) + + // A read loop is always required so gorilla can process pong responses + // and close frames from the client. + go func() { + for { + if err := conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { + logger.Info("pubsub ws: set read deadline failed", "error", err) + break + } + msgType, p, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { + logger.Info("pubsub ws: read error", "error", err) + } else { + logger.Info("pubsub ws: read loop ended", "error", err) + } + break + } + + if isPublisher { + logger.Info("pubsub ws: publisher message from ws", "type", msgType, "size", len(p)) + if err := sc.WriteToStream(p); err != nil { + logger.Info("pubsub ws: write to p2p stream failed", "error", err) + break + } + } + } + options.Cancel() + }() + + // Forward mux-delivered broker messages to this WebSocket session. + go func() { + defer sc.Unsubscribe(subID) + for { + select { + case <-ctx.Done(): + logger.Info("pubsub ws: p2p reader context done") + return + case msg, ok := <-msgCh: + if !ok { + logger.Info("pubsub ws: mux channel closed") + options.Cancel() + return + } + logger.Info("pubsub ws: forwarding broker message to ws", "size", len(msg)) + if err := conn.WriteMessage(websocket.BinaryMessage, msg); err != nil { + logger.Info("pubsub ws: write to ws failed", "error", err) + options.Cancel() + return + } + } + } + }() + + defer func() { + ticker.Stop() + _ = conn.Close() + logger.Info("pubsub ws: closed", "topic", mode.TopicAddress()) + }() + + for { + if err := conn.SetWriteDeadline(time.Now().Add(writeDeadline)); err != nil { + logger.Info("pubsub ws: set write deadline failed", "error", err) + return + } + select { + case <-ctx.Done(): + logger.Info("pubsub ws: context cancelled, closing") + _ = conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + case <-ticker.C: + if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { + logger.Info("pubsub ws: ping failed, closing", "error", err) + return + } + } + } +} diff --git a/pkg/pullsync/mock/pullsync.go b/pkg/pullsync/mock/pullsync.go index bb77934924f..3954c92e62b 100644 --- a/pkg/pullsync/mock/pullsync.go +++ b/pkg/pullsync/mock/pullsync.go @@ -72,7 +72,6 @@ func NewPullSync(opts ...Option) *PullSyncMock { } func (p *PullSyncMock) Sync(ctx context.Context, peer swarm.Address, bin uint8, start uint64) (topmost uint64, count int, err error) { - p.mtx.Lock() id := toID(peer, bin, start) diff --git a/pkg/replicas/export_test.go b/pkg/replicas/export_test.go index 271ad71ed0b..50270b91f47 100644 --- a/pkg/replicas/export_test.go +++ b/pkg/replicas/export_test.go @@ -6,9 +6,7 @@ package replicas import "github.com/ethersphere/bee/v2/pkg/storage" -var ( - Signer = signer -) +var Signer = signer func Wait(g storage.Getter) { g.(*getter).wg.Wait() diff --git a/pkg/replicas/putter_test.go b/pkg/replicas/putter_test.go index cee4960f5b0..a626f38801e 100644 --- a/pkg/replicas/putter_test.go +++ b/pkg/replicas/putter_test.go @@ -34,7 +34,6 @@ type testBasePutter struct { } func (tbp *testBasePutter) Get(ctx context.Context, addr swarm.Address) (swarm.Chunk, error) { - g := tbp.getErrors if g != nil { return nil, g(ctx, addr) @@ -43,7 +42,6 @@ func (tbp *testBasePutter) Get(ctx context.Context, addr swarm.Address) (swarm.C } func (tbp *testBasePutter) Put(ctx context.Context, ch swarm.Chunk) error { - g := tbp.putErrors if g != nil { return g(ctx, ch.Address()) @@ -192,5 +190,4 @@ func TestPutter(t *testing.T) { }) } }) - } diff --git a/pkg/resolver/client/ens/ens.go b/pkg/resolver/client/ens/ens.go index b921ad8c9b7..3a6ad83e5cc 100644 --- a/pkg/resolver/client/ens/ens.go +++ b/pkg/resolver/client/ens/ens.go @@ -137,7 +137,6 @@ func (c *Client) Resolve(name string) (Address, error) { func (c *Client) Close() error { if c.ethCl != nil { c.ethCl.Close() - } c.ethCl = nil diff --git a/pkg/resolver/multiresolver/config.go b/pkg/resolver/multiresolver/config.go index 52ddd5f6122..6e552b1af88 100644 --- a/pkg/resolver/multiresolver/config.go +++ b/pkg/resolver/multiresolver/config.go @@ -49,7 +49,6 @@ func parseConnectionString(cs string) (ConnectionConfig, error) { tld = endpoint[:i] if len(tld) > maxTLDLength { return ConnectionConfig{}, fmt.Errorf("tld %s: %w", tld, ErrTLDTooLong) - } endpoint = endpoint[i+1:] } diff --git a/pkg/resolver/multiresolver/multiresolver.go b/pkg/resolver/multiresolver/multiresolver.go index 87cfda56d16..2980dbfc35a 100644 --- a/pkg/resolver/multiresolver/multiresolver.go +++ b/pkg/resolver/multiresolver/multiresolver.go @@ -79,7 +79,6 @@ func NewMultiResolver(opts ...Option) *MultiResolver { // Attempt to connect to each resolver using the connection string. for _, c := range mr.cfgs { - // NOTE: if we want to create a specific client based on the TLD // we can do it here. mr.connectENSClient(c.TLD, c.Address, c.Endpoint) diff --git a/pkg/retrieval/retrieval.go b/pkg/retrieval/retrieval.go index f1e5f5193b7..9432a058690 100644 --- a/pkg/retrieval/retrieval.go +++ b/pkg/retrieval/retrieval.go @@ -157,7 +157,6 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s spanCtx := context.WithoutCancel(ctx) v, _, err := s.singleflight.Do(ctx, flightRoute, func(ctx context.Context) (swarm.Chunk, error) { - skip := skippeers.NewList(0) defer skip.Close() @@ -170,7 +169,7 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s quit := make(chan struct{}) defer close(quit) - var forwards = maxMultiplexForwards + forwards := maxMultiplexForwards // if we are the origin node, allow many preemptive retries to speed up the retrieval of the chunk. errorsLeft := 1 @@ -197,7 +196,6 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s inflight := 0 for errorsLeft > 0 { - select { case <-ctx.Done(): return nil, ctx.Err() @@ -212,7 +210,7 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s peer, err := s.closestPeer(chunkAddr, fullSkip, origin) if errors.Is(err, topology.ErrNotFound) { - if skip.PruneExpiresAfter(chunkAddr, overDraftRefresh) == 0 { //no overdraft peers, we have depleted ALL peers + if skip.PruneExpiresAfter(chunkAddr, overDraftRefresh) == 0 { // no overdraft peers, we have depleted ALL peers if inflight == 0 { loggerV1.Debug("no peers left", "chunk_address", chunkAddr, "errors_left", errorsLeft, "isOrigin", origin, "own_proximity", swarm.Proximity(s.addr.Bytes(), chunkAddr.Bytes()), "error", err) return nil, err @@ -298,7 +296,6 @@ func (s *Service) RetrieveChunk(ctx context.Context, chunkAddr, sourcePeerAddr s } func (s *Service) retrieveChunk(ctx context.Context, quit chan struct{}, chunkAddr, peer swarm.Address, result chan retrievalResult, action accounting.Action, span opentracing.Span) { - var ( startTime = time.Now() err error @@ -370,7 +367,6 @@ func (s *Service) retrieveChunk(ctx context.Context, quit chan struct{}, chunkAd } func (s *Service) prepareCredit(ctx context.Context, peer, chunk swarm.Address, origin bool) (accounting.Action, error) { - price := s.pricer.PeerPrice(peer, chunk) s.metrics.ChunkPrice.Observe(float64(price)) @@ -388,7 +384,6 @@ func (s *Service) prepareCredit(ctx context.Context, peer, chunk swarm.Address, // the chunk than this node is, could also be returned, allowing the upstream // retrieve request. func (s *Service) closestPeer(addr swarm.Address, skipPeers []swarm.Address, allowUpstream bool) (swarm.Address, error) { - var ( closest swarm.Address err error diff --git a/pkg/retrieval/retrieval_test.go b/pkg/retrieval/retrieval_test.go index 5d9eb331324..436b9e97b1a 100644 --- a/pkg/retrieval/retrieval_test.go +++ b/pkg/retrieval/retrieval_test.go @@ -182,7 +182,7 @@ func TestWaitForInflight(t *testing.T) { badServer := createRetrieval(t, badServerAddr, badMockStorer, nil, nil, logger, badServerMockAccounting, pricerMock, nil, false) - var fail = true + fail := true var lock sync.Mutex recorder := streamtest.New( diff --git a/pkg/salud/salud.go b/pkg/salud/salud.go index 1cf4ffa8373..4a3bebf76bf 100644 --- a/pkg/salud/salud.go +++ b/pkg/salud/salud.go @@ -144,7 +144,6 @@ func (s *service) salud(mode string, durPercentile float64, connsPercentile floa err := s.topology.EachConnectedPeer(func(addr swarm.Address, bin uint8) (stop bool, jumpToNext bool, err error) { wg.Go(func() { - ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() diff --git a/pkg/settlement/interface.go b/pkg/settlement/interface.go index f24ec60ce69..cd4465aac98 100644 --- a/pkg/settlement/interface.go +++ b/pkg/settlement/interface.go @@ -11,9 +11,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -var ( - ErrPeerNoSettlements = errors.New("no settlements for peer") -) +var ErrPeerNoSettlements = errors.New("no settlements for peer") // Interface is the interface used by Accounting to trigger settlement type Interface interface { diff --git a/pkg/settlement/pseudosettle/pseudosettle_test.go b/pkg/settlement/pseudosettle/pseudosettle_test.go index 397592e9d96..c1b1b32f3c2 100644 --- a/pkg/settlement/pseudosettle/pseudosettle_test.go +++ b/pkg/settlement/pseudosettle/pseudosettle_test.go @@ -75,11 +75,9 @@ func (t *testObserver) PeerDebt(peer swarm.Address) (*big.Int, error) { } func (t *testObserver) Connect(peer swarm.Address, full bool) { - } func (t *testObserver) Disconnect(peer swarm.Address) { - } func (t *testObserver) NotifyRefreshmentSent(peer swarm.Address, attemptedAmount, amount *big.Int, timestamp int64, allegedInterval int64, receivedError error) { @@ -120,8 +118,10 @@ func (t *testObserver) Reserve(ctx context.Context, peer swarm.Address, amount u func (t *testObserver) Release(peer swarm.Address, amount uint64) { } -var testRefreshRate = int64(10000) -var testRefreshRateLight = int64(1000) +var ( + testRefreshRate = int64(10000) + testRefreshRateLight = int64(1000) +) func testCaseNotAccepted(t *testing.T, recorder *streamtest.Recorder, payerObserver, receiverObserver *testObserver, payer, recipient *pseudosettle.Service, peerID swarm.Address, payerTime, recipientTime int64, recordsLength int, debtAmount, amount *big.Int, expectedError error) { t.Helper() diff --git a/pkg/settlement/swap/chequebook/cashout.go b/pkg/settlement/swap/chequebook/cashout.go index 44cda8c160a..a1aeccac7ac 100644 --- a/pkg/settlement/swap/chequebook/cashout.go +++ b/pkg/settlement/swap/chequebook/cashout.go @@ -19,10 +19,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/transaction" ) -var ( - // ErrNoCashout is the error if there has not been any cashout action for the chequebook - ErrNoCashout = errors.New("no prior cashout") -) +// ErrNoCashout is the error if there has not been any cashout action for the chequebook +var ErrNoCashout = errors.New("no prior cashout") // CashoutService is the service responsible for managing cashout actions type CashoutService interface { diff --git a/pkg/settlement/swap/chequebook/cashout_test.go b/pkg/settlement/swap/chequebook/cashout_test.go index 563b6ce4aa2..6717f7701a4 100644 --- a/pkg/settlement/swap/chequebook/cashout_test.go +++ b/pkg/settlement/swap/chequebook/cashout_test.go @@ -372,7 +372,6 @@ func TestCashoutStatusPending(t *testing.T) { }, UncashedAmount: big.NewInt(0), }) - } func verifyStatus(t *testing.T, status *chequebook.CashoutStatus, expected chequebook.CashoutStatus) { diff --git a/pkg/settlement/swap/chequebook/cheque_test.go b/pkg/settlement/swap/chequebook/cheque_test.go index f0aac83a43c..06586f3b215 100644 --- a/pkg/settlement/swap/chequebook/cheque_test.go +++ b/pkg/settlement/swap/chequebook/cheque_test.go @@ -6,7 +6,6 @@ package chequebook_test import ( "bytes" - "encoding/hex" "math/big" "testing" @@ -34,7 +33,6 @@ func TestSignCheque(t *testing.T) { signer := signermock.New( signermock.WithSignTypedDataFunc(func(data *eip712.TypedData) ([]byte, error) { - if data.Message["beneficiary"].(string) != beneficiaryAddress.Hex() { t.Fatal("signing cheque with wrong beneficiary") } diff --git a/pkg/settlement/swap/chequebook/chequestore.go b/pkg/settlement/swap/chequebook/chequestore.go index 8db2e6e3a28..ce5eaf4f33b 100644 --- a/pkg/settlement/swap/chequebook/chequestore.go +++ b/pkg/settlement/swap/chequebook/chequestore.go @@ -67,7 +67,8 @@ func NewChequeStore( chainID int64, beneficiary common.Address, transactionService transaction.Service, - recoverChequeFunc RecoverChequeFunc) ChequeStore { + recoverChequeFunc RecoverChequeFunc, +) ChequeStore { return &chequeStore{ store: store, factory: factory, diff --git a/pkg/settlement/swap/chequebook/factory.go b/pkg/settlement/swap/chequebook/factory.go index f548643bac1..818e79a3f9c 100644 --- a/pkg/settlement/swap/chequebook/factory.go +++ b/pkg/settlement/swap/chequebook/factory.go @@ -5,11 +5,11 @@ package chequebook import ( + "context" "errors" "fmt" "math/big" - "context" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/sctx" diff --git a/pkg/settlement/swap/erc20/erc20_test.go b/pkg/settlement/swap/erc20/erc20_test.go index 5322afb4a1d..e5e69390d4f 100644 --- a/pkg/settlement/swap/erc20/erc20_test.go +++ b/pkg/settlement/swap/erc20/erc20_test.go @@ -16,9 +16,7 @@ import ( "github.com/ethersphere/go-sw3-abi/sw3abi" ) -var ( - erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_9) -) +var erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_9) func TestBalanceOf(t *testing.T) { t.Parallel() diff --git a/pkg/settlement/swap/headers/utilities.go b/pkg/settlement/swap/headers/utilities.go index a940c45b7c3..c3c81ce533f 100644 --- a/pkg/settlement/swap/headers/utilities.go +++ b/pkg/settlement/swap/headers/utilities.go @@ -26,7 +26,6 @@ var ( ) func MakeSettlementHeaders(exchangeRate, deduction *big.Int) p2p.Headers { - return p2p.Headers{ exchangeRateFieldName: exchangeRate.Bytes(), deductionFieldName: deduction.Bytes(), @@ -34,7 +33,6 @@ func MakeSettlementHeaders(exchangeRate, deduction *big.Int) p2p.Headers { } func ParseSettlementResponseHeaders(receivedHeaders p2p.Headers) (exchange, deduction *big.Int, err error) { - exchangeRate, err := ParseExchangeHeader(receivedHeaders) if err != nil { return nil, nil, err diff --git a/pkg/settlement/swap/headers/utilities_test.go b/pkg/settlement/swap/headers/utilities_test.go index 0cb074ac00f..9c2ee6d5d4d 100644 --- a/pkg/settlement/swap/headers/utilities_test.go +++ b/pkg/settlement/swap/headers/utilities_test.go @@ -65,7 +65,6 @@ func TestParseExchangeHeader(t *testing.T) { if parsedExchange.Cmp(big.NewInt(906000)) != 0 { t.Fatalf("Allowance mismatch, got %v, want %v", parsedExchange, big.NewInt(906000)) } - } func TestParseDeductionHeader(t *testing.T) { @@ -83,5 +82,4 @@ func TestParseDeductionHeader(t *testing.T) { if parsedDeduction.Cmp(big.NewInt(5348)) != 0 { t.Fatalf("Allowance mismatch, got %v, want %v", parsedDeduction, big.NewInt(5348)) } - } diff --git a/pkg/settlement/swap/priceoracle/priceoracle.go b/pkg/settlement/swap/priceoracle/priceoracle.go index 1b60b7c89ec..027c8effc39 100644 --- a/pkg/settlement/swap/priceoracle/priceoracle.go +++ b/pkg/settlement/swap/priceoracle/priceoracle.go @@ -22,9 +22,7 @@ import ( // loggerName is the tree path name of the logger for this package. const loggerName = "priceoracle" -var ( - errDecodeABI = errors.New("could not decode abi data") -) +var errDecodeABI = errors.New("could not decode abi data") type service struct { logger log.Logger @@ -46,9 +44,7 @@ type Service interface { Start() } -var ( - priceOracleABI = abiutil.MustParseABI(priceoracleabi.PriceOracleABIv0_6_9) -) +var priceOracleABI = abiutil.MustParseABI(priceoracleabi.PriceOracleABIv0_6_9) func New(logger log.Logger, priceOracleAddress common.Address, transactionService transaction.Service, timeDivisor int64) Service { return &service{ diff --git a/pkg/settlement/swap/priceoracle/priceoracle_test.go b/pkg/settlement/swap/priceoracle/priceoracle_test.go index 53b1c4ef911..b924925482a 100644 --- a/pkg/settlement/swap/priceoracle/priceoracle_test.go +++ b/pkg/settlement/swap/priceoracle/priceoracle_test.go @@ -17,9 +17,7 @@ import ( "github.com/ethersphere/go-price-oracle-abi/priceoracleabi" ) -var ( - priceOracleABI = abiutil.MustParseABI(priceoracleabi.PriceOracleABIv0_6_9) -) +var priceOracleABI = abiutil.MustParseABI(priceoracleabi.PriceOracleABIv0_6_9) func TestExchangeGetPrice(t *testing.T) { t.Parallel() diff --git a/pkg/settlement/swap/swap.go b/pkg/settlement/swap/swap.go index 4e7f99bd060..c8cf0092093 100644 --- a/pkg/settlement/swap/swap.go +++ b/pkg/settlement/swap/swap.go @@ -144,7 +144,6 @@ func (s *Service) Pay(ctx context.Context, peer swarm.Address, amount *big.Int) } balance, err := s.proto.EmitCheque(ctx, peer, beneficiary, amount, s.chequebook.Issue) - if err != nil { return } @@ -276,9 +275,7 @@ func (s *Service) Handshake(peer swarm.Address, beneficiary common.Address) erro // LastSentCheque returns the last sent cheque for the peer func (s *Service) LastSentCheque(peer swarm.Address) (*chequebook.SignedCheque, error) { - common, known, err := s.addressbook.Beneficiary(peer) - if err != nil { return nil, err } @@ -296,9 +293,7 @@ func (s *Service) LastSentCheque(peer swarm.Address) (*chequebook.SignedCheque, // LastReceivedCheque returns the last received cheque for the peer func (s *Service) LastReceivedCheque(peer swarm.Address) (*chequebook.SignedCheque, error) { - common, known, err := s.addressbook.Chequebook(peer) - if err != nil { return nil, err } @@ -387,8 +382,7 @@ func (s *Service) AddDeductionByPeer(peer swarm.Address) error { return s.addressbook.AddDeductionBy(peer) } -type NoOpSwap struct { -} +type NoOpSwap struct{} func (*NoOpSwap) TotalSent(peer swarm.Address) (totalSent *big.Int, err error) { return nil, postagecontract.ErrChainDisabled diff --git a/pkg/settlement/swap/swap_test.go b/pkg/settlement/swap/swap_test.go index 2f4d748740e..a64f121ebda 100644 --- a/pkg/settlement/swap/swap_test.go +++ b/pkg/settlement/swap/swap_test.go @@ -85,11 +85,9 @@ func (t *testObserver) NotifyPaymentSent(peer swarm.Address, amount *big.Int, er } func (t *testObserver) Connect(peer swarm.Address, full bool) { - } func (t *testObserver) Disconnect(peer swarm.Address) { - } type addressbookMock struct { @@ -109,33 +107,43 @@ type addressbookMock struct { func (m *addressbookMock) MigratePeer(oldPeer, newPeer swarm.Address) error { return m.migratePeer(oldPeer, newPeer) } + func (m *addressbookMock) Beneficiary(peer swarm.Address) (beneficiary common.Address, known bool, err error) { return m.beneficiary(peer) } + func (m *addressbookMock) Chequebook(peer swarm.Address) (chequebookAddress common.Address, known bool, err error) { return m.chequebook(peer) } + func (m *addressbookMock) BeneficiaryPeer(beneficiary common.Address) (peer swarm.Address, known bool, err error) { return m.beneficiaryPeer(beneficiary) } + func (m *addressbookMock) ChequebookPeer(chequebook common.Address) (peer swarm.Address, known bool, err error) { return m.chequebookPeer(chequebook) } + func (m *addressbookMock) PutBeneficiary(peer swarm.Address, beneficiary common.Address) error { return m.putBeneficiary(peer, beneficiary) } + func (m *addressbookMock) PutChequebook(peer swarm.Address, chequebook common.Address) error { return m.putChequebook(peer, chequebook) } + func (m *addressbookMock) AddDeductionFor(peer swarm.Address) error { return m.addDeductionFor(peer) } + func (m *addressbookMock) AddDeductionBy(peer swarm.Address) error { return m.addDeductionBy(peer) } + func (m *addressbookMock) GetDeductionFor(peer swarm.Address) (bool, error) { return m.getDeductionFor(peer) } + func (m *addressbookMock) GetDeductionBy(peer swarm.Address) (bool, error) { return m.getDeductionBy(peer) } @@ -148,6 +156,7 @@ type cashoutMock struct { func (m *cashoutMock) CashCheque(ctx context.Context, chequebook, recipient common.Address) (common.Hash, error) { return m.cashCheque(ctx, chequebook, recipient) } + func (m *cashoutMock) CashoutStatus(ctx context.Context, chequebookAddress common.Address) (*chequebook.CashoutStatus, error) { return m.cashoutStatus(ctx, chequebookAddress) } @@ -255,7 +264,6 @@ func TestReceiveCheque(t *testing.T) { if !peerDeductionFor { t.Fatal("add deduction for peer not called") } - } func TestReceiveChequeReject(t *testing.T) { @@ -278,7 +286,7 @@ func TestReceiveChequeReject(t *testing.T) { Signature: []byte{}, } - var errReject = errors.New("reject") + errReject := errors.New("reject") chequeStore := mockchequestore.NewChequeStore( mockchequestore.WithReceiveChequeFunc(func(ctx context.Context, c *chequebook.SignedCheque, e *big.Int, d *big.Int) (*big.Int, error) { @@ -320,7 +328,6 @@ func TestReceiveChequeReject(t *testing.T) { t.Fatalf("observer called by error.") default: } - } func TestReceiveChequeWrongChequebook(t *testing.T) { @@ -378,7 +385,6 @@ func TestReceiveChequeWrongChequebook(t *testing.T) { t.Fatalf("observer called by error.") default: } - } func TestPay(t *testing.T) { @@ -493,7 +499,6 @@ func TestPayIssueError(t *testing.T) { case <-time.After(time.Second): t.Fatal("expected observer to be called") } - } func TestPayUnknownBeneficiary(t *testing.T) { @@ -556,7 +561,6 @@ func TestHandshake(t *testing.T) { txHash := common.HexToHash("0x1") peer, err := crypto.NewOverlayFromEthereumAddress(beneficiary[:], networkID, txHash.Bytes()) - if err != nil { t.Fatalf("crypto.NewOverlayFromEthereumAddress(...): unexpected error: %v", err) } @@ -609,7 +613,6 @@ func TestHandshakeNewPeer(t *testing.T) { trx := common.HexToHash("0x1") networkID := uint64(1) peer, err := crypto.NewOverlayFromEthereumAddress(beneficiary[:], networkID, trx.Bytes()) - if err != nil { t.Fatalf("crypto.NewOverlayFromEthereumAddress(...): unexpected error: %v", err) } @@ -662,7 +665,6 @@ func TestMigratePeer(t *testing.T) { trx := common.HexToHash("0x1") networkID := uint64(1) peer, err := crypto.NewOverlayFromEthereumAddress(beneficiary[:], networkID, trx.Bytes()) - if err != nil { t.Fatalf("crypto.NewOverlayFromEthereumAddress(...): unexpected error: %v", err) } diff --git a/pkg/settlement/swap/swapprotocol/export_test.go b/pkg/settlement/swap/swapprotocol/export_test.go index 31fc9e6b79f..7a7c31ab773 100644 --- a/pkg/settlement/swap/swapprotocol/export_test.go +++ b/pkg/settlement/swap/swapprotocol/export_test.go @@ -6,6 +6,7 @@ package swapprotocol import ( "context" + "github.com/ethersphere/bee/v2/pkg/p2p" ) diff --git a/pkg/settlement/swap/swapprotocol/swapprotocol.go b/pkg/settlement/swap/swapprotocol/swapprotocol.go index 5153a307bc3..4c0f3a41d5e 100644 --- a/pkg/settlement/swap/swapprotocol/swapprotocol.go +++ b/pkg/settlement/swap/swapprotocol/swapprotocol.go @@ -146,7 +146,6 @@ func (s *Service) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream) (e } func (s *Service) headler(receivedHeaders p2p.Headers, peerAddress swarm.Address) (returnHeaders p2p.Headers) { - exchangeRate, deduction, err := s.priceOracle.CurrentRates() if err != nil { return p2p.Headers{} @@ -240,7 +239,6 @@ func (s *Service) EmitCheque(ctx context.Context, peer swarm.Address, beneficiar return w.WriteMsgWithContext(ctx, &pb.EmitCheque{ Cheque: encodedCheque, }) - }) if err != nil { return nil, fmt.Errorf("call issue function: %w", err) diff --git a/pkg/sharky/recovery.go b/pkg/sharky/recovery.go index af064fda60a..1af3985197e 100644 --- a/pkg/sharky/recovery.go +++ b/pkg/sharky/recovery.go @@ -31,7 +31,7 @@ func NewRecovery(dir string, shardCnt int, datasize int) (*Recovery, error) { shardFiles := make([]*os.File, shardCnt) for i := range shardCnt { - file, err := os.OpenFile(path.Join(dir, fmt.Sprintf("shard_%03d", i)), os.O_RDWR, 0666) + file, err := os.OpenFile(path.Join(dir, fmt.Sprintf("shard_%03d", i)), os.O_RDWR, 0o666) if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("index %d: %w", i, ErrShardNotFound) } @@ -43,7 +43,7 @@ func NewRecovery(dir string, shardCnt int, datasize int) (*Recovery, error) { return nil, err } size := uint32(fi.Size() / int64(datasize)) - ffile, err := os.OpenFile(path.Join(dir, fmt.Sprintf("free_%03d", i)), os.O_RDWR|os.O_CREATE, 0666) + ffile, err := os.OpenFile(path.Join(dir, fmt.Sprintf("free_%03d", i)), os.O_RDWR|os.O_CREATE, 0o666) if err != nil { return nil, err } diff --git a/pkg/sharky/shard_slots_test.go b/pkg/sharky/shard_slots_test.go index 7738a1595aa..3c09c66f7cc 100644 --- a/pkg/sharky/shard_slots_test.go +++ b/pkg/sharky/shard_slots_test.go @@ -108,7 +108,7 @@ func releaseSlot(t *testing.T, shard *shard, loc Location) { type dirFS string func (d *dirFS) Open(path string) (fs.File, error) { - return os.OpenFile(filepath.Join(string(*d), path), os.O_RDWR|os.O_CREATE, 0644) + return os.OpenFile(filepath.Join(string(*d), path), os.O_RDWR|os.O_CREATE, 0o644) } func newShard(t *testing.T) *shard { diff --git a/pkg/sharky/slots.go b/pkg/sharky/slots.go index 71d058f2b88..e8fd72e7571 100644 --- a/pkg/sharky/slots.go +++ b/pkg/sharky/slots.go @@ -40,11 +40,13 @@ func (sl *slots) load() (err error) { return err } -// save persists the free slot bitvector on disk (without closing) +// save persists the free slot bitvector on disk (without closing). +// slots only ever grow (extend is the only mutation), so sl.data is always >= +// the previous file size. Seeking to 0 and overwriting is therefore always +// safe: no stale tail bytes can survive. Truncate(0) is intentionally absent +// because truncating before the write creates a crash window where the file is +// empty; removing it eliminates that vulnerability. func (sl *slots) save() error { - if err := sl.file.Truncate(0); err != nil { - return err - } if _, err := sl.file.Seek(0, 0); err != nil { return err } diff --git a/pkg/shed/example_store_test.go b/pkg/shed/example_store_test.go index a6adbb2c703..c3a8498cb91 100644 --- a/pkg/shed/example_store_test.go +++ b/pkg/shed/example_store_test.go @@ -321,5 +321,5 @@ func Example_store() { fmt.Println(bytes.Equal(got.Data(), ch.Data())) - //Output: true + // Output: true } diff --git a/pkg/shed/vector_uint64_test.go b/pkg/shed/vector_uint64_test.go index 4eeaf1cdaf3..87b47fa5e55 100644 --- a/pkg/shed/vector_uint64_test.go +++ b/pkg/shed/vector_uint64_test.go @@ -56,7 +56,7 @@ func TestUint64Vector(t *testing.T) { } for _, index := range []uint64{0, 1, 2, 5, 100} { - var want = 42 + index + want := 42 + index err = bins.Put(index, want) if err != nil { t.Fatal(err) @@ -70,7 +70,7 @@ func TestUint64Vector(t *testing.T) { } t.Run("overwrite", func(t *testing.T) { - var want = 84 + index + want := 84 + index err = bins.Put(index, want) if err != nil { t.Fatal(err) @@ -97,7 +97,7 @@ func TestUint64Vector(t *testing.T) { for _, index := range []uint64{0, 1, 2, 3, 5, 10} { batch := new(leveldb.Batch) - var want = 43 + index + want := 43 + index bins.PutInBatch(batch, index, want) err = db.WriteBatch(batch) if err != nil { @@ -113,7 +113,7 @@ func TestUint64Vector(t *testing.T) { t.Run("overwrite", func(t *testing.T) { batch := new(leveldb.Batch) - var want = 85 + index + want := 85 + index bins.PutInBatch(batch, index, want) err = db.WriteBatch(batch) if err != nil { diff --git a/pkg/skippeers/skippeers.go b/pkg/skippeers/skippeers.go index 20bbe3ba8f7..2da71c7f597 100644 --- a/pkg/skippeers/skippeers.go +++ b/pkg/skippeers/skippeers.go @@ -41,7 +41,6 @@ func NewList(workerWakeUpDur time.Duration) *List { } func (l *List) worker(d time.Duration) { - defer l.wg.Done() ticker := time.NewTicker(d) @@ -62,7 +61,6 @@ func (l *List) Forever(chunk, peer swarm.Address) { } func (l *List) Add(chunk, peer swarm.Address, expire time.Duration) { - l.mtx.Lock() defer l.mtx.Unlock() @@ -116,7 +114,6 @@ func (l *List) prune() { // Must be called under lock func (l *List) pruneChunk(ch string, now int64) int { - count := 0 for peer, exp := range l.skip[ch] { diff --git a/pkg/spinlock/wait_test.go b/pkg/spinlock/wait_test.go index 3091da15ddb..676691afac9 100644 --- a/pkg/spinlock/wait_test.go +++ b/pkg/spinlock/wait_test.go @@ -14,7 +14,6 @@ import ( ) func TestWait(t *testing.T) { - t.Run("timed out", func(t *testing.T) { synctest.Test(t, func(t *testing.T) { err := spinlock.Wait(time.Millisecond*20, func() bool { return false }) diff --git a/pkg/stabilization/stabilization.go b/pkg/stabilization/stabilization.go index e104050496e..56dfb2a95a8 100644 --- a/pkg/stabilization/stabilization.go +++ b/pkg/stabilization/stabilization.go @@ -31,8 +31,10 @@ const ( subscriptionTopic int = 0 ) -var _ Subscriber = (*Detector)(nil) -var _ io.Closer = (*Detector)(nil) +var ( + _ Subscriber = (*Detector)(nil) + _ io.Closer = (*Detector)(nil) +) // Subscriber defines the interface for stabilization subscription. type Subscriber interface { diff --git a/pkg/statestore/leveldb/leveldb.go b/pkg/statestore/leveldb/leveldb.go index 8ddd71d0215..2fdd5fe2543 100644 --- a/pkg/statestore/leveldb/leveldb.go +++ b/pkg/statestore/leveldb/leveldb.go @@ -24,9 +24,7 @@ import ( // loggerName is the tree path name of the logger for this package. const loggerName = "leveldb" -var ( - _ storage.StateStorer = (*Store)(nil) -) +var _ storage.StateStorer = (*Store)(nil) // Store uses LevelDB to store values. type Store struct { diff --git a/pkg/statestore/storeadapter/migration.go b/pkg/statestore/storeadapter/migration.go index e933fd93f39..96f471d027f 100644 --- a/pkg/statestore/storeadapter/migration.go +++ b/pkg/statestore/storeadapter/migration.go @@ -110,10 +110,8 @@ func deletePrefix(s storage.Store, prefix string) migration.StepFn { } func epochMigration(s storage.Store) migration.StepFn { - return func() error { - - var deleteEntries = []string{ + deleteEntries := []string{ "statestore_schema", "tags", puller.IntervalPrefix, diff --git a/pkg/status/status_test.go b/pkg/status/status_test.go index 2b3f9383fe3..b8693c704c3 100644 --- a/pkg/status/status_test.go +++ b/pkg/status/status_test.go @@ -256,6 +256,7 @@ func (m *statusSnapshotMock) Commitment() (uint64, error) { return m.BatchCommit func (m *statusSnapshotMock) GetChainState() *postage.ChainState { return &postage.ChainState{Block: m.LastSyncedBlock} } + func (m *statusSnapshotMock) ReserveSizeWithinRadius() uint64 { return m.Snapshot.ReserveSizeWithinRadius } diff --git a/pkg/steward/steward_test.go b/pkg/steward/steward_test.go index 4684c4713c4..7f80614e6d6 100644 --- a/pkg/steward/steward_test.go +++ b/pkg/steward/steward_test.go @@ -41,7 +41,7 @@ func TestSteward(t *testing.T) { var ( ctx = context.Background() chunks = 1000 - data = make([]byte, chunks*4096) //1k chunks + data = make([]byte, chunks*4096) // 1k chunks chunkStore = inmem store = mockstorer.NewWithChunkStore(chunkStore) localRetrieval = &localRetriever{ChunkStore: chunkStore} diff --git a/pkg/storage/inmemstore/inmemstore.go b/pkg/storage/inmemstore/inmemstore.go index 88076df2722..12c04b90a03 100644 --- a/pkg/storage/inmemstore/inmemstore.go +++ b/pkg/storage/inmemstore/inmemstore.go @@ -156,7 +156,6 @@ func (s *Store) Iterate(q storage.Query, fn storage.IterateFn) error { switch q.Order { case storage.KeyAscendingOrder: s.st.WalkPrefix(prefix, func(k string, v any) bool { - if q.PrefixAtStart && !skipUntil { if k >= prefix+separator+q.Prefix { skipUntil = true diff --git a/pkg/storage/migration/migration.go b/pkg/storage/migration/migration.go index b19d44a0868..b6ff0dfb5f0 100644 --- a/pkg/storage/migration/migration.go +++ b/pkg/storage/migration/migration.go @@ -21,11 +21,9 @@ type ( Steps = map[uint64]StepFn ) -var ( - // errStorageVersionItemUnmarshalInvalidSize is returned when trying - // to unmarshal buffer that is not of size storageVersionItemSize. - errStorageVersionItemUnmarshalInvalidSize = errors.New("unmarshal StorageVersionItem: invalid size") -) +// errStorageVersionItemUnmarshalInvalidSize is returned when trying +// to unmarshal buffer that is not of size storageVersionItemSize. +var errStorageVersionItemUnmarshalInvalidSize = errors.New("unmarshal StorageVersionItem: invalid size") // Migrate migrates the storage to the latest version. // The steps are separated by groups so different lists of steps can run individually, for example, diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 312364e2676..dfa56ddf4bc 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -300,7 +300,6 @@ func ChunkType(ch swarm.Chunk) swarm.ChunkType { // but hashing the soc address and the wrapped chunk address is. // it is used in the reserve sampling and other places where a key is needed to represent a chunk. func IdentityAddress(chunk swarm.Chunk) (swarm.Address, error) { - if cac.Valid(chunk) { return chunk.Address(), nil } diff --git a/pkg/storage/storagetest/benchmark_test.go b/pkg/storage/storagetest/benchmark_test.go index e1c986aefc8..fb9f17948ed 100644 --- a/pkg/storage/storagetest/benchmark_test.go +++ b/pkg/storage/storagetest/benchmark_test.go @@ -17,9 +17,7 @@ const ( vs = 100 ) -var ( - format = "100000000000000%d" -) +var format = "100000000000000%d" func TestCompressibleBytes(t *testing.T) { t.Parallel() diff --git a/pkg/storage/testing/chunk.go b/pkg/storage/testing/chunk.go index 512cf1746c6..48e1c197e49 100644 --- a/pkg/storage/testing/chunk.go +++ b/pkg/storage/testing/chunk.go @@ -91,7 +91,6 @@ func GenerateTestRandomChunkAt(tb testing.TB, target swarm.Address, po int) swar addr := swarm.RandAddressAt(tb, target, po) stamp := postagetesting.MustNewStamp() return swarm.NewChunk(addr, data).WithStamp(stamp) - } // GenerateValidRandomChunkAt generates an valid chunk with address of proximity order po wrt target. diff --git a/pkg/storageincentives/redistribution/redistribution_test.go b/pkg/storageincentives/redistribution/redistribution_test.go index 6f4f429b705..b48e9014b02 100644 --- a/pkg/storageincentives/redistribution/redistribution_test.go +++ b/pkg/storageincentives/redistribution/redistribution_test.go @@ -66,7 +66,7 @@ func TestRedistribution(t *testing.T) { owner := common.HexToAddress("abcd") overlay := swarm.NewAddress(common.HexToHash("cbd").Bytes()) redistributionContractAddress := common.HexToAddress("ffff") - //nonce := common.BytesToHash(make([]byte, 32)) + // nonce := common.BytesToHash(make([]byte, 32)) txHashDeposited := common.HexToHash("c3a7") t.Run("IsPlaying - true", func(t *testing.T) { @@ -372,7 +372,6 @@ func TestRedistribution(t *testing.T) { transactionMock.New( transactionMock.WithCallFunc(func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error) { if *request.To == redistributionContractAddress { - return someSalt, nil } return nil, errors.New("unexpected call") diff --git a/pkg/storageincentives/redistributionstate_test.go b/pkg/storageincentives/redistributionstate_test.go index 38c3ae8280d..9e2930642a4 100644 --- a/pkg/storageincentives/redistributionstate_test.go +++ b/pkg/storageincentives/redistributionstate_test.go @@ -87,7 +87,6 @@ func TestState(t *testing.T) { if diff := cmp.Diff(want, *got, opt...); diff != "" { t.Errorf("result mismatch (-want +have):\n%s", diff) } - } func TestStateRoundData(t *testing.T) { @@ -155,7 +154,6 @@ func TestStateRoundData(t *testing.T) { t.Error("should be revealed") } }) - } func TestPurgeRoundData(t *testing.T) { diff --git a/pkg/storageincentives/staking/contract_test.go b/pkg/storageincentives/staking/contract_test.go index e032c4f2339..71762a569a1 100644 --- a/pkg/storageincentives/staking/contract_test.go +++ b/pkg/storageincentives/staking/contract_test.go @@ -77,7 +77,6 @@ func TestIsOverlayFrozen(t *testing.T) { if frozen { t.Fatalf("expected owner to not be frozen") } - }) } @@ -522,7 +521,6 @@ func TestDepositStake(t *testing.T) { } if *request.To == stakingContractAddress { return getPotentialStakeResponse(t, prevStake), nil - } return nil, errors.New("unexpected call") }), @@ -583,7 +581,6 @@ func TestDepositStake(t *testing.T) { } if *request.To == stakingContractAddress { return getPotentialStakeResponse(t, prevStake), nil - } return nil, errors.New("unexpected call") }), @@ -1609,7 +1606,6 @@ func TestMigrateStake(t *testing.T) { stakedAmount := big.NewInt(100000000000000000) t.Run("ok", func(t *testing.T) { - expectedCallDataForPaused, err := stakingContractABI.Pack("paused") if err != nil { t.Fatal(err) @@ -1790,7 +1786,6 @@ func TestMigrateStake(t *testing.T) { }) t.Run("tx reverted", func(t *testing.T) { - expectedCallDataForPaused, err := stakingContractABI.Pack("paused") if err != nil { t.Fatal(err) @@ -1855,7 +1850,6 @@ func TestMigrateStake(t *testing.T) { }) t.Run("is paused with err", func(t *testing.T) { - expectedCallDataForPaused, err := stakingContractABI.Pack("paused") if err != nil { t.Fatal(err) @@ -1890,7 +1884,6 @@ func TestMigrateStake(t *testing.T) { }) t.Run("get stake with err", func(t *testing.T) { - expectedCallDataForPaused, err := stakingContractABI.Pack("paused") if err != nil { t.Fatal(err) diff --git a/pkg/storageincentives/staking/export_test.go b/pkg/storageincentives/staking/export_test.go index 7e921cb08d0..55fd4332a71 100644 --- a/pkg/storageincentives/staking/export_test.go +++ b/pkg/storageincentives/staking/export_test.go @@ -4,6 +4,4 @@ package staking -var ( - Erc20ABI = erc20ABI -) +var Erc20ABI = erc20ABI diff --git a/pkg/storer/cachestore.go b/pkg/storer/cachestore.go index 9a894a8683c..84c150c0ec0 100644 --- a/pkg/storer/cachestore.go +++ b/pkg/storer/cachestore.go @@ -20,7 +20,6 @@ const ( ) func (db *DB) cacheWorker(ctx context.Context) { - defer db.inFlight.Done() overCapTrigger, overCapUnsub := db.events.Subscribe(cacheOverCapacity) @@ -114,7 +113,6 @@ func (db *DB) CacheShallowCopy(ctx context.Context, store transaction.Storage, a } func (db *DB) triggerCacheEviction() { - var ( size = db.cacheObj.Size() capc = db.cacheObj.Capacity() diff --git a/pkg/storer/cachestore_test.go b/pkg/storer/cachestore_test.go index c63733dc83d..c709312673a 100644 --- a/pkg/storer/cachestore_test.go +++ b/pkg/storer/cachestore_test.go @@ -94,7 +94,6 @@ func TestCacheStore(t *testing.T) { t.Parallel() testCacheStore(t, func() (*storer.DB, error) { - opts := dbTestOps(swarm.RandAddress(t), 100, nil, nil, time.Second) opts.CacheCapacity = 10 diff --git a/pkg/storer/debug_test.go b/pkg/storer/debug_test.go index 8e68b8ac972..17270a2a62f 100644 --- a/pkg/storer/debug_test.go +++ b/pkg/storer/debug_test.go @@ -194,7 +194,6 @@ func testDebugInfo(t *testing.T, newStorer func() (*storer.DB, swarm.Address, er t.Fatalf("invalid info (+want -have):\n%s", diff) } }) - } func TestDebugInfo(t *testing.T) { diff --git a/pkg/storer/internal/cache/cache.go b/pkg/storer/internal/cache/cache.go index bea87954ed8..6e30a56d6c3 100644 --- a/pkg/storer/internal/cache/cache.go +++ b/pkg/storer/internal/cache/cache.go @@ -74,7 +74,6 @@ func (c *Cache) Capacity() int64 { // chunkstore and also adds a Cache entry for the chunk. func (c *Cache) Putter(store transaction.Storage) storage.Putter { return storage.PutterFunc(func(ctx context.Context, chunk swarm.Chunk) error { - c.glock.Lock(chunk.Address().ByteString()) defer c.glock.Unlock(chunk.Address().ByteString()) @@ -127,7 +126,6 @@ func (c *Cache) Putter(store transaction.Storage) storage.Putter { // of this getter to rollback the operation. func (c *Cache) Getter(store transaction.Storage) storage.Getter { return storage.GetterFunc(func(ctx context.Context, address swarm.Address) (swarm.Chunk, error) { - c.glock.Lock(address.ByteString()) defer c.glock.Unlock(address.ByteString()) @@ -184,7 +182,6 @@ func (c *Cache) Getter(store transaction.Storage) storage.Getter { // RemoveOldest removes the oldest cache entries from the store. The count // specifies the number of entries to remove. func (c *Cache) RemoveOldest(ctx context.Context, st transaction.Storage, count uint64) error { - if count <= 0 { return nil } @@ -249,7 +246,6 @@ func (c *Cache) ShallowCopy( store transaction.Storage, addrs ...swarm.Address, ) (err error) { - // TODO: add proper mutex locking before usage entries := make([]*cacheEntry, 0, len(addrs)) @@ -286,7 +282,7 @@ func (c *Cache) ShallowCopy( return nil } - //consider only the amount that can fit, the rest should be deleted from the chunkstore. + // consider only the amount that can fit, the rest should be deleted from the chunkstore. if len(entries) > c.capacity { for _, addr := range entries[:len(entries)-c.capacity] { _ = store.Run(ctx, func(s transaction.Store) error { return s.ChunkStore().Delete(ctx, addr.Address) }) diff --git a/pkg/storer/internal/chunkstore/chunkstore.go b/pkg/storer/internal/chunkstore/chunkstore.go index bc2d7931681..6d2745cc5fd 100644 --- a/pkg/storer/internal/chunkstore/chunkstore.go +++ b/pkg/storer/internal/chunkstore/chunkstore.go @@ -187,7 +187,6 @@ func IterateLocations( ctx context.Context, st storage.Reader, ) <-chan LocationResult { - locationResultC := make(chan LocationResult) go func() { diff --git a/pkg/storer/internal/reserve/items.go b/pkg/storer/internal/reserve/items.go index a88aaae2e8b..e05b5c0bed4 100644 --- a/pkg/storer/internal/reserve/items.go +++ b/pkg/storer/internal/reserve/items.go @@ -57,7 +57,6 @@ func (b *BatchRadiusItem) Clone() storage.Item { const batchRadiusItemSize = 1 + swarm.HashSize + swarm.HashSize + 8 + swarm.HashSize func (b *BatchRadiusItem) Marshal() ([]byte, error) { - if b.Address.IsZero() { return nil, errMarshalInvalidAddress } @@ -83,7 +82,6 @@ func (b *BatchRadiusItem) Marshal() ([]byte, error) { } func (b *BatchRadiusItem) Unmarshal(buf []byte) error { - if len(buf) != batchRadiusItemSize { return errUnmarshalInvalidSize } @@ -152,7 +150,6 @@ func (c *ChunkBinItem) Clone() storage.Item { const chunkBinItemSize = 1 + 8 + swarm.HashSize + swarm.HashSize + 1 + swarm.HashSize func (c *ChunkBinItem) Marshal() ([]byte, error) { - if c.Address.IsZero() { return nil, errMarshalInvalidAddress } @@ -180,7 +177,6 @@ func (c *ChunkBinItem) Marshal() ([]byte, error) { } func (c *ChunkBinItem) Unmarshal(buf []byte) error { - if len(buf) != chunkBinItemSize { return errUnmarshalInvalidSize } @@ -222,6 +218,7 @@ func (b *BinItem) ID() string { func (c *BinItem) String() string { return path.Join(c.Namespace(), c.ID()) } + func (b *BinItem) Clone() storage.Item { if b == nil { return nil diff --git a/pkg/storer/internal/reserve/olditems.go b/pkg/storer/internal/reserve/olditems.go index 37bcdd9cc75..d92ad8e09ea 100644 --- a/pkg/storer/internal/reserve/olditems.go +++ b/pkg/storer/internal/reserve/olditems.go @@ -48,7 +48,6 @@ func (b *BatchRadiusItemV1) Clone() storage.Item { const batchRadiusItemSizeV1 = 1 + swarm.HashSize + swarm.HashSize + 8 func (b *BatchRadiusItemV1) Marshal() ([]byte, error) { - if b.Address.IsZero() { return nil, errMarshalInvalidAddress } @@ -72,7 +71,6 @@ func (b *BatchRadiusItemV1) Marshal() ([]byte, error) { } func (b *BatchRadiusItemV1) Unmarshal(buf []byte) error { - if len(buf) != batchRadiusItemSizeV1 { return errUnmarshalInvalidSize } @@ -130,7 +128,6 @@ func (c *ChunkBinItemV1) Clone() storage.Item { const chunkBinItemSizeV1 = 1 + 8 + swarm.HashSize + swarm.HashSize + 1 func (c *ChunkBinItemV1) Marshal() ([]byte, error) { - if c.Address.IsZero() { return nil, errMarshalInvalidAddress } @@ -156,7 +153,6 @@ func (c *ChunkBinItemV1) Marshal() ([]byte, error) { } func (c *ChunkBinItemV1) Unmarshal(buf []byte) error { - if len(buf) != chunkBinItemSizeV1 { return errUnmarshalInvalidSize } diff --git a/pkg/storer/internal/reserve/reserve.go b/pkg/storer/internal/reserve/reserve.go index e90fa27b999..5566946942e 100644 --- a/pkg/storer/internal/reserve/reserve.go +++ b/pkg/storer/internal/reserve/reserve.go @@ -99,7 +99,6 @@ func New( // 3. A new chunk that has the same address belonging to the same stamp index with an already stored chunk will overwrite the existing chunk // if the new chunk has a higher stamp timestamp (regardless of batch type and chunk type, eg CAC & SOC). func (r *Reserve) Put(ctx context.Context, chunk swarm.Chunk) error { - // batchID lock, Put vs Eviction r.multx.Lock(string(chunk.Stamp().BatchID())) defer r.multx.Unlock(string(chunk.Stamp().BatchID())) @@ -129,7 +128,6 @@ func (r *Reserve) Put(ctx context.Context, chunk swarm.Chunk) error { var shouldIncReserveSize bool err = r.st.Run(ctx, func(s transaction.Store) error { - oldStampIndex, loadedStampIndex, err := stampindex.LoadOrStore(s.IndexStore(), reserveScope, chunk) if err != nil { return fmt.Errorf("load or store stamp index for chunk %v has fail: %w", chunk, err) @@ -495,6 +493,47 @@ func RemoveChunkMetaData( ) } +// DeleteCorruptedChunkMetadata removes all reserve index entries for a chunk +// whose Sharky data was found to be corrupted during recovery. It is intended +// to be called from the recovery path, where only a storage.IndexStore (not a +// full transaction.Store) is available. If the chunk has no reserve metadata +// (e.g. it belongs to the upload store or cache), the function is a no-op. +func DeleteCorruptedChunkMetadata(store storage.IndexStore, baseAddr swarm.Address, addr swarm.Address) error { + stamp, err := chunkstamp.Load(store, reserveScope, addr) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil + } + return fmt.Errorf("load chunkstamp: %w", err) + } + + stampHash, err := stamp.Hash() + if err != nil { + return fmt.Errorf("compute stamp hash: %w", err) + } + + bin := swarm.Proximity(baseAddr.Bytes(), addr.Bytes()) + batchRadiusItem := &BatchRadiusItem{ + Bin: bin, + BatchID: stamp.BatchID(), + Address: addr, + StampHash: stampHash, + } + if err := store.Get(batchRadiusItem); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil + } + return fmt.Errorf("get batch radius item: %w", err) + } + + return errors.Join( + stampindex.Delete(store, reserveScope, stamp), + chunkstamp.DeleteWithStamp(store, reserveScope, addr, stamp), + store.Delete(batchRadiusItem), + store.Delete(&ChunkBinItem{Bin: bin, BinID: batchRadiusItem.BinID}), + ) +} + func (r *Reserve) IterateBin(bin uint8, startBinID uint64, cb func(swarm.Address, uint64, []byte, []byte) (bool, error)) error { err := r.st.IndexStore().Iterate(storage.Query{ Factory: func() storage.Item { return &ChunkBinItem{} }, diff --git a/pkg/storer/internal/reserve/reserve_test.go b/pkg/storer/internal/reserve/reserve_test.go index e0dbd6a4305..d67ec34916e 100644 --- a/pkg/storer/internal/reserve/reserve_test.go +++ b/pkg/storer/internal/reserve/reserve_test.go @@ -469,7 +469,6 @@ func TestSameChunkAddress(t *testing.T) { t.Fatalf("expected reserve size to increase by 2, got %d", size2-size1) } }) - } func TestReplaceOldIndex(t *testing.T) { diff --git a/pkg/storer/internal/transaction/transaction.go b/pkg/storer/internal/transaction/transaction.go index ae97e06ce3b..0f403580152 100644 --- a/pkg/storer/internal/transaction/transaction.go +++ b/pkg/storer/internal/transaction/transaction.go @@ -85,7 +85,6 @@ type transaction struct { // By design, it is best to not batch too many writes to a single transaction, including multiple chunks writes. // Calls made to the transaction are NOT thread-safe. func (s *store) NewTransaction(ctx context.Context) (Transaction, func()) { - b := s.bstore.Batch(ctx) index := &indexTrx{s.bstore, b, s.metrics} @@ -158,7 +157,6 @@ func (s *store) Close() error { } func (t *transaction) Commit() (err error) { - defer func() { t.metrics.MethodDuration.WithLabelValues("transaction", "success").Observe(time.Since(t.start).Seconds()) }() @@ -226,24 +224,28 @@ func (c *chunkStoreTrx) Get(ctx context.Context, addr swarm.Address) (ch swarm.C ch, err = chunkstore.Get(ctx, c.indexStore, c.sharkyTrx, addr) return ch, err } + func (c *chunkStoreTrx) Has(ctx context.Context, addr swarm.Address) (_ bool, err error) { defer handleMetric("chunkstore_has", c.metrics)(&err) unlock := c.lock(addr) defer unlock() return chunkstore.Has(ctx, c.indexStore, addr) } + func (c *chunkStoreTrx) Put(ctx context.Context, ch swarm.Chunk) (err error) { defer handleMetric("chunkstore_put", c.metrics)(&err) unlock := c.lock(ch.Address()) defer unlock() return chunkstore.Put(ctx, c.indexStore, c.sharkyTrx, ch) } + func (c *chunkStoreTrx) Delete(ctx context.Context, addr swarm.Address) (err error) { defer handleMetric("chunkstore_delete", c.metrics)(&err) unlock := c.lock(addr) defer unlock() return chunkstore.Delete(ctx, c.indexStore, c.sharkyTrx, addr) } + func (c *chunkStoreTrx) Iterate(ctx context.Context, fn storage.IterateChunkFn) (err error) { defer handleMetric("chunkstore_iterate", c.metrics)(&err) return chunkstore.Iterate(ctx, c.indexStore, c.sharkyTrx, fn) diff --git a/pkg/storer/internal/transaction/transaction_test.go b/pkg/storer/internal/transaction/transaction_test.go index 28008e20a9d..0fcfa42b5ef 100644 --- a/pkg/storer/internal/transaction/transaction_test.go +++ b/pkg/storer/internal/transaction/transaction_test.go @@ -26,7 +26,7 @@ type dirFS struct { } func (d *dirFS) Open(path string) (fs.File, error) { - return os.OpenFile(filepath.Join(d.basedir, path), os.O_RDWR|os.O_CREATE, 0644) + return os.OpenFile(filepath.Join(d.basedir, path), os.O_RDWR|os.O_CREATE, 0o644) } func Test_TransactionStorage(t *testing.T) { diff --git a/pkg/storer/internal/upload/uploadstore.go b/pkg/storer/internal/upload/uploadstore.go index b6943d1a9ac..51e99fa16d3 100644 --- a/pkg/storer/internal/upload/uploadstore.go +++ b/pkg/storer/internal/upload/uploadstore.go @@ -115,11 +115,9 @@ func (i pushItem) String() string { return storageutil.JoinFields(i.Namespace(), i.ID()) } -var ( - // errTagIDAddressItemUnmarshalInvalidSize is returned when trying - // to unmarshal buffer that is not of size tagItemSize. - errTagItemUnmarshalInvalidSize = errors.New("unmarshal TagItem: invalid size") -) +// errTagIDAddressItemUnmarshalInvalidSize is returned when trying +// to unmarshal buffer that is not of size tagItemSize. +var errTagItemUnmarshalInvalidSize = errors.New("unmarshal TagItem: invalid size") // tagItemSize is the size of a marshaled TagItem. const tagItemSize = swarm.HashSize + 7*8 @@ -565,7 +563,6 @@ func CleanupDirty(st transaction.Storage) error { // Report is the implementation of the PushReporter interface. func Report(ctx context.Context, st transaction.Store, chunk swarm.Chunk, state storage.ChunkState) error { - indexStore := st.IndexStore() ui := &uploadItem{Address: chunk.Address(), BatchID: chunk.Stamp().BatchID()} @@ -624,9 +621,7 @@ func Report(ctx context.Context, st transaction.Store, chunk swarm.Chunk, state return deleteFunc() } -var ( - errNextTagIDUnmarshalInvalidSize = errors.New("unmarshal nextTagID: invalid size") -) +var errNextTagIDUnmarshalInvalidSize = errors.New("unmarshal nextTagID: invalid size") // nextTagID is a storage.Item which stores a uint64 value in the store. type nextTagID uint64 diff --git a/pkg/storer/internal/upload/uploadstore_test.go b/pkg/storer/internal/upload/uploadstore_test.go index d932bb85868..5eaec5d2795 100644 --- a/pkg/storer/internal/upload/uploadstore_test.go +++ b/pkg/storer/internal/upload/uploadstore_test.go @@ -818,7 +818,6 @@ func TestChunkReporter(t *testing.T) { } func TestDeleteTagReporter(t *testing.T) { - t.Parallel() ts := newTestStorage(t) @@ -844,7 +843,6 @@ func TestDeleteTagReporter(t *testing.T) { } t.Run("delete tag while uploading", func(t *testing.T) { - chunk := chunktest.GenerateTestRandomChunks(1)[0] if err := ts.Run(context.Background(), func(s transaction.Store) error { @@ -924,7 +922,6 @@ func TestDeleteTagReporter(t *testing.T) { t.Fatalf("expected uploadItem to be deleted, got error: %v", err) } }) - } func TestNextTagID(t *testing.T) { diff --git a/pkg/storer/metrics.go b/pkg/storer/metrics.go index 68be82ac039..787e8a7c9aa 100644 --- a/pkg/storer/metrics.go +++ b/pkg/storer/metrics.go @@ -35,6 +35,7 @@ type metrics struct { ReserveSampleDuration *prometheus.HistogramVec ReserveSampleRunSummary *prometheus.GaugeVec ReserveSampleLastRunTimestamp prometheus.Gauge + RecoveryPrunedChunkCount prometheus.Counter } // newMetrics is a convenient constructor for creating new metrics. @@ -192,6 +193,14 @@ func newMetrics() metrics { Help: "Unix timestamp of the last ReserveSample run completion.", }, ), + RecoveryPrunedChunkCount: prometheus.NewCounter( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "recovery_pruned_chunk_count", + Help: "Number of corrupted chunks pruned from the index during sharky recovery.", + }, + ), } } diff --git a/pkg/storer/migration/refCntSize.go b/pkg/storer/migration/refCntSize.go index 2075c50effb..4fbfcfc6c09 100644 --- a/pkg/storer/migration/refCntSize.go +++ b/pkg/storer/migration/refCntSize.go @@ -102,7 +102,6 @@ func (r OldRetrievalIndexItem) String() string { func RefCountSizeInc(s storage.BatchStore, logger log.Logger) func() error { return func() error { - logger := logger.WithName("migration-RefCountSizeInc").Register() logger.Info("starting migration of replacing chunkstore items to increase refCnt capacity") @@ -129,7 +128,7 @@ func RefCountSizeInc(s storage.BatchStore, logger log.Logger) func() error { b := s.Batch(context.Background()) for _, item := range itemsToDelete[i:end] { - //create new + // create new err = b.Put(&chunkstore.RetrievalIndexItem{ Address: item.Address, Timestamp: item.Timestamp, diff --git a/pkg/storer/migration/reserveRepair.go b/pkg/storer/migration/reserveRepair.go index b8378d92550..b925ff063ef 100644 --- a/pkg/storer/migration/reserveRepair.go +++ b/pkg/storer/migration/reserveRepair.go @@ -176,9 +176,7 @@ func ReserveRepairer( for _, item := range batchRadiusItems { func(item *reserve.BatchRadiusItem) { eg.Go(func() error { - return st.Run(context.Background(), func(s transaction.Store) error { - chunk, err := s.ChunkStore().Get(context.Background(), item.Address) if err != nil { if errors.Is(err, storage.ErrNotFound) { diff --git a/pkg/storer/migration/reserveRepair_test.go b/pkg/storer/migration/reserveRepair_test.go index b60f51769eb..273d9555025 100644 --- a/pkg/storer/migration/reserveRepair_test.go +++ b/pkg/storer/migration/reserveRepair_test.go @@ -30,7 +30,7 @@ func TestReserveRepair(t *testing.T) { return swarm.ChunkTypeContentAddressed }, log.Noop) - var chunksPO = make([][]swarm.Chunk, 5) + chunksPO := make([][]swarm.Chunk, 5) var chunksPerPO uint64 = 2 for i := range swarm.MaxBins { diff --git a/pkg/storer/migration/step_02.go b/pkg/storer/migration/step_02.go index 7e2a087f51c..3730ab9d377 100644 --- a/pkg/storer/migration/step_02.go +++ b/pkg/storer/migration/step_02.go @@ -18,9 +18,7 @@ import ( // the old cacheEntry item has the same key, but the value is different. So only // a Put is needed. func step_02(st transaction.Storage) func() error { - return func() error { - trx, done := st.NewTransaction(context.Background()) defer done() @@ -52,5 +50,4 @@ func step_02(st transaction.Storage) func() error { return trx.Commit() } - } diff --git a/pkg/storer/migration/step_04_test.go b/pkg/storer/migration/step_04_test.go index fcf54225af4..6aea8e2edbe 100644 --- a/pkg/storer/migration/step_04_test.go +++ b/pkg/storer/migration/step_04_test.go @@ -27,7 +27,7 @@ type dirFS struct { } func (d *dirFS) Open(path string) (fs.File, error) { - return os.OpenFile(filepath.Join(d.basedir, path), os.O_RDWR|os.O_CREATE, 0644) + return os.OpenFile(filepath.Join(d.basedir, path), os.O_RDWR|os.O_CREATE, 0o644) } func Test_Step_04(t *testing.T) { diff --git a/pkg/storer/migration/step_05.go b/pkg/storer/migration/step_05.go index 94b23d9ef67..5bd0055e901 100644 --- a/pkg/storer/migration/step_05.go +++ b/pkg/storer/migration/step_05.go @@ -17,7 +17,6 @@ import ( // step_05 is a migration step that removes all upload items from the store. func step_05(st transaction.Storage, logger log.Logger) func() error { return func() error { - logger := logger.WithName("migration-step-05").Register() logger.Info("start removing upload items") @@ -53,5 +52,4 @@ func step_05(st transaction.Storage, logger log.Logger) func() error { logger.Info("finished removing upload items") return <-errC } - } diff --git a/pkg/storer/migration/step_06.go b/pkg/storer/migration/step_06.go index 4160e198b4c..0f477f40e72 100644 --- a/pkg/storer/migration/step_06.go +++ b/pkg/storer/migration/step_06.go @@ -39,7 +39,6 @@ func step_06(st transaction.Storage, logger log.Logger) func() error { } func addStampHash(logger log.Logger, st transaction.Storage) (int64, int64, error) { - preBatchRadiusCnt, err := st.IndexStore().Count(&reserve.BatchRadiusItemV1{}) if err != nil { return 0, 0, err @@ -229,7 +228,6 @@ func addStampHash(logger log.Logger, st transaction.Storage) (int64, int64, erro return false, nil }) - if err != nil { return 0, 0, errors.New("post-migration check: items fields not match. It's recommended that the nuke cmd is run to reset the node") } diff --git a/pkg/storer/mock/mockreserve.go b/pkg/storer/mock/mockreserve.go index 7ac74ee49ce..7e8dfc11625 100644 --- a/pkg/storer/mock/mockreserve.go +++ b/pkg/storer/mock/mockreserve.go @@ -146,6 +146,7 @@ func (s *ReserveStore) SetStorageRadius(r uint8) { s.radius = r s.mtx.Unlock() } + func (s *ReserveStore) CommittedDepth() uint8 { s.mtx.Lock() defer s.mtx.Unlock() diff --git a/pkg/storer/mock/mockstorer.go b/pkg/storer/mock/mockstorer.go index 2d851355714..ac431774d0a 100644 --- a/pkg/storer/mock/mockstorer.go +++ b/pkg/storer/mock/mockstorer.go @@ -196,16 +196,17 @@ func (m *mockStorer) Cache() storage.Putter { } func (m *mockStorer) DirectUpload() storer.PutterSession { - return &putterSession{chunkStore: storage.PutterFunc( - func(ctx context.Context, ch swarm.Chunk) error { - op := &pusher.Op{Chunk: ch, Err: make(chan error, 1), Direct: true} - select { - case <-ctx.Done(): - return ctx.Err() - case m.chunkPushC <- op: - return nil - } - }), + return &putterSession{ + chunkStore: storage.PutterFunc( + func(ctx context.Context, ch swarm.Chunk) error { + op := &pusher.Op{Chunk: ch, Err: make(chan error, 1), Direct: true} + select { + case <-ctx.Done(): + return ctx.Err() + case m.chunkPushC <- op: + return nil + } + }), } } diff --git a/pkg/storer/netstore.go b/pkg/storer/netstore.go index 1a192114920..e2becfb5027 100644 --- a/pkg/storer/netstore.go +++ b/pkg/storer/netstore.go @@ -82,7 +82,6 @@ func (db *DB) DirectUpload() PutterSession { func (db *DB) Download(cache bool) storage.Getter { return getterWithMetrics{ storage.GetterFunc(func(ctx context.Context, address swarm.Address) (ch swarm.Chunk, err error) { - span, logger, ctx := db.tracer.StartSpanFromContext(ctx, "get-chunk", db.logger) defer func() { if err != nil { diff --git a/pkg/storer/netstore_test.go b/pkg/storer/netstore_test.go index 9c073c09a20..2e0db6c11b7 100644 --- a/pkg/storer/netstore_test.go +++ b/pkg/storer/netstore_test.go @@ -326,7 +326,6 @@ func TestNetStore(t *testing.T) { t.Parallel() testNetStore(t, func(r retrieval.Interface) (*storer.DB, error) { - opts := dbTestOps(swarm.RandAddress(t), 0, nil, nil, time.Second) opts.CacheCapacity = 100 diff --git a/pkg/storer/recover.go b/pkg/storer/recover.go index 07d300e5d46..472b6a9e7e2 100644 --- a/pkg/storer/recover.go +++ b/pkg/storer/recover.go @@ -7,14 +7,19 @@ package storer import ( "context" "errors" + "fmt" "io/fs" "os" "path/filepath" "time" + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/sharky" + "github.com/ethersphere/bee/v2/pkg/soc" storage "github.com/ethersphere/bee/v2/pkg/storage" "github.com/ethersphere/bee/v2/pkg/storer/internal/chunkstore" + "github.com/ethersphere/bee/v2/pkg/storer/internal/reserve" "github.com/ethersphere/bee/v2/pkg/swarm" ) @@ -22,7 +27,7 @@ const ( sharkyDirtyFileName = ".DIRTY" ) -func sharkyRecovery(ctx context.Context, sharkyBasePath string, store storage.Store, opts *Options) (closerFn, error) { +func sharkyRecovery(ctx context.Context, sharkyBasePath string, store storage.Store, opts *Options) (closerFn, int, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -32,7 +37,7 @@ func sharkyRecovery(ctx context.Context, sharkyBasePath string, store storage.St closer := func() error { return os.Remove(dirtyFilePath) } if _, err := os.Stat(dirtyFilePath); errors.Is(err, fs.ErrNotExist) { - return closer, os.WriteFile(dirtyFilePath, []byte{}, 0644) + return closer, 0, os.WriteFile(dirtyFilePath, []byte{}, 0o644) } logger.Info("localstore sharky .DIRTY file exists: starting recovery due to previous dirty exit") @@ -42,7 +47,7 @@ func sharkyRecovery(ctx context.Context, sharkyBasePath string, store storage.St sharkyRecover, err := sharky.NewRecovery(sharkyBasePath, sharkyNoOfShards, swarm.SocMaxChunkSize) if err != nil { - return closer, err + return closer, 0, err } defer func() { @@ -51,29 +56,71 @@ func sharkyRecovery(ctx context.Context, sharkyBasePath string, store storage.St } }() - c := chunkstore.IterateLocations(ctx, store) - - if err := addLocations(c, sharkyRecover); err != nil { - return closer, err + pruned, err := validateAndAddLocations(ctx, store, sharkyRecover, opts.Address, logger) + if err != nil { + return closer, 0, err } - return closer, nil + return closer, pruned, nil } -func addLocations(locationResultC <-chan chunkstore.LocationResult, sharkyRecover *sharky.Recovery) error { - for res := range locationResultC { - if res.Err != nil { - return res.Err +// validateAndAddLocations iterates every chunk index entry, reads its data from +// Sharky, and validates the content hash. Valid chunks are registered with the +// recovery so their slots are preserved. Corrupted entries (unreadable data or +// hash mismatch) are logged, excluded from the recovery bitmap, and deleted from +// the index store — including all associated reserve metadata (BatchRadiusItem, +// ChunkBinItem, stampindex, chunkstamp) — so the node starts clean without +// serving invalid data and with correct reserve size accounting. +// If a corrupted index entry cannot be deleted, an error is returned and the +// node startup is aborted to prevent serving or operating on corrupt state. +// It returns the number of corrupted entries that were pruned. +func validateAndAddLocations(ctx context.Context, store storage.Store, sharkyRecover *sharky.Recovery, baseAddr swarm.Address, logger log.Logger) (int, error) { + var corrupted []*chunkstore.RetrievalIndexItem + + buf := make([]byte, swarm.SocMaxChunkSize) + + err := chunkstore.IterateItems(store, func(item *chunkstore.RetrievalIndexItem) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + if err := sharkyRecover.Read(ctx, item.Location, buf[:item.Location.Length]); err != nil { + logger.Warning("recovery: unreadable chunk, marking corrupted", "address", item.Address, "err", err) + corrupted = append(corrupted, item) + return nil } - if err := sharkyRecover.Add(res.Location); err != nil { - return err + ch := swarm.NewChunk(item.Address, buf[:item.Location.Length]) + if !cac.Valid(ch) && !soc.Valid(ch) { + logger.Warning("recovery: invalid chunk hash, marking corrupted", "address", item.Address) + corrupted = append(corrupted, item) + return nil } + + return sharkyRecover.Add(item.Location) + }) + if err != nil { + return 0, fmt.Errorf("recovery: failed iterating chunk index: %w", err) } if err := sharkyRecover.Save(); err != nil { - return err + return 0, fmt.Errorf("recovery: failed saving sharky recovery state: %w", err) + } + + for _, item := range corrupted { + if err := reserve.DeleteCorruptedChunkMetadata(store, baseAddr, item.Address); err != nil { + return 0, fmt.Errorf("recovery: failed deleting corrupted chunk metadata %s: %w", item.Address, err) + } + if err := store.Delete(item); err != nil { + return 0, fmt.Errorf("recovery: failed deleting corrupted chunk index %s: %w", item.Address, err) + } + } + + if len(corrupted) > 0 { + logger.Warning("recovery: removed corrupted chunk index entries", "count", len(corrupted)) } - return nil + return len(corrupted), nil } diff --git a/pkg/storer/recover_test.go b/pkg/storer/recover_test.go new file mode 100644 index 00000000000..8bf00bb4fed --- /dev/null +++ b/pkg/storer/recover_test.go @@ -0,0 +1,212 @@ +// 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 storer_test + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing" + pullerMock "github.com/ethersphere/bee/v2/pkg/puller/mock" + "github.com/ethersphere/bee/v2/pkg/sharky" + "github.com/ethersphere/bee/v2/pkg/storage" + chunk "github.com/ethersphere/bee/v2/pkg/storage/testing" + "github.com/ethersphere/bee/v2/pkg/storer" + "github.com/ethersphere/bee/v2/pkg/storer/internal/chunkstore" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +// TestRecoveryPrunesCorruptedChunks verifies that on restart after an unclean +// shutdown, validateAndAddLocations removes index entries whose Sharky data no +// longer validates (hash mismatch), while leaving intact entries unaffected. +func TestRecoveryPrunesCorruptedChunks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + basePath := t.TempDir() + opts := dbTestOps(swarm.RandAddress(t), 1000, nil, nil, time.Minute) + + batch := postagetesting.MustNewBatch() + + // 1. Open the storer and write two chunks. + st, err := storer.New(ctx, basePath, opts) + if err != nil { + t.Fatalf("New: %v", err) + } + readyC := make(chan struct{}) + st.StartReserveWorker(ctx, pullerMock.NewMockRateReporter(0), networkRadiusFunc(0), readyC) + <-readyC + + goodChunk := chunk.GenerateTestRandomChunk().WithStamp(postagetesting.MustNewBatchStamp(batch.ID)) + badChunk := chunk.GenerateTestRandomChunk().WithStamp(postagetesting.MustNewBatchStamp(batch.ID)) + + putter := st.ReservePutter() + if err := putter.Put(ctx, goodChunk); err != nil { + t.Fatalf("Put good chunk: %v", err) + } + if err := putter.Put(ctx, badChunk); err != nil { + t.Fatalf("Put bad chunk: %v", err) + } + + // 2. Locate the bad chunk's Sharky slot before closing. + var badLoc sharky.Location + if err := st.Storage().IndexStore().Iterate(storage.Query{ + Factory: func() storage.Item { return new(chunkstore.RetrievalIndexItem) }, + }, func(r storage.Result) (bool, error) { + item := r.Entry.(*chunkstore.RetrievalIndexItem) + if item.Address.Equal(badChunk.Address()) { + badLoc = item.Location + } + return false, nil + }); err != nil { + t.Fatalf("Iterate index: %v", err) + } + if badLoc.Length == 0 { + t.Fatal("bad chunk not found in index") + } + + // 3. Close cleanly (removes .DIRTY). + if err := st.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + // 4. Simulate unclean shutdown: recreate .DIRTY so recovery runs on next open. + dirtyPath := filepath.Join(basePath, "sharky", ".DIRTY") + if err := os.WriteFile(dirtyPath, []byte{}, 0o644); err != nil { + t.Fatalf("WriteFile .DIRTY: %v", err) + } + + // 5. Overwrite the bad chunk's slot with zeros so its hash will not validate. + shardPath := filepath.Join(basePath, "sharky", fmt.Sprintf("shard_%03d", badLoc.Shard)) + f, err := os.OpenFile(shardPath, os.O_RDWR, 0o666) + if err != nil { + t.Fatalf("OpenFile shard: %v", err) + } + if _, err := f.WriteAt(make([]byte, badLoc.Length), int64(badLoc.Slot)*int64(swarm.SocMaxChunkSize)); err != nil { + _ = f.Close() + t.Fatalf("WriteAt zeros: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("Close shard: %v", err) + } + + // 6. Reopen the storer. The .DIRTY file triggers sharkyRecovery → + // validateAndAddLocations, which prunes the corrupted index entry. + st2, err := storer.New(ctx, basePath, opts) + if err != nil { + t.Fatalf("New (reopen): %v", err) + } + t.Cleanup(func() { _ = st2.Close() }) + + // 7. The valid chunk must still be retrievable after recovery. + got, err := st2.Storage().ChunkStore().Get(ctx, goodChunk.Address()) + if err != nil { + t.Fatalf("Get good chunk after recovery: %v", err) + } + if !got.Address().Equal(goodChunk.Address()) { + t.Fatalf("good chunk address mismatch after recovery") + } + + // 8. The corrupted chunk must have been pruned from the index. + _, err = st2.Storage().ChunkStore().Get(ctx, badChunk.Address()) + if !errors.Is(err, storage.ErrNotFound) { + t.Fatalf("expected ErrNotFound for corrupted chunk, got: %v", err) + } +} + +// TestRecoveryPrunesUnreadableChunks verifies that on restart after an unclean +// shutdown, validateAndAddLocations removes index entries whose Sharky slot +// cannot be read at all (Read error path), while leaving intact entries unaffected. +func TestRecoveryPrunesUnreadableChunks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + basePath := t.TempDir() + opts := dbTestOps(swarm.RandAddress(t), 1000, nil, nil, time.Minute) + + batch := postagetesting.MustNewBatch() + + // 1. Open the storer and write two chunks. + st, err := storer.New(ctx, basePath, opts) + if err != nil { + t.Fatalf("New: %v", err) + } + readyC := make(chan struct{}) + st.StartReserveWorker(ctx, pullerMock.NewMockRateReporter(0), networkRadiusFunc(0), readyC) + <-readyC + + goodChunk := chunk.GenerateTestRandomChunk().WithStamp(postagetesting.MustNewBatchStamp(batch.ID)) + badChunk := chunk.GenerateTestRandomChunk().WithStamp(postagetesting.MustNewBatchStamp(batch.ID)) + + putter := st.ReservePutter() + if err := putter.Put(ctx, goodChunk); err != nil { + t.Fatalf("Put good chunk: %v", err) + } + if err := putter.Put(ctx, badChunk); err != nil { + t.Fatalf("Put bad chunk: %v", err) + } + + // 2. Locate the bad chunk's Sharky slot before closing. + var badLoc sharky.Location + if err := st.Storage().IndexStore().Iterate(storage.Query{ + Factory: func() storage.Item { return new(chunkstore.RetrievalIndexItem) }, + }, func(r storage.Result) (bool, error) { + item := r.Entry.(*chunkstore.RetrievalIndexItem) + if item.Address.Equal(badChunk.Address()) { + badLoc = item.Location + } + return false, nil + }); err != nil { + t.Fatalf("Iterate index: %v", err) + } + if badLoc.Length == 0 { + t.Fatal("bad chunk not found in index") + } + + // 3. Close cleanly (removes .DIRTY). + if err := st.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + // 4. Simulate unclean shutdown: recreate .DIRTY so recovery runs on next open. + dirtyPath := filepath.Join(basePath, "sharky", ".DIRTY") + if err := os.WriteFile(dirtyPath, []byte{}, 0o644); err != nil { + t.Fatalf("WriteFile .DIRTY: %v", err) + } + + // 5. Truncate the shard file to the start of the bad chunk's slot so that + // ReadAt returns EOF — simulating data that was never flushed to disk. + shardPath := filepath.Join(basePath, "sharky", fmt.Sprintf("shard_%03d", badLoc.Shard)) + if err := os.Truncate(shardPath, int64(badLoc.Slot)*int64(swarm.SocMaxChunkSize)); err != nil { + t.Fatalf("Truncate shard: %v", err) + } + + // 6. Reopen the storer. Recovery must detect the unreadable slot and prune it. + st2, err := storer.New(ctx, basePath, opts) + if err != nil { + t.Fatalf("New (reopen): %v", err) + } + t.Cleanup(func() { _ = st2.Close() }) + + // 7. The valid chunk must still be retrievable after recovery. + got, err := st2.Storage().ChunkStore().Get(ctx, goodChunk.Address()) + if err != nil { + t.Fatalf("Get good chunk after recovery: %v", err) + } + if !got.Address().Equal(goodChunk.Address()) { + t.Fatalf("good chunk address mismatch after recovery") + } + + // 8. The unreadable chunk must have been pruned from the index. + _, err = st2.Storage().ChunkStore().Get(ctx, badChunk.Address()) + if !errors.Is(err, storage.ErrNotFound) { + t.Fatalf("expected ErrNotFound for unreadable chunk, got: %v", err) + } +} diff --git a/pkg/storer/reserve_test.go b/pkg/storer/reserve_test.go index 4891781da15..4c83df534dc 100644 --- a/pkg/storer/reserve_test.go +++ b/pkg/storer/reserve_test.go @@ -540,7 +540,7 @@ func TestSubscribeBin(t *testing.T) { t.Helper() var ( chunksPerPO uint64 = 50 - chunks = make([]swarm.Chunk, 0, int(chunksPerPO)*2) + chunks = make([]swarm.Chunk, 0, chunksPerPO*2) putter = storer.ReservePutter() ) diff --git a/pkg/storer/sample_test.go b/pkg/storer/sample_test.go index c1a34ddec73..ccc01946276 100644 --- a/pkg/storer/sample_test.go +++ b/pkg/storer/sample_test.go @@ -430,9 +430,7 @@ func BenchmarkReserveSample1k(b *testing.B) { anchor = swarm.RandAddressAt(b, baseAddr, int(radius)).Bytes() ) - b.ResetTimer() - - for range b.N { + for b.Loop() { _, err := st.ReserveSample(context.TODO(), anchor, radius, timeVar, nil) if err != nil { b.Fatal(err) diff --git a/pkg/storer/storer.go b/pkg/storer/storer.go index 6425213ca76..20711d91e42 100644 --- a/pkg/storer/storer.go +++ b/pkg/storer/storer.go @@ -282,15 +282,15 @@ func initDiskRepository( ctx context.Context, basePath string, opts *Options, -) (transaction.Storage, *PinIntegrity, io.Closer, error) { +) (transaction.Storage, *PinIntegrity, io.Closer, int, error) { store, err := initStore(basePath, opts) if err != nil { - return nil, nil, nil, fmt.Errorf("failed creating levelDB index store: %w", err) + return nil, nil, nil, 0, fmt.Errorf("failed creating levelDB index store: %w", err) } err = migration.Migrate(store, "core-migration", localmigration.BeforeInitSteps(store, opts.Logger)) if err != nil { - return nil, nil, nil, errors.Join(store.Close(), fmt.Errorf("failed core migration: %w", err)) + return nil, nil, nil, 0, errors.Join(store.Close(), fmt.Errorf("failed core migration: %w", err)) } if opts.LdbStats.Load() != nil { @@ -342,13 +342,13 @@ func initDiskRepository( if _, err := os.Stat(sharkyBasePath); os.IsNotExist(err) { err := os.Mkdir(sharkyBasePath, 0o777) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, 0, err } } - recoveryCloser, err := sharkyRecovery(ctx, sharkyBasePath, store, opts) + recoveryCloser, pruned, err := sharkyRecovery(ctx, sharkyBasePath, store, opts) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to recover sharky: %w", err) + return nil, nil, nil, 0, fmt.Errorf("failed to recover sharky: %w", err) } sharky, err := sharky.New( @@ -357,7 +357,7 @@ func initDiskRepository( swarm.SocMaxChunkSize, ) if err != nil { - return nil, nil, nil, fmt.Errorf("failed creating sharky instance: %w", err) + return nil, nil, nil, 0, fmt.Errorf("failed creating sharky instance: %w", err) } pinIntegrity := &PinIntegrity{ @@ -365,7 +365,7 @@ func initDiskRepository( Sharky: sharky, } - return transaction.NewStorage(sharky, store), pinIntegrity, closer(store, sharky, recoveryCloser), nil + return transaction.NewStorage(sharky, store), pinIntegrity, closer(store, sharky, recoveryCloser), pruned, nil } const lockKeyNewSession string = "new_session" @@ -469,6 +469,7 @@ func New(ctx context.Context, dirPath string, opts *Options) (*DB, error) { pinIntegrity *PinIntegrity st transaction.Storage dbCloser io.Closer + pruned int ) if opts == nil { opts = defaultOptions() @@ -488,7 +489,7 @@ func New(ctx context.Context, dirPath string, opts *Options) (*DB, error) { return nil, err } } else { - st, pinIntegrity, dbCloser, err = initDiskRepository(ctx, dirPath, opts) + st, pinIntegrity, dbCloser, pruned, err = initDiskRepository(ctx, dirPath, opts) if err != nil { return nil, err } @@ -578,6 +579,9 @@ func New(ctx context.Context, dirPath string, opts *Options) (*DB, error) { db.metrics.ReserveSize.Set(float64(rs.Size())) } db.metrics.CacheSize.Set(float64(db.cacheObj.Size())) + if pruned > 0 { + db.metrics.RecoveryPrunedChunkCount.Add(float64(pruned)) + } // Cleanup any dirty state in upload and pinning stores, this could happen // in case of dirty shutdowns diff --git a/pkg/storer/subscribe_push.go b/pkg/storer/subscribe_push.go index 237b155b66c..dcd4a201e83 100644 --- a/pkg/storer/subscribe_push.go +++ b/pkg/storer/subscribe_push.go @@ -24,7 +24,6 @@ func (db *DB) SubscribePush(ctx context.Context) (<-chan swarm.Chunk, func()) { ) db.subscriptionsWG.Go(func() { - trigger, unsub := db.events.Subscribe(subscribePushEventKey) defer unsub() @@ -47,7 +46,6 @@ func (db *DB) SubscribePush(ctx context.Context) (<-chan swarm.Chunk, func()) { return true, ctx.Err() } }) - if err != nil { // if we get storage.ErrNotFound, it could happen that the previous // iteration happened on a snapshot that was not fully updated yet. diff --git a/pkg/storer/uploadstore.go b/pkg/storer/uploadstore.go index f4f21fc59ba..e4cabe3c243 100644 --- a/pkg/storer/uploadstore.go +++ b/pkg/storer/uploadstore.go @@ -23,7 +23,6 @@ const uploadsLock = "pin-upload-store" // Report implements the storage.PushReporter by wrapping the internal reporter // with a transaction. func (db *DB) Report(ctx context.Context, chunk swarm.Chunk, state storage.ChunkState) error { - unlock := db.Lock(uploadsLock) defer unlock() @@ -63,7 +62,6 @@ func (db *DB) Upload(ctx context.Context, pin bool, tagID uint64) (PutterSession } return nil }) - if err != nil { return nil, err } diff --git a/pkg/storer/validate.go b/pkg/storer/validate.go index 01f6904d02c..d4d0958a12b 100644 --- a/pkg/storer/validate.go +++ b/pkg/storer/validate.go @@ -10,9 +10,8 @@ import ( "os" "path" "sync" - "time" - "sync/atomic" + "time" "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/log" @@ -26,7 +25,6 @@ import ( // Validate ensures that all retrievalIndex chunks are correctly stored in sharky. func ValidateReserve(ctx context.Context, basePath string, opts *Options) error { - logger := opts.Logger store, err := initStore(basePath, opts) @@ -59,7 +57,6 @@ func ValidateReserve(ctx context.Context, basePath string, opts *Options) error // ValidateRetrievalIndex ensures that all retrievalIndex chunks are correctly stored in sharky. func ValidateRetrievalIndex(ctx context.Context, basePath string, opts *Options) error { - logger := opts.Logger store, err := initStore(basePath, opts) @@ -90,7 +87,6 @@ func ValidateRetrievalIndex(ctx context.Context, basePath string, opts *Options) } func validateWork(logger log.Logger, store storage.Store, readFn func(context.Context, sharky.Location, []byte) error) { - total := 0 socCount := 0 invalidCount := 0 @@ -226,7 +222,7 @@ func ValidatePinCollectionChunks(ctx context.Context, basePath, pin, location st location = path.Join(fileLoc, fileName) - f, err := os.OpenFile(location, os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(location, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open output file for writing: %w", err) } @@ -237,7 +233,7 @@ func ValidatePinCollectionChunks(ctx context.Context, basePath, pin, location st defer f.Close() - var ch = make(chan PinStat) + ch := make(chan PinStat) go pv.Check(ctx, logger, pin, ch) for st := range ch { @@ -323,9 +319,7 @@ func (p *PinIntegrity) Check(ctx context.Context, logger log.Logger, pin string, for _, pin := range pins { var wg sync.WaitGroup - var ( - total, missing, invalid atomic.Int32 - ) + var total, missing, invalid atomic.Int32 iteratateItemsC := make(chan *chunkstore.RetrievalIndexItem) @@ -392,7 +386,8 @@ func (p *PinIntegrity) Check(ctx context.Context, logger log.Logger, pin string, Ref: pin, Total: int(total.Load()), Missing: int(missing.Load()), - Invalid: int(invalid.Load())}: + Invalid: int(invalid.Load()), + }: } } } diff --git a/pkg/swarm/swarm.go b/pkg/swarm/swarm.go index 1b2e83b2ed7..4086e17f227 100644 --- a/pkg/swarm/swarm.go +++ b/pkg/swarm/swarm.go @@ -33,15 +33,11 @@ const ( SocMaxChunkSize = SocMinChunkSize + ChunkSize ) -var ( - ErrInvalidChunk = errors.New("invalid chunk") -) +var ErrInvalidChunk = errors.New("invalid chunk") -var ( - // Ethereum Address for SOC owner of Dispersed Replicas - // generated from private key 0x0100000000000000000000000000000000000000000000000000000000000000 - ReplicasOwner, _ = hex.DecodeString("dc5b20847f43d67928f49cd4f85d696b5a7617b5") -) +// Ethereum Address for SOC owner of Dispersed Replicas +// generated from private key 0x0100000000000000000000000000000000000000000000000000000000000000 +var ReplicasOwner, _ = hex.DecodeString("dc5b20847f43d67928f49cd4f85d696b5a7617b5") var ( // EmptyAddress is the address that is all zeroes. @@ -297,7 +293,6 @@ var errBadCharacter = errors.New("bad character in binary address") // ParseBitStrAddress parses overlay addresses in binary format (eg: 111101101) to it's corresponding overlay address. func ParseBitStrAddress(src string) (Address, error) { - bitPos := 7 b := uint8(0) @@ -361,7 +356,6 @@ func (n Neighborhood) Clone() Neighborhood { } func bitStr(src []byte, bits uint8) string { - ret := "" for _, b := range src { diff --git a/pkg/swarm/test_helpers_test.go b/pkg/swarm/test_helpers_test.go index 11d88842681..c856d2ffb6c 100644 --- a/pkg/swarm/test_helpers_test.go +++ b/pkg/swarm/test_helpers_test.go @@ -37,7 +37,7 @@ func Test_RandAddressAt(t *testing.T) { hw1 := []byte{b1[0], b1[1], 0, 0} // highest words of 1 hw1int := binary.BigEndian.Uint32(hw1) - //bb0 is the bit mask to AND with hw0 and hw1 + // bb0 is the bit mask to AND with hw0 and hw1 bb0 := uint32(0) for i := range bitsInCommon { bb0 |= (1 << (31 - i)) diff --git a/pkg/topology/kademlia/export_test.go b/pkg/topology/kademlia/export_test.go index 3c5ad3e1390..69a354cbac5 100644 --- a/pkg/topology/kademlia/export_test.go +++ b/pkg/topology/kademlia/export_test.go @@ -23,8 +23,10 @@ const ( DefaultOverSaturationPeers = defaultOverSaturationPeers ) -type PeerExcludeFunc = peerExcludeFunc -type ExcludeFunc = excludeFunc +type ( + PeerExcludeFunc = peerExcludeFunc + ExcludeFunc = excludeFunc +) func (k *Kad) IsWithinConnectionDepth(addr swarm.Address) bool { return swarm.Proximity(k.base.Bytes(), addr.Bytes()) >= k.ConnectionDepth() diff --git a/pkg/topology/kademlia/internal/metrics/metrics_test.go b/pkg/topology/kademlia/internal/metrics/metrics_test.go index 4fb7e33023f..47884d33947 100644 --- a/pkg/topology/kademlia/internal/metrics/metrics_test.go +++ b/pkg/topology/kademlia/internal/metrics/metrics_test.go @@ -221,7 +221,7 @@ func TestExclude(t *testing.T) { t.Fatal(err) } - var addr = swarm.RandAddress(t) + addr := swarm.RandAddress(t) // record unhealthy, unreachable, bootnode mc.Record(addr, metrics.PeerHealth(false), metrics.IsBootnode(true), metrics.PeerReachability(p2p.ReachabilityStatusPrivate)) diff --git a/pkg/topology/kademlia/internal/waitnext/waitnext.go b/pkg/topology/kademlia/internal/waitnext/waitnext.go index 9a93f274eba..ee10fd8363e 100644 --- a/pkg/topology/kademlia/internal/waitnext/waitnext.go +++ b/pkg/topology/kademlia/internal/waitnext/waitnext.go @@ -30,7 +30,6 @@ func New() *WaitNext { } func (r *WaitNext) Set(addr swarm.Address, tryAfter time.Time, attempts int) { - r.Lock() defer r.Unlock() @@ -38,7 +37,6 @@ func (r *WaitNext) Set(addr swarm.Address, tryAfter time.Time, attempts int) { } func (r *WaitNext) SetTryAfter(addr swarm.Address, tryAfter time.Time) { - r.Lock() defer r.Unlock() @@ -50,7 +48,6 @@ func (r *WaitNext) SetTryAfter(addr swarm.Address, tryAfter time.Time) { } func (r *WaitNext) Waiting(addr swarm.Address) bool { - r.Lock() defer r.Unlock() @@ -59,7 +56,6 @@ func (r *WaitNext) Waiting(addr swarm.Address) bool { } func (r *WaitNext) Attempts(addr swarm.Address) int { - r.Lock() defer r.Unlock() @@ -71,7 +67,6 @@ func (r *WaitNext) Attempts(addr swarm.Address) int { } func (r *WaitNext) Remove(addr swarm.Address) { - r.Lock() defer r.Unlock() diff --git a/pkg/topology/kademlia/kademlia.go b/pkg/topology/kademlia/kademlia.go index 14c4c71ef92..dac26ab2acd 100644 --- a/pkg/topology/kademlia/kademlia.go +++ b/pkg/topology/kademlia/kademlia.go @@ -991,7 +991,7 @@ func (k *Kad) connect(ctx context.Context, peer swarm.Address, ma []ma.Multiaddr case errors.Is(err, p2p.ErrUnsupportedAddresses): return err case err != nil: - k.logger.Info("could not connect to peer", "peer_address", peer, "error", err) + k.logger.Debug("could not connect to peer", "peer_address", peer, "error", err) retryTime := time.Now().Add(k.opt.TimeToRetry) var e *p2p.ConnectionBackoffError diff --git a/pkg/topology/lightnode/metrics.go b/pkg/topology/lightnode/metrics.go index cea1504f8a9..9e4ee6bc0d5 100644 --- a/pkg/topology/lightnode/metrics.go +++ b/pkg/topology/lightnode/metrics.go @@ -31,7 +31,8 @@ func newMetrics() metrics { Subsystem: subsystem, Name: "currently_disconnected_peers", Help: "Number of currently disconnected peers.", - })} + }), + } } // Metrics returns set of prometheus collectors. diff --git a/pkg/topology/pslice/pslice.go b/pkg/topology/pslice/pslice.go index ca1b8e5c4c3..3b98ad3c1fb 100644 --- a/pkg/topology/pslice/pslice.go +++ b/pkg/topology/pslice/pslice.go @@ -79,7 +79,6 @@ func (s *PSlice) Add(addrs ...swarm.Address) { // iterates over all peers from deepest bin to shallowest. func (s *PSlice) EachBin(pf topology.EachPeerFunc) error { - for i := s.maxBins - 1; i >= 0; i-- { s.mu.RLock() @@ -105,7 +104,6 @@ func (s *PSlice) EachBin(pf topology.EachPeerFunc) error { // EachBinRev iterates over all peers from shallowest bin to deepest. func (s *PSlice) EachBinRev(pf topology.EachPeerFunc) error { - for i := 0; i < s.maxBins; i++ { s.mu.RLock() @@ -130,7 +128,6 @@ func (s *PSlice) EachBinRev(pf topology.EachPeerFunc) error { } func (s *PSlice) BinSize(bin uint8) int { - if int(bin) >= s.maxBins { return 0 } @@ -142,7 +139,6 @@ func (s *PSlice) BinSize(bin uint8) int { } func (s *PSlice) BinPeers(bin uint8) []swarm.Address { - if int(bin) >= s.maxBins { return nil } @@ -177,11 +173,9 @@ func (s *PSlice) ShallowestEmpty() (uint8, bool) { defer s.mu.RUnlock() for i, peers := range s.peers { - if len(peers) == 0 { return uint8(i), false } - } return 0, true diff --git a/pkg/transaction/event_test.go b/pkg/transaction/event_test.go index 6760a805ccb..769499d7a0b 100644 --- a/pkg/transaction/event_test.go +++ b/pkg/transaction/event_test.go @@ -16,9 +16,7 @@ import ( "github.com/ethersphere/go-sw3-abi/sw3abi" ) -var ( - erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_9) -) +var erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_9) type transferEvent struct { From common.Address diff --git a/pkg/transaction/export_test.go b/pkg/transaction/export_test.go index 5ead1d78865..3d723b4fb19 100644 --- a/pkg/transaction/export_test.go +++ b/pkg/transaction/export_test.go @@ -4,6 +4,4 @@ package transaction -var ( - StoredTransactionKey = storedTransactionKey -) +var StoredTransactionKey = storedTransactionKey diff --git a/pkg/transaction/monitor.go b/pkg/transaction/monitor.go index 806f24e6cc6..e5e37a83a84 100644 --- a/pkg/transaction/monitor.go +++ b/pkg/transaction/monitor.go @@ -18,8 +18,10 @@ import ( "github.com/ethersphere/bee/v2/pkg/log" ) -var ErrTransactionCancelled = errors.New("transaction cancelled") -var ErrMonitorClosed = errors.New("monitor closed") +var ( + ErrTransactionCancelled = errors.New("transaction cancelled") + ErrMonitorClosed = errors.New("monitor closed") +) // Monitor is a nonce-based watcher for transaction confirmations. // Instead of watching transactions individually, the senders nonce is monitored and transactions are checked based on this. diff --git a/pkg/transaction/monitor_test.go b/pkg/transaction/monitor_test.go index 7b88e84629f..429a30bba95 100644 --- a/pkg/transaction/monitor_test.go +++ b/pkg/transaction/monitor_test.go @@ -387,5 +387,4 @@ func TestMonitorWatchTransaction(t *testing.T) { t.Fatal("timed out") } }) - } diff --git a/pkg/transaction/transaction.go b/pkg/transaction/transaction.go index e03dc003ce9..68fc74d6f53 100644 --- a/pkg/transaction/transaction.go +++ b/pkg/transaction/transaction.go @@ -6,6 +6,7 @@ package transaction import ( "bytes" + "context" "errors" "fmt" "io" @@ -14,8 +15,6 @@ import ( "sync" "time" - "context" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -164,9 +163,9 @@ func (t *transactionService) waitForAllPendingTx() error { return err } - pendingTxs = t.filterPendingTransactions(t.ctx, pendingTxs) + pending := t.filterPendingTransactions(t.ctx, pendingTxs) - for _, txHash := range pendingTxs { + for txHash := range pending { t.waitForPendingTx(txHash) } @@ -392,18 +391,16 @@ func (t *transactionService) nextNonce(ctx context.Context) (uint64, error) { return 0, err } - pendingTxs = t.filterPendingTransactions(t.ctx, pendingTxs) + pending := t.filterPendingTransactions(t.ctx, pendingTxs) // PendingNonceAt returns the nonce we should use, but we will // compare this to our pending tx list, therefore the -1. maxNonce := onchainNonce - 1 - for _, txHash := range pendingTxs { - trx, _, err := t.backend.TransactionByHash(ctx, txHash) - if err != nil { - t.logger.Error(err, "pending transaction not found", "tx", txHash) - return 0, err + for txHash, trx := range pending { + if trx == nil { + t.logger.Warning("pending transaction data unavailable, relying on onchain nonce", "tx", txHash) + continue } - maxNonce = max(maxNonce, trx.Nonce()) } @@ -457,11 +454,12 @@ func (t *transactionService) PendingTransactions() ([]common.Hash, error) { // filterPendingTransactions will filter supplied transaction hashes removing those that are not pending anymore. // Removed transactions will be also removed from store. -func (t *transactionService) filterPendingTransactions(ctx context.Context, txHashes []common.Hash) []common.Hash { - result := make([]common.Hash, 0, len(txHashes)) +// Returns the pending transactions keyed by hash. +func (t *transactionService) filterPendingTransactions(ctx context.Context, txHashes []common.Hash) map[common.Hash]*types.Transaction { + result := make(map[common.Hash]*types.Transaction, len(txHashes)) for _, txHash := range txHashes { - _, isPending, err := t.backend.TransactionByHash(ctx, txHash) + trx, isPending, err := t.backend.TransactionByHash(ctx, txHash) // When error occurres consider transaction as pending (so this transaction won't be filtered out), // unless it was not found if err != nil { @@ -475,7 +473,7 @@ func (t *transactionService) filterPendingTransactions(ctx context.Context, txHa } if isPending { - result = append(result, txHash) + result[txHash] = trx } else { err := t.store.Delete(pendingTransactionKey(txHash)) if err != nil { diff --git a/pkg/transaction/wrapped/cache/cache.go b/pkg/transaction/wrapped/cache/cache.go new file mode 100644 index 00000000000..66d161253ac --- /dev/null +++ b/pkg/transaction/wrapped/cache/cache.go @@ -0,0 +1,86 @@ +// 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 cache + +import ( + "context" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "resenje.org/singleflight" +) + +type ( + Loader[T any] func() (T, error) + ReuseEvaluator[T any] func(value T) bool +) + +type SingleFlightCache[T any] struct { + mu sync.RWMutex + value T + + group singleflight.Group[string, any] + key string + metrics metricSet +} + +func NewSingleFlightCache[T any](metricsPrefix string) *SingleFlightCache[T] { + return &SingleFlightCache[T]{ + key: metricsPrefix, + metrics: newMetricSet(metricsPrefix), + } +} + +func (c *SingleFlightCache[T]) Collectors() []prometheus.Collector { + return []prometheus.Collector{ + c.metrics.Hits, + c.metrics.Misses, + c.metrics.Loads, + c.metrics.SharedLoads, + c.metrics.LoadErrors, + } +} + +func (c *SingleFlightCache[T]) Set(value T) { + c.mu.Lock() + defer c.mu.Unlock() + + c.value = value +} + +func (c *SingleFlightCache[T]) PeekOrLoad(ctx context.Context, canReuse ReuseEvaluator[T], loader Loader[T]) (T, error) { + c.mu.RLock() + value := c.value + c.mu.RUnlock() + + if canReuse(value) { + c.metrics.Hits.Inc() + return value, nil + } + + c.metrics.Misses.Inc() + + result, shared, err := c.group.Do(ctx, c.key, func(ctx context.Context) (any, error) { + c.metrics.Loads.Inc() + value, err := loader() + if err != nil { + c.metrics.LoadErrors.Inc() + return value, err + } + c.Set(value) + return value, nil + }) + + if shared { + c.metrics.SharedLoads.Inc() + } + + if err != nil { + var zero T + return zero, err + } + + return result.(T), nil +} diff --git a/pkg/transaction/wrapped/cache/cache_test.go b/pkg/transaction/wrapped/cache/cache_test.go new file mode 100644 index 00000000000..d954a7aee4b --- /dev/null +++ b/pkg/transaction/wrapped/cache/cache_test.go @@ -0,0 +1,222 @@ +// 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 cache + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "testing/synctest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testMetricsPrefix = "test" + +func newTestCache() *SingleFlightCache[uint64] { + return &SingleFlightCache[uint64]{ + key: testMetricsPrefix, + metrics: newMetricSet(testMetricsPrefix), + } +} + +func TestPeekOrLoadHit(t *testing.T) { + t.Parallel() + + c := newTestCache() + c.Set(42) + + var loadCount atomic.Int32 + val, err := c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return true + }, + func() (uint64, error) { + loadCount.Add(1) + return 0, errors.New("loader must not run") + }, + ) + + require.NoError(t, err) + assert.Equal(t, uint64(42), val) + assert.Zero(t, loadCount.Load()) +} + +func TestPeekOrLoadMiss(t *testing.T) { + t.Parallel() + + c := newTestCache() + var loadCount atomic.Int32 + + val, err := c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return false + }, + func() (uint64, error) { + loadCount.Add(1) + return 99, nil + }, + ) + + require.NoError(t, err) + assert.Equal(t, uint64(99), val) + assert.Equal(t, int32(1), loadCount.Load()) + + var verifyLoads atomic.Int32 + got, err := c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return true + }, + func() (uint64, error) { + verifyLoads.Add(1) + return 0, errors.New("unexpected load on verify") + }, + ) + require.NoError(t, err) + assert.Equal(t, uint64(99), got) + assert.Zero(t, verifyLoads.Load()) +} + +func TestPeekOrLoadError(t *testing.T) { + t.Parallel() + + c := newTestCache() + errLoad := errors.New("load failed") + + var loadCount atomic.Int32 + val, err := c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return false + }, + func() (uint64, error) { + loadCount.Add(1) + return 99, errLoad + }, + ) + + assert.ErrorIs(t, err, errLoad) + assert.Equal(t, uint64(0), val) + assert.Equal(t, int32(1), loadCount.Load()) + + _, err = c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return false + }, + func() (uint64, error) { + loadCount.Add(1) + return 0, errLoad + }, + ) + assert.ErrorIs(t, err, errLoad) + assert.Equal(t, int32(2), loadCount.Load()) +} + +func TestPeekOrLoadSingleflight(t *testing.T) { + const value = uint64(77) + synctest.Test(t, func(t *testing.T) { + c := newTestCache() + var loadCount atomic.Int32 + gate := make(chan struct{}) + + const n = 10 + var wg sync.WaitGroup + results := make([]uint64, n) + errs := make([]error, n) + + wg.Add(n) + for i := range n { + go func(idx int) { + defer wg.Done() + + results[idx], errs[idx] = c.PeekOrLoad( + context.Background(), + func(value uint64) bool { + return false + }, + func() (uint64, error) { + loadCount.Add(1) + <-gate + return value, nil + }, + ) + }(i) + } + + synctest.Wait() + close(gate) + wg.Wait() + + assert.Equal(t, int32(1), loadCount.Load()) + for i := range n { + assert.NoError(t, errs[i]) + assert.Equal(t, value, results[i]) + } + }) +} + +func TestPeekOrLoadContextCancellation(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + const expectedVal = 55 + c := newTestCache() + gate := make(chan struct{}) + + ctx1, cancel1 := context.WithCancel(context.Background()) + ctx2 := context.Background() + + var wg sync.WaitGroup + var result1, result2 uint64 + var err1, err2 error + + wg.Add(2) + go func() { + defer wg.Done() + + result1, err1 = c.PeekOrLoad( + ctx1, + func(value uint64) bool { + return false + }, + func() (uint64, error) { + <-gate + return expectedVal, nil + }, + ) + }() + + go func() { + defer wg.Done() + result2, err2 = c.PeekOrLoad( + ctx2, + func(value uint64) bool { + return false + }, + func() (uint64, error) { + <-gate + return expectedVal, nil + }, + ) + }() + + synctest.Wait() + cancel1() + synctest.Wait() + close(gate) + wg.Wait() + + assert.ErrorIs(t, err1, context.Canceled) + assert.Equal(t, uint64(0), result1) + + require.NoError(t, err2) + assert.Equal(t, uint64(expectedVal), result2) + }) +} diff --git a/pkg/transaction/wrapped/cache/metrics.go b/pkg/transaction/wrapped/cache/metrics.go new file mode 100644 index 00000000000..e7452fc0615 --- /dev/null +++ b/pkg/transaction/wrapped/cache/metrics.go @@ -0,0 +1,54 @@ +// 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 cache + +import ( + m "github.com/ethersphere/bee/v2/pkg/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +type metricSet struct { + Hits prometheus.Counter + Misses prometheus.Counter + Loads prometheus.Counter + SharedLoads prometheus.Counter + LoadErrors prometheus.Counter +} + +func newMetricSet(prefix string) metricSet { + subsystem := "eth_backend_cache" + return metricSet{ + Hits: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: prefix + "_hits", + Help: prefix + " cache hits", + }), + Misses: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: prefix + "_misses", + Help: prefix + " cache misses", + }), + Loads: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: prefix + "_loads", + Help: prefix + " cache backend loads", + }), + SharedLoads: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: prefix + "_shared_loads", + Help: prefix + " cache shared loads", + }), + LoadErrors: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: prefix + "_load_errors", + Help: prefix + " cache load errors", + }), + } +} diff --git a/pkg/transaction/wrapped/fee.go b/pkg/transaction/wrapped/fee.go index 34fe2cc60d5..dd8f5b80e80 100644 --- a/pkg/transaction/wrapped/fee.go +++ b/pkg/transaction/wrapped/fee.go @@ -16,9 +16,7 @@ const ( baseFeeMultiplier = 2 ) -var ( - ErrEIP1559NotSupported = errors.New("network does not appear to support EIP-1559 (no baseFee)") -) +var ErrEIP1559NotSupported = errors.New("network does not appear to support EIP-1559 (no baseFee)") // SuggestedFeeAndTip calculates the recommended gasFeeCap (maxFeePerGas) and gasTipCap (maxPriorityFeePerGas) for a transaction. // If gasPrice is provided (legacy mode): diff --git a/pkg/transaction/wrapped/fee_test.go b/pkg/transaction/wrapped/fee_test.go index 41afb69f28f..33ddc31da88 100644 --- a/pkg/transaction/wrapped/fee_test.go +++ b/pkg/transaction/wrapped/fee_test.go @@ -9,6 +9,7 @@ import ( "errors" "math/big" "testing" + "time" "github.com/ethereum/go-ethereum/core/types" "github.com/ethersphere/bee/v2/pkg/transaction/backendmock" @@ -105,6 +106,8 @@ func TestSuggestedFeeAndTip(t *testing.T) { }), ), minimumGasTipCap, + 5*time.Second, + 90, ) gasFeeCap, gasTipCap, err := backend.SuggestedFeeAndTip(ctx, tc.gasPrice, tc.boostPercent) diff --git a/pkg/transaction/wrapped/metrics.go b/pkg/transaction/wrapped/metrics.go index a5cfaa554a0..0570cc813d3 100644 --- a/pkg/transaction/wrapped/metrics.go +++ b/pkg/transaction/wrapped/metrics.go @@ -13,19 +13,19 @@ type metrics struct { TotalRPCCalls prometheus.Counter TotalRPCErrors prometheus.Counter - TransactionReceiptCalls prometheus.Counter - TransactionCalls prometheus.Counter - BlockNumberCalls prometheus.Counter - BlockHeaderCalls prometheus.Counter - BalanceCalls prometheus.Counter - NonceAtCalls prometheus.Counter - PendingNonceCalls prometheus.Counter - CallContractCalls prometheus.Counter - SuggestGasTipCapCalls prometheus.Counter - EstimateGasCalls prometheus.Counter - SendTransactionCalls prometheus.Counter - FilterLogsCalls prometheus.Counter - ChainIDCalls prometheus.Counter + TransactionReceiptCalls prometheus.Counter + TransactionCalls prometheus.Counter + BlockHeaderAsBlockNumberCalls prometheus.Counter + BlockHeaderCalls prometheus.Counter + BalanceCalls prometheus.Counter + NonceAtCalls prometheus.Counter + PendingNonceCalls prometheus.Counter + CallContractCalls prometheus.Counter + SuggestGasTipCapCalls prometheus.Counter + EstimateGasCalls prometheus.Counter + SendTransactionCalls prometheus.Counter + FilterLogsCalls prometheus.Counter + ChainIDCalls prometheus.Counter } func newMetrics() metrics { @@ -56,11 +56,11 @@ func newMetrics() metrics { Name: "calls_transaction_receipt", Help: "Count of eth_getTransactionReceipt rpc errors", }), - BlockNumberCalls: prometheus.NewCounter(prometheus.CounterOpts{ + BlockHeaderAsBlockNumberCalls: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: m.Namespace, Subsystem: subsystem, - Name: "calls_block_number", - Help: "Count of eth_blockNumber rpc calls", + Name: "calls_block_header_as_block_number", + Help: "Count of eth_getBlockByNumber for getting block number rpc calls", }), BlockHeaderCalls: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: m.Namespace, @@ -126,5 +126,7 @@ func newMetrics() metrics { } func (b *wrappedBackend) Metrics() []prometheus.Collector { - return m.PrometheusCollectorsFromFields(b.metrics) + collectors := m.PrometheusCollectorsFromFields(b.metrics) + collectors = append(collectors, b.blockNumberCache.Collectors()...) + return collectors } diff --git a/pkg/transaction/wrapped/wrapped.go b/pkg/transaction/wrapped/wrapped.go index f573934a7d3..723c405587e 100644 --- a/pkg/transaction/wrapped/wrapped.go +++ b/pkg/transaction/wrapped/wrapped.go @@ -8,29 +8,49 @@ import ( "context" "errors" "math/big" + "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethersphere/bee/v2/pkg/transaction" "github.com/ethersphere/bee/v2/pkg/transaction/backend" + "github.com/ethersphere/bee/v2/pkg/transaction/wrapped/cache" ) -var ( - _ transaction.Backend = (*wrappedBackend)(nil) -) +var _ transaction.Backend = (*wrappedBackend)(nil) -type wrappedBackend struct { - backend backend.Geth - metrics metrics - minimumGasTipCap int64 +type blockNumberAnchor struct { + number uint64 + timestamp time.Time } -func NewBackend(backend backend.Geth, minimumGasTipCap uint64) transaction.Backend { +type wrappedBackend struct { + backend backend.Geth + metrics metrics + minimumGasTipCap int64 + blockTime time.Duration + blockSyncInterval uint64 + blockNumberCache *cache.SingleFlightCache[blockNumberAnchor] +} + +func NewBackend( + backend backend.Geth, + minimumGasTipCap uint64, + blockTime time.Duration, + blockSyncInterval uint64, +) transaction.Backend { + if blockSyncInterval == 0 { + blockSyncInterval = 1 + } + return &wrappedBackend{ - backend: backend, - minimumGasTipCap: int64(minimumGasTipCap), - metrics: newMetrics(), + backend: backend, + minimumGasTipCap: int64(minimumGasTipCap), + blockTime: blockTime, + metrics: newMetrics(), + blockSyncInterval: blockSyncInterval, + blockNumberCache: cache.NewSingleFlightCache[blockNumberAnchor]("block_number"), } } @@ -61,14 +81,53 @@ func (b *wrappedBackend) TransactionByHash(ctx context.Context, hash common.Hash } func (b *wrappedBackend) BlockNumber(ctx context.Context) (uint64, error) { - b.metrics.TotalRPCCalls.Inc() - b.metrics.BlockNumberCalls.Inc() - blockNumber, err := b.backend.BlockNumber(ctx) + canReuseBlockAnchorFn := func(anchor blockNumberAnchor) bool { + _, elapsedBlocks := b.estimatedBlockNumberWithElapsed(anchor, time.Now().UTC()) + return elapsedBlocks < b.blockSyncInterval + } + + loadFreshBlockFn := func() (blockNumberAnchor, error) { + b.metrics.TotalRPCCalls.Inc() + b.metrics.BlockHeaderAsBlockNumberCalls.Inc() + + header, err := b.backend.HeaderByNumber(ctx, nil) + if err != nil { + b.metrics.TotalRPCErrors.Inc() + return blockNumberAnchor{}, err + } + if header == nil || header.Number == nil { + b.metrics.TotalRPCErrors.Inc() + return blockNumberAnchor{}, errors.New("latest block header unavailable") + } + return blockNumberAnchor{ + number: header.Number.Uint64(), + timestamp: time.Unix(int64(header.Time), 0).UTC(), + }, nil + } + + anchor, err := b.blockNumberCache.PeekOrLoad( + ctx, + canReuseBlockAnchorFn, + loadFreshBlockFn, + ) if err != nil { - b.metrics.TotalRPCErrors.Inc() return 0, err } - return blockNumber, nil + return b.estimatedBlockNumber(anchor, time.Now().UTC()), nil +} + +func (b *wrappedBackend) estimatedBlockNumber(anchor blockNumberAnchor, now time.Time) uint64 { + currentBlock, _ := b.estimatedBlockNumberWithElapsed(anchor, now) + return currentBlock +} + +func (b *wrappedBackend) estimatedBlockNumberWithElapsed(anchor blockNumberAnchor, now time.Time) (uint64, uint64) { + if now.Before(anchor.timestamp) { + return anchor.number, 0 + } + + elapsedBlocks := uint64(now.Sub(anchor.timestamp) / b.blockTime) + return anchor.number + elapsedBlocks, elapsedBlocks } func (b *wrappedBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { @@ -138,6 +197,7 @@ func (b *wrappedBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) } return gasTipCap, nil } + func (b *wrappedBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { b.metrics.TotalRPCCalls.Inc() b.metrics.EstimateGasCalls.Inc() diff --git a/pkg/transaction/wrapped/wrapped_test.go b/pkg/transaction/wrapped/wrapped_test.go new file mode 100644 index 00000000000..b0af463f6a6 --- /dev/null +++ b/pkg/transaction/wrapped/wrapped_test.go @@ -0,0 +1,195 @@ +// 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 wrapped + +import ( + "context" + "errors" + "math/big" + "sync/atomic" + "testing" + "testing/synctest" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethersphere/bee/v2/pkg/transaction/backendmock" + "github.com/stretchr/testify/assert" +) + +const ( + testBlockTime = 5 * time.Second + testBlockSyncInterval = uint64(20) + testMinimumGasTipCap = 0 +) + +func Test_BlockNumberCache_MissLoadsFromRPC(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + const expectedBlock = uint64(10) + var headerCalls atomic.Int32 + + backend := newTestWrappedBackend(t, backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + headerCalls.Add(1) + assert.Nil(t, number) + return &types.Header{ + Number: big.NewInt(int64(expectedBlock)), + Time: uint64(time.Now().UTC().Unix()), + }, nil + })) + + got, err := backend.BlockNumber(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, expectedBlock, got) + assert.Equal(t, int32(1), headerCalls.Load()) + }) +} + +func Test_BlockNumberReturns_FreshCache(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + const cachedBlock = uint64(20) + var headerCalls atomic.Int32 + + backend := newTestWrappedBackend(t, backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + headerCalls.Add(1) + return nil, errors.New("unexpected rpc call") + })) + + now := time.Now().UTC() + backend.blockNumberCache.Set(blockNumberAnchor{ + number: cachedBlock, + timestamp: now, + }) + + got, err := backend.BlockNumber(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, cachedBlock, got) + assert.Zero(t, headerCalls.Load()) + }) +} + +func Test_BlockNumber_ReturnsCalculatedBlock(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + const ( + anchorBlock = uint64(30) + elapsedBlocks = uint64(3) + expectedBlock = anchorBlock + elapsedBlocks + ) + var headerCalls atomic.Int32 + + backend := newTestWrappedBackend(t, backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + headerCalls.Add(1) + return nil, errors.New("unexpected rpc call") + })) + + now := time.Now().UTC() + backend.blockNumberCache.Set(blockNumberAnchor{ + number: anchorBlock, + timestamp: now.Add(-time.Duration(elapsedBlocks) * testBlockTime), + }) + + got, err := backend.BlockNumber(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, expectedBlock, got) + assert.Zero(t, headerCalls.Load()) + }) +} + +func Test_BlockNumber_ExpiredAnchor(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + const ( + staleBlock = uint64(40) + freshBlock = uint64(100) + elapsedBlocks = testBlockSyncInterval + 1 + ) + var headerCalls atomic.Int32 + + backend := newTestWrappedBackend(t, backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + headerCalls.Add(1) + return &types.Header{ + Number: big.NewInt(int64(freshBlock)), + Time: uint64(time.Now().UTC().Unix()), + }, nil + })) + + now := time.Now().UTC() + backend.blockNumberCache.Set(blockNumberAnchor{ + number: staleBlock, + timestamp: now.Add(-time.Duration(elapsedBlocks) * testBlockTime), + }) + + got, err := backend.BlockNumber(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, freshBlock, got) + assert.Equal(t, int32(1), headerCalls.Load()) + }) +} + +func Test_BlockNumber_ExpiredAnchor_RetriesAfterRPCError(t *testing.T) { + t.Parallel() + + synctest.Test(t, func(t *testing.T) { + const ( + staleBlock = uint64(50) + recoveredBlock = uint64(200) + elapsedBlocks = testBlockSyncInterval + 1 + ) + rpcErr := errors.New("rpc unavailable") + var headerCalls atomic.Int32 + + backend := newTestWrappedBackend(t, backendmock.WithHeaderbyNumberFunc(func(ctx context.Context, number *big.Int) (*types.Header, error) { + call := headerCalls.Add(1) + if call == 1 { + return nil, rpcErr + } + return &types.Header{ + Number: big.NewInt(int64(recoveredBlock)), + Time: uint64(time.Now().UTC().Unix()), + }, nil + })) + + now := time.Now().UTC() + backend.blockNumberCache.Set(blockNumberAnchor{ + number: staleBlock, + timestamp: now.Add(-time.Duration(elapsedBlocks) * testBlockTime), + }) + + first, err := backend.BlockNumber(context.Background()) + + assert.ErrorIs(t, err, rpcErr) + assert.Zero(t, first) + assert.Equal(t, int32(1), headerCalls.Load()) + + second, err := backend.BlockNumber(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, recoveredBlock, second) + assert.Equal(t, int32(2), headerCalls.Load()) + }) +} + +func newTestWrappedBackend(t *testing.T, opts ...backendmock.Option) *wrappedBackend { + t.Helper() + + backend, ok := NewBackend( + backendmock.New(opts...), + testMinimumGasTipCap, + testBlockTime, + testBlockSyncInterval, + ).(*wrappedBackend) + assert.True(t, ok) + + return backend +} diff --git a/pkg/util/nbhdutil/miner.go b/pkg/util/nbhdutil/miner.go index 83a51a8eeb7..112404b3ec2 100644 --- a/pkg/util/nbhdutil/miner.go +++ b/pkg/util/nbhdutil/miner.go @@ -15,7 +15,6 @@ import ( ) func MineOverlay(ctx context.Context, p ecdsa.PublicKey, networkID uint64, targetNeighborhood string) (swarm.Address, []byte, error) { - nonce := make([]byte, 32) neighborhood, err := swarm.ParseBitStrAddress(targetNeighborhood) diff --git a/pkg/util/nbhdutil/miner_test.go b/pkg/util/nbhdutil/miner_test.go index bcf72128bb9..4814bf2bbc5 100644 --- a/pkg/util/nbhdutil/miner_test.go +++ b/pkg/util/nbhdutil/miner_test.go @@ -50,7 +50,6 @@ func TestMiner(t *testing.T) { } func bitStr(src []byte, bits int) string { - ret := "" for _, b := range src { diff --git a/pkg/util/testutil/pseudorand/reader.go b/pkg/util/testutil/pseudorand/reader.go index bccb8900d7e..32893757c08 100644 --- a/pkg/util/testutil/pseudorand/reader.go +++ b/pkg/util/testutil/pseudorand/reader.go @@ -88,7 +88,6 @@ func (r1 *Reader) Equal(r2 io.Reader) (bool, error) { // Match compares the contents of the reader with the contents of // the given reader. It returns true if the contents are equal upto n bytes func (r1 *Reader) Match(r2 io.Reader, l int) (bool, error) { - read := func(r io.Reader, buf []byte) (n int, err error) { for n < len(buf) && err == nil { i, e := r.Read(buf[n:])