From 8e9dde404606a1c93b3777c06157d2af66ebfb15 Mon Sep 17 00:00:00 2001 From: 0xBECEDA Date: Wed, 29 Jul 2026 09:18:45 +0200 Subject: [PATCH 01/21] feat: pullsync fix tests --- config/config.yaml | 10 + config/local.yaml | 10 + config/soc-partition.yaml | 69 ++ pkg/bee/api/api.go | 3 + pkg/bee/api/chunks.go | 6 +- pkg/bee/api/envelope.go | 53 ++ pkg/bee/api/node.go | 8 +- pkg/bee/api/options.go | 1 + pkg/bee/api/soc.go | 21 +- pkg/bee/client.go | 19 +- .../chunkconvergence/chunkconvergence.go | 713 ++++++++++++++++++ pkg/config/check.go | 24 + pkg/config/soc_partition_test.go | 76 ++ pkg/orchestration/key.go | 46 ++ scripts/genpartitionkeys/main.go | 143 ++++ 15 files changed, 1191 insertions(+), 11 deletions(-) create mode 100644 config/soc-partition.yaml create mode 100644 pkg/bee/api/envelope.go create mode 100644 pkg/check/chunkconvergence/chunkconvergence.go create mode 100644 pkg/config/soc_partition_test.go create mode 100644 scripts/genpartitionkeys/main.go diff --git a/config/config.yaml b/config/config.yaml index d214b8447..c2487912c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -355,6 +355,16 @@ checks: request-timeout: 5m timeout: 5m type: soc + chunk-convergence: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: chunk-convergence + sync-wait: 45s + request-timeout: 15m + password: beekeeper + timeout: 20m + type: chunk-convergence stake: type: stake timeout: 5m diff --git a/config/local.yaml b/config/local.yaml index d389ee2b3..03043d2dc 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -382,6 +382,16 @@ checks: request-timeout: 5m timeout: 5m type: soc + ci-chunk-convergence: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: chunk-convergence + sync-wait: 45s + request-timeout: 15m + password: beekeeper + timeout: 20m + type: chunk-convergence ci-content-availability: type: content-availability timeout: 5m diff --git a/config/soc-partition.yaml b/config/soc-partition.yaml new file mode 100644 index 000000000..29be78492 --- /dev/null +++ b/config/soc-partition.yaml @@ -0,0 +1,69 @@ +# local-soc-partition — 5-node cluster for divergent-SOC / pullsync convergence tests. +# +# Topology (before the cross-group /connect): +# +# bootnode-0 +# │ +# group-a-0 ── group-a-1 group-b-0 ── group-b-1 +# (bootnodes → bootnode) (bootnodes → group-b-0 only) +# ✕ no P2P between A and B +# +# All four full nodes share target-neighborhood=11111100 so their overlays are +# mined into the same Swarm neighborhood. After you POST /connect between the +# groups they form one connected neighborhood and can pullsync. +# +# Create: +# beekeeper create bee-cluster --cluster-name=local-soc-partition +# +# Verify isolation (A must not list B overlays in /peers and vice versa), then +# upload conflicting SOC variants to each group. Heal with e.g.: +# curl -X POST "http://group-a-0.localhost:1633/connect/dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR" +# +# Libp2p keys for group-b were generated with: +# go run ./scripts/genpartitionkeys/ + +clusters: + local-soc-partition: + _inherit: "local" + node-groups: + bootnode: + mode: bootnode + bee-config: bootnode-local-dns + config: local-dns + nodes: + - name: bootnode-0 + bootnodes: /dns4/bootnode-0-headless.%s.svc.cluster.local/tcp/1634/p2p/QmaHzvd3iZduu275CMkMVZKwbsjXSyH3GJRj4UvFJApKcb + libp2p-key: '{"address":"28678fe31f09f722d53e77ca2395569f19959fa5","crypto":{"cipher":"aes-128-ctr","ciphertext":"0ff319684c4f8decf9c998047febe3417cfc45832b8bb62fd818183d54cf5d0183bfa021ed95addce3b33e83ce7ee73e926f00eea8241d96b349266a4d299829d3d22db0d536315b52b34db4a6778bfd3ce7631ad7256ea0bb9c50abea9de35d740b6fdc50caf929b1d19494690d9ed649105d02c14f5ec49d","cipherparams":{"iv":"4e9a50fb5852b5e61964f696be78066b"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"4d513e81647e4150bb648ed8d2dda28d460802336bf24d620119eac66ae0c0c4"},"mac":"9ae71db96e5ddc1c214538d42082212bbbe53aeac09fcc3e3a8eff815648331e"},"version":3,"id":"ae3bc991-d89f-405a-9e6a-60e27347e22d"}' + swarm-key: '{"address":"f176839c150e52fe30e5c2b5c648465c6fdfa532","crypto":{"cipher":"aes-128-ctr","ciphertext":"352af096f0fca9dfbd20a6861bde43d988efe7f179e0a9ffd812a285fdcd63b9","cipherparams":{"iv":"613003f1f1bf93430c92629da33f8828"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"ad1d99a4c64c95c26131e079e8c8a82221d58bf66a7ceb767c33a4c376c564b8"},"mac":"cafda1bc8ca0ffc2b22eb69afd1cf5072fd09412243443be1b0c6832f57924b6"},"version":3}' + group-a: + mode: node + bee-config: bee-soc-partition-group-a + config: local-dns + count: 2 + group-b: + mode: node + bee-config: bee-soc-partition-group-b + config: local-dns + nodes: + # Soft bootnode for the B island. Peer ID must match libp2p-key. + - name: group-b-0 + libp2p-key: '{"address":"dbc4584b50b571e4c426048ca30f3bbf31256664","crypto":{"cipher":"aes-128-ctr","ciphertext":"9e0fb2432abc8d5a591e62e9e79bb028564e6de172b3ad3acacceec01c52bf1c","cipherparams":{"iv":"e6421bbcbaee2afe1cde22cb9c87fe6a"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"e707f2cc3b18e98b4901df78afff7d22c42ee8bd5ffb4aa2692c327a406fda48"},"mac":"14516bd67bc56c8722b5784e9de3844a10eefedfcd2746deacd8eba46e834aa9"},"version":3,"id":"a3f5c9e7-e116-4c9b-b605-f007b0d94589"}' + - name: group-b-1 + libp2p-key: '{"address":"f64dc5d1a2233919f903a5582dd1d5b0b24cd255","crypto":{"cipher":"aes-128-ctr","ciphertext":"69eaf4a5a65992b0ac69f0a73c768cff8507e0d3538629cfbf46ce3cc57d6270","cipherparams":{"iv":"13a5007d75c76d353d7ee05913fc6905"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"69b56b0b71cef94a143b8cea095d8c2ee08a27d53cc99e007e8a5a36fd4f1c08"},"mac":"3514368a10020d238af1c5af42b134d1b179e59e4acbb8f4ded938178cdec8eb"},"version":3,"id":"e66e3d13-db28-455b-a02d-98f3d7a6508f"}' + +bee-configs: + # Joins the main bootnode (empty bootnodes → filled from bootnode group). + bee-soc-partition-group-a: + _inherit: "bee-local-dns" + full-node: true + target-neighborhood: "11111100" + welcome-message: "soc-partition group-a" + + # Isolated from the main bootnode: dials group-b-0 instead (soft bootnode). + # Non-empty bootnodes is required so beekeeper does not inject bootnode-0. + bee-soc-partition-group-b: + _inherit: "bee-local-dns" + full-node: true + bootnodes: /dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR + target-neighborhood: "11111100" + welcome-message: "soc-partition group-b" diff --git a/pkg/bee/api/api.go b/pkg/bee/api/api.go index f943fc33b..48158fdf2 100644 --- a/pkg/bee/api/api.go +++ b/pkg/bee/api/api.go @@ -17,6 +17,7 @@ const ( apiVersion = "v1" contentType = "application/json, text/plain, */*; charset=utf-8" postageStampBatchHeader = "Swarm-Postage-Batch-Id" + postageStampHeader = "Swarm-Postage-Stamp" deferredUploadHeader = "Swarm-Deferred-Upload" swarmAct = "Swarm-Act" swarmActHistoryAddress = "Swarm-Act-History-Address" @@ -46,6 +47,7 @@ type Client struct { Bytes *BytesService Chunks *ChunksService Dirs *DirsService + Envelope *EnvelopeService Feed *FeedService Files *FilesService Node *NodeService @@ -96,6 +98,7 @@ func newClient(apiURL *url.URL, httpClient *http.Client) (c *Client) { c.Bytes = (*BytesService)(&c.service) c.Chunks = (*ChunksService)(&c.service) c.Dirs = (*DirsService)(&c.service) + c.Envelope = (*EnvelopeService)(&c.service) c.Feed = (*FeedService)(&c.service) c.Files = (*FilesService)(&c.service) c.Node = (*NodeService)(&c.service) diff --git a/pkg/bee/api/chunks.go b/pkg/bee/api/chunks.go index e384b6e3d..7d257feb6 100644 --- a/pkg/bee/api/chunks.go +++ b/pkg/bee/api/chunks.go @@ -36,7 +36,11 @@ func (c *ChunksService) Upload(ctx context.Context, data []byte, o UploadOptions if o.Direct { h.Add(deferredUploadHeader, "false") } - h.Add(postageStampBatchHeader, o.BatchID) + if o.Stamp != "" { + h.Add(postageStampHeader, o.Stamp) + } else { + h.Add(postageStampBatchHeader, o.BatchID) + } err := c.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/chunks", h, bytes.NewReader(data), &resp) return resp, err } diff --git a/pkg/bee/api/envelope.go b/pkg/bee/api/envelope.go new file mode 100644 index 000000000..0021f9361 --- /dev/null +++ b/pkg/bee/api/envelope.go @@ -0,0 +1,53 @@ +package api + +import ( + "context" + "encoding/hex" + "fmt" + "net/http" + + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +// EnvelopeService represents Bee's postage envelope service. +type EnvelopeService service + +// EnvelopeResponse is the JSON body returned by POST /envelope/{address}. +type EnvelopeResponse struct { + Issuer string `json:"issuer"` + Index string `json:"index"` + Timestamp string `json:"timestamp"` + Signature string `json:"signature"` +} + +// Create requests a postage stamp for the given chunk address from the node's +// stamper for batchID. The stamp is returned as a postage.Stamp ready for upload. +func (e *EnvelopeService) Create(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + h := http.Header{} + h.Add(postageStampBatchHeader, batchID) + + var resp EnvelopeResponse + err := e.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/envelope/"+addr.String(), h, nil, &resp) + if err != nil { + return nil, err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return nil, fmt.Errorf("decode batch id: %w", err) + } + index, err := hex.DecodeString(resp.Index) + if err != nil { + return nil, fmt.Errorf("decode stamp index: %w", err) + } + ts, err := hex.DecodeString(resp.Timestamp) + if err != nil { + return nil, fmt.Errorf("decode stamp timestamp: %w", err) + } + sig, err := hex.DecodeString(resp.Signature) + if err != nil { + return nil, fmt.Errorf("decode stamp signature: %w", err) + } + return postage.NewStamp(batchBytes, index, ts, sig), nil +} diff --git a/pkg/bee/api/node.go b/pkg/bee/api/node.go index 789eeccc3..16bf172ab 100644 --- a/pkg/bee/api/node.go +++ b/pkg/bee/api/node.go @@ -80,12 +80,8 @@ func (n *NodeService) Balances(ctx context.Context) (resp Balances, err error) { // HasChunk returns true/false if node has a chunk func (n *NodeService) HasChunk(ctx context.Context, a swarm.Address) (bool, error) { - resp := struct { - Message string `json:"message,omitempty"` - Code int `json:"code,omitempty"` - }{} - - err := n.client.requestJSON(ctx, http.MethodGet, "/chunks/"+a.String(), nil, &resp) + // HEAD must not decode a JSON body (response is empty); nil target avoids EOF. + err := n.client.requestJSON(ctx, http.MethodHead, "/chunks/"+a.String(), nil, nil) if IsHTTPStatusErrorCode(err, http.StatusNotFound) { return false, nil } else if err != nil { diff --git a/pkg/bee/api/options.go b/pkg/bee/api/options.go index a88e7bcd6..a1a81f5de 100644 --- a/pkg/bee/api/options.go +++ b/pkg/bee/api/options.go @@ -10,6 +10,7 @@ type UploadOptions struct { Pin bool Tag uint64 BatchID string + Stamp string // hex-encoded postage stamp; when set, BatchID is ignored Direct bool ActHistoryAddress swarm.Address RLevel *redundancy.Level diff --git a/pkg/bee/api/soc.go b/pkg/bee/api/soc.go index 1fc84bd9b..c8f64f816 100644 --- a/pkg/bee/api/soc.go +++ b/pkg/bee/api/soc.go @@ -9,17 +9,32 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -// PSSService represents Bee's PSS service +// SOCService represents Bee's SOC service. type SOCService service +// SocResponse is the JSON body returned by POST /soc. type SocResponse struct { - Reference swarm.Address + Reference swarm.Address `json:"reference"` } -// Sends a PSS message to a recipienct with a specific topic +// UploadSOC uploads a single owner chunk using a postage batch ID. func (p *SOCService) UploadSOC(ctx context.Context, owner, ID, signature string, data io.Reader, batchID string) (*SocResponse, error) { h := http.Header{} h.Add(postageStampBatchHeader, batchID) + h.Add(deferredUploadHeader, "false") + url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) + + resp := SocResponse{} + return &resp, p.client.requestWithHeader(ctx, http.MethodPost, url, h, data, &resp) +} + +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp +// (hex-encoded Swarm-Postage-Stamp header). Direct upload is forced so the chunk +// enters the reserve immediately for pullsync. +func (p *SOCService) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data io.Reader, stampHex string) (*SocResponse, error) { + h := http.Header{} + h.Add(postageStampHeader, stampHex) + h.Add(deferredUploadHeader, "false") url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) resp := SocResponse{} diff --git a/pkg/bee/client.go b/pkg/bee/client.go index 711ffeb65..cc8b5e57b 100644 --- a/pkg/bee/client.go +++ b/pkg/bee/client.go @@ -3,6 +3,7 @@ package bee import ( "bytes" "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -17,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee/api" "github.com/ethersphere/beekeeper/pkg/logging" @@ -640,7 +642,7 @@ func (c *Client) SendPSSMessage(ctx context.Context, nodeAddress swarm.Address, return c.api.PSS.SendMessage(ctx, nodeAddress, publicKey, topic, prefix, bytes.NewReader(data), batchID) } -// UploadSOC uploads a single owner chunk to a node with a E +// UploadSOC uploads a single owner chunk to a node with a batch ID. func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, data []byte, batchID string) (swarm.Address, error) { resp, err := c.api.SOC.UploadSOC(ctx, owner, ID, signature, bytes.NewReader(data), batchID) if err != nil { @@ -650,6 +652,21 @@ func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, dat return resp.Reference, nil } +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp. +func (c *Client) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data []byte, stamp []byte) (swarm.Address, error) { + resp, err := c.api.SOC.UploadSOCWithStamp(ctx, owner, ID, signature, bytes.NewReader(data), hex.EncodeToString(stamp)) + if err != nil { + return swarm.ZeroAddress, err + } + + return resp.Reference, nil +} + +// CreateEnvelope requests a postage stamp for addr from the node's stamper. +func (c *Client) CreateEnvelope(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + return c.api.Envelope.Create(ctx, addr, batchID) +} + // Settlements represents Settlements's response type Settlements struct { Settlements []Settlement diff --git a/pkg/check/chunkconvergence/chunkconvergence.go b/pkg/check/chunkconvergence/chunkconvergence.go new file mode 100644 index 000000000..c9f1de0de --- /dev/null +++ b/pkg/check/chunkconvergence/chunkconvergence.go @@ -0,0 +1,713 @@ +package chunkconvergence + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/bee/api" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +const bucketDepth = postage.BucketDepth + +// Options represents check options. +type Options struct { + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + SyncWait time.Duration + RequestTimeout time.Duration + Password string // password for decrypting the batch-issuer Swarm key +} + +// NewDefaultOptions returns new default options. +func NewDefaultOptions() Options { + return Options{ + PostageTTL: 24 * time.Hour, + PostageDepth: 21, + PostageLabel: "chunk-convergence", + SyncWait: 45 * time.Second, + RequestTimeout: 10 * time.Minute, + Password: "beekeeper", + } +} + +var _ beekeeper.Action = (*Check)(nil) + +// Check verifies pullsync convergence for conflicting SOC/CAC stamp claims. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{logger: logger} +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + fullNodes, err := cluster.ShuffledFullNodeClients(ctx, random.PseudoGenerator(time.Now().UnixNano())) + if err != nil { + return fmt.Errorf("full node clients: %w", err) + } + if len(fullNodes) < 2 { + return fmt.Errorf("chunk convergence requires at least 2 full nodes, got %d", len(fullNodes)) + } + + n1, n2, err := pickClosestPair(ctx, fullNodes) + if err != nil { + return err + } + c.logger.Infof("neighborhood upload pair: %s and %s", n1.Name(), n2.Name()) + neighborhood := []*bee.Client{n1, n2} + + issuer, signer, batchID, err := c.setupIssuerBatch(ctx, cluster, o) + if err != nil { + return err + } + c.logger.Infof("batch issuer=%s batch_id=%s", issuer.Name(), batchID) + + scenarios := []struct { + name string + run func() error + }{ + {"1 divergent soc identical stamp", func() error { + return c.scenarioDivergentSOC(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"2 soc same index higher timestamp", func() error { + return c.scenarioSOCSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) + }}, + {"3 soc different index higher timestamp", func() error { + return c.scenarioSOCDifferentIndexTimestamp(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"4 cac same index higher timestamp", func() error { + return c.scenarioCACSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) + }}, + {"5 soc cross-batch higher timestamp", func() error { + return c.scenarioSOCCrossBatchTimestamp(ctx, cluster, neighborhood, o) + }}, + {"6 soc cross-batch equal timestamp", func() error { + return c.scenarioSOCEqualTimestampTieBreak(ctx, issuer, signer, neighborhood, batchID, o, o.SyncWait) + }}, + } + + var errs error + for _, s := range scenarios { + c.logger.Infof("========== %s ==========", s.name) + if err := s.run(); err != nil { + c.logger.Errorf("%s failed: %v", s.name, err) + errs = errors.Join(errs, fmt.Errorf("%s: %w", s.name, err)) + continue + } + c.logger.Infof("%s passed", s.name) + } + return errs +} + +func (c *Check) setupIssuerBatch(ctx context.Context, cluster orchestration.Cluster, o Options) (*bee.Client, crypto.Signer, string, error) { + nodes := cluster.Nodes() + var issuerNode orchestration.Node + var issuerName string + for name, n := range nodes { + if n.SwarmKey() != nil { + issuerNode = n + issuerName = name + break + } + } + if issuerNode == nil { + return nil, nil, "", fmt.Errorf("no node with swarm-key found; bootnode with configured swarm-key is required") + } + + privKey, err := issuerNode.SwarmKey().Decrypt(o.Password) + if err != nil { + return nil, nil, "", fmt.Errorf("decrypt swarm key for %s: %w", issuerName, err) + } + signer := crypto.NewDefaultSigner(privKey) + + clients, err := cluster.NodesClients(ctx) + if err != nil { + return nil, nil, "", fmt.Errorf("nodes clients: %w", err) + } + issuer, ok := clients[issuerName] + if !ok { + return nil, nil, "", fmt.Errorf("client for issuer node %s not found", issuerName) + } + + label := fmt.Sprintf("%s-%s", o.PostageLabel, hex.EncodeToString(randomBytes(4))) + batchID, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, label) + if err != nil { + return nil, nil, "", fmt.Errorf("create batch on %s: %w", issuerName, err) + } + return issuer, signer, batchID, nil +} + +func (c *Check) scenarioDivergentSOC(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + low, high, err := orderedSOCs(signer, id) + if err != nil { + return err + } + stamp, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope: %w", err) + } + stampBytes, err := stamp.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof("soc=%s wrapped_low=%s wrapped_high=%s", addr, mustWrapped(low), mustWrapped(high)) + // Identical stamp: second upload may lose the wrapped-CAC tie-break if the + // winner already synced. Require only the first (winning) upload. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, low, high, stampBytes, stampBytes, false); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, low.Data()) +} + +func (c *Check) scenarioSOCSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-same-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-same-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // CreateEnvelope yields a postage-valid index; reuse it with two timestamps. + env, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + stampOld, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), addr) + if err != nil { + return fmt.Errorf("sign older stamp: %w", err) + } + stampNew, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), addr) + if err != nil { + return fmt.Errorf("sign newer stamp: %w", err) + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s index=%s", addr, hex.EncodeToString(index)) + // Older first, then newer — avoids racing a losing upload after the winner synced. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioSOCDifferentIndexTimestamp(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-diff-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-diff-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + stampOld, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope older: %w", err) + } + stampNew, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer: %w", err) + } + for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { + stampNew, err = issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer retry: %w", err) + } + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s idx_old=%s idx_new=%s", addr, hex.EncodeToString(stampOld.Index()), hex.EncodeToString(stampNew.Index())) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Valid index for this postage bucket (both CACs share the bucket). + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + // higher timestamp on high address chunk so timestamp rule (not address tie-break) decides + stampLow, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s", low.Address(), high.Address(), hex.EncodeToString(index)) + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], low, high); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing higher-timestamp CAC %s", n.Name(), high.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has lower-timestamp CAC %s", n.Name(), low.Address()) + } + } + return nil +} + +func (c *Check) scenarioSOCCrossBatchTimestamp(ctx context.Context, cluster orchestration.Cluster, neighborhood []*bee.Client, o Options) error { + clients, err := cluster.NodesClients(ctx) + if err != nil { + return err + } + var issuerA, issuerB *bee.Client + for _, cl := range clients { + if issuerA == nil { + issuerA = cl + continue + } + if issuerB == nil && cl.Name() != issuerA.Name() { + issuerB = cl + break + } + } + if issuerA == nil || issuerB == nil { + return fmt.Errorf("need two nodes to create two batches") + } + + batchA, err := issuerA.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-a-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch A: %w", err) + } + batchB, err := issuerB.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-b-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch B: %w", err) + } + + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-xbatch-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-xbatch-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + stampOld, err := issuerA.CreateEnvelope(ctx, addr, batchA) + if err != nil { + return fmt.Errorf("envelope A: %w", err) + } + stampNew, err := issuerB.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope B: %w", err) + } + for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { + stampNew, err = issuerB.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope B retry: %w", err) + } + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s batchA=%s… batchB=%s…", addr, batchA[:8], batchB[:8]) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(o.SyncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioSOCEqualTimestampTieBreak(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { + // Two batches from the same issuer (bootnode) so one key can sign stamps for both. + batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-eqts-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch B: %w", err) + } + + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + a, err := buildSOC(signer, id, append([]byte("soc-eqts-a-"), randomBytes(4)...)) + if err != nil { + return err + } + b, err := buildSOC(signer, id, append([]byte("soc-eqts-b-"), randomBytes(4)...)) + if err != nil { + return err + } + + batchABytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + batchBBytes, err := hex.DecodeString(batchB) + if err != nil { + return err + } + // Valid indexes from real envelopes on each batch; equal timestamps for stampHash tie-break. + envA, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope A: %w", err) + } + envB, err := issuer.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope B: %w", err) + } + ts := binary.BigEndian.Uint64(envA.Timestamp()) + if tsb := binary.BigEndian.Uint64(envB.Timestamp()); tsb > ts { + ts = tsb + } + tsBytes := uint64ToBytes(ts) + idxA := append([]byte{}, envA.Index()...) + idxB := append([]byte{}, envB.Index()...) + + stampA, err := signStamp(batchSigner, batchABytes, idxA, tsBytes, addr) + if err != nil { + return err + } + stampB, err := signStamp(batchSigner, batchBBytes, idxB, tsBytes, addr) + if err != nil { + return err + } + hashA, err := stampA.Hash() + if err != nil { + return err + } + hashB, err := stampB.Hash() + if err != nil { + return err + } + + var winner, loser swarm.Chunk + var winStamp, loseStamp []byte + if bytes.Compare(hashA, hashB) < 0 { + winner, loser = a.WithStamp(stampA), b.WithStamp(stampB) + winStamp, _ = stampA.MarshalBinary() + loseStamp, _ = stampB.MarshalBinary() + } else { + winner, loser = b.WithStamp(stampB), a.WithStamp(stampA) + winStamp, _ = stampB.MarshalBinary() + loseStamp, _ = stampA.MarshalBinary() + } + + c.logger.Infof("soc=%s cross-batch equal_ts lower stampHash wins batchA=%s… batchB=%s…", addr, batchID[:8], batchB[:8]) + // Upload loser first, then winner. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, loser, winner, loseStamp, winStamp, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, winner.Data()) +} + +// uploadSOCPair stores ch1 on n1 then ch2 on n2. Callers should pass the +// expected loser/older chunk first so the winner can overwrite during sync. +// If requireSecond is false, a failed second upload is ignored (tie-break loser). +func uploadSOCPair(ctx context.Context, n1, n2 *bee.Client, owner string, id []byte, ch1, ch2 swarm.Chunk, stamp1, stamp2 []byte, requireSecond bool) error { + sig1 := hex.EncodeToString(ch1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2 := hex.EncodeToString(ch2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped1 := ch1.Data()[swarm.HashSize+swarm.SocSignatureSize:] + wrapped2 := ch2.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + + if _, err := n1.UploadSOCWithStamp(ctx, owner, idHex, sig1, wrapped1, stamp1); err != nil { + return fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + if _, err := n2.UploadSOCWithStamp(ctx, owner, idHex, sig2, wrapped2, stamp2); err != nil { + if requireSecond { + return fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + } + return nil +} + +func uploadChunkPair(ctx context.Context, n1, n2 *bee.Client, ch1, ch2 swarm.Chunk) error { + st1, err := ch1.Stamp().MarshalBinary() + if err != nil { + return err + } + st2, err := ch2.Stamp().MarshalBinary() + if err != nil { + return err + } + if _, err := n1.UploadChunk(ctx, ch1.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st1), Direct: true}); err != nil { + return fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + if _, err := n2.UploadChunk(ctx, ch2.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st2), Direct: true}); err != nil { + return fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + return nil +} + +func assertNeighborhoodPayload(ctx context.Context, logger logging.Logger, nodes []*bee.Client, addr swarm.Address, want []byte) error { + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + if !bytes.Equal(got, want) { + logger.Errorf("node %s payload mismatch want_len=%d got_len=%d", n.Name(), len(want), len(got)) + return fmt.Errorf("node %s: payload mismatch", n.Name()) + } + } + return nil +} + +func pickClosestPair(ctx context.Context, nodes orchestration.ClientList) (*bee.Client, *bee.Client, error) { + overlays := make([]swarm.Address, len(nodes)) + for i, n := range nodes { + a, err := n.Addresses(ctx) + if err != nil { + return nil, nil, fmt.Errorf("addresses for %s: %w", n.Name(), err) + } + overlays[i] = a.Overlay + } + bestI, bestJ, bestPO := 0, 1, -1 + for i := 0; i < len(nodes); i++ { + for j := i + 1; j < len(nodes); j++ { + p := int(swarm.Proximity(overlays[i].Bytes(), overlays[j].Bytes())) + if p > bestPO { + bestPO, bestI, bestJ = p, i, j + } + } + } + if bestPO < 0 { + return nil, nil, fmt.Errorf("could not find a node pair") + } + return nodes[bestI], nodes[bestJ], nil +} + +func mineSOC(ctx context.Context, n1, n2 *bee.Client) (crypto.Signer, string, []byte, swarm.Address, error) { + a1, err := n1.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + a2, err := n2.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + t1, t2 := a1.Overlay, a2.Overlay + minPO := swarm.Proximity(t1.Bytes(), t2.Bytes()) + if minPO > 8 { + minPO = 8 + } + + for { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + signer := crypto.NewDefaultSigner(privKey) + ownerBytes, err := signer.EthereumAddress() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + owner := hex.EncodeToString(ownerBytes.Bytes()) + for range 8192 { + id := make([]byte, swarm.HashSize) + if _, err := crand.Read(id); err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + tmp, err := cac.New([]byte("x")) + if err != nil { + continue + } + ch, err := soc.New(id, tmp).Sign(signer) + if err != nil { + continue + } + a := ch.Address() + if swarm.Proximity(a.Bytes(), t1.Bytes()) >= minPO && + swarm.Proximity(a.Bytes(), t2.Bytes()) >= minPO { + return signer, owner, id, a, nil + } + } + } +} + +func orderedSOCs(signer crypto.Signer, id []byte) (low, high swarm.Chunk, err error) { + a, err := buildSOC(signer, id, []byte("soc-conv-payload-aaa")) + if err != nil { + return nil, nil, err + } + b, err := buildSOC(signer, id, []byte("soc-conv-payload-bbb")) + if err != nil { + return nil, nil, err + } + wa, wb := mustWrapped(a), mustWrapped(b) + if bytes.Compare(wa.Bytes(), wb.Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func buildSOC(signer crypto.Signer, id, payload []byte) (swarm.Chunk, error) { + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + return soc.New(id, ch).Sign(signer) +} + +func orderedCACsInBucket(target swarm.Address) (low, high swarm.Chunk, err error) { + bucket := addressBucket(bucketDepth, target) + a, err := cacInBucket(bucket, append([]byte("cac-conv-aaa-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + b, err := cacInBucket(bucket, append([]byte("cac-conv-bbb-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + for a.Address().Equal(b.Address()) { + b, err = cacInBucket(bucket, randomBytes(32)) + if err != nil { + return nil, nil, err + } + } + if bytes.Compare(a.Address().Bytes(), b.Address().Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func cacInBucket(bucket uint32, seed []byte) (swarm.Chunk, error) { + for range 5_000_000 { + payload := append(append([]byte{}, seed...), randomBytes(16)...) + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + if addressBucket(bucketDepth, ch.Address()) == bucket { + return ch, nil + } + seed = randomBytes(16) + } + return nil, fmt.Errorf("could not mine CAC into bucket %d", bucket) +} + +func addressBucket(depth uint8, addr swarm.Address) uint32 { + return binary.BigEndian.Uint32(addr.Bytes()[:4]) >> (32 - depth) +} + +func signStamp(signer crypto.Signer, batchID, index, ts []byte, addr swarm.Address) (*postage.Stamp, error) { + digest, err := postage.ToSignDigest(addr.Bytes(), batchID, index, ts) + if err != nil { + return nil, err + } + sig, err := signer.Sign(digest) + if err != nil { + return nil, err + } + return postage.NewStamp(batchID, index, ts, sig), nil +} + +func mustWrapped(ch swarm.Chunk) swarm.Address { + s, err := soc.FromChunk(ch) + if err != nil { + return swarm.ZeroAddress + } + return s.WrappedChunk().Address() +} + +func uint64ToBytes(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + _, _ = crand.Read(b) + return b +} diff --git a/pkg/config/check.go b/pkg/config/check.go index 541afb7e5..e6e65eb97 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -12,6 +12,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/autotls" "github.com/ethersphere/beekeeper/pkg/check/balances" "github.com/ethersphere/beekeeper/pkg/check/cashout" + "github.com/ethersphere/beekeeper/pkg/check/chunkconvergence" "github.com/ethersphere/beekeeper/pkg/check/datadurability" "github.com/ethersphere/beekeeper/pkg/check/feed" "github.com/ethersphere/beekeeper/pkg/check/fileretrieval" @@ -693,6 +694,29 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "chunk-convergence": { + NewAction: chunkconvergence.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + SyncWait *time.Duration `yaml:"sync-wait"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + Password *string `yaml:"password"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := chunkconvergence.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "feed-v1": { NewAction: feed.NewCheckV1, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { diff --git a/pkg/config/soc_partition_test.go b/pkg/config/soc_partition_test.go new file mode 100644 index 000000000..79b484abb --- /dev/null +++ b/pkg/config/soc_partition_test.go @@ -0,0 +1,76 @@ +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/ethersphere/beekeeper/pkg/config" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestSOCPartitionClusterConfig(t *testing.T) { + t.Parallel() + + dir := filepath.Join("..", "..", "config") + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + + var files []config.YamlFile + for _, e := range entries { + if e.IsDir() { + continue + } + ext := filepath.Ext(e.Name()) + if ext != ".yaml" && ext != ".yml" { + continue + } + content, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + t.Fatal(err) + } + files = append(files, config.YamlFile{Name: e.Name(), Content: content}) + } + + cfg, err := config.Read(logging.New(os.Stderr, 0), files) + if err != nil { + t.Fatalf("read config: %v", err) + } + + cluster, ok := cfg.Clusters["local-soc-partition"] + if !ok { + t.Fatal("local-soc-partition cluster missing") + } + groups := cluster.GetNodeGroups() + if _, ok := groups["bootnode"]; !ok { + t.Fatal("bootnode group missing") + } + if g, ok := groups["group-a"]; !ok || g.Count != 2 { + t.Fatalf("group-a: %+v", g) + } + if g, ok := groups["group-b"]; !ok || len(g.Nodes) != 2 { + t.Fatalf("group-b: %+v", g) + } + + a, ok := cfg.BeeConfigs["bee-soc-partition-group-a"] + if !ok || a.TargetNeighborhood == nil || *a.TargetNeighborhood != "11111100" { + t.Fatalf("group-a bee config: %+v", a) + } + b, ok := cfg.BeeConfigs["bee-soc-partition-group-b"] + if !ok || b.Bootnodes == nil || *b.Bootnodes == "" { + t.Fatalf("group-b must have non-empty bootnodes: %+v", b) + } + if b.TargetNeighborhood == nil || *b.TargetNeighborhood != "11111100" { + t.Fatalf("group-b target neighborhood: %+v", b) + } + + exported := b.Export() + if exported.TargetNeighborhood != "11111100" { + t.Fatalf("export target neighborhood: %q", exported.TargetNeighborhood) + } + if exported.Bootnodes == "" { + t.Fatal("export bootnodes empty") + } +} diff --git a/pkg/orchestration/key.go b/pkg/orchestration/key.go index 22c12d5af..056ab7681 100644 --- a/pkg/orchestration/key.go +++ b/pkg/orchestration/key.go @@ -1,6 +1,7 @@ package orchestration import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" @@ -13,6 +14,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/google/uuid" "golang.org/x/crypto/scrypt" + "golang.org/x/crypto/sha3" "gopkg.in/yaml.v3" ) @@ -69,6 +71,50 @@ func (k *EncryptedKey) StringJSON() (string, error) { return string(data), nil } +// Decrypt decrypts the encrypted Swarm key with the given password and returns +// the secp256k1 private key. The key file format matches Ethereum JSON v3. +func (k *EncryptedKey) Decrypt(password string) (*ecdsa.PrivateKey, error) { + if k == nil { + return nil, fmt.Errorf("nil encrypted key") + } + salt, err := hex.DecodeString(k.Crypto.KDFParams.Salt) + if err != nil { + return nil, fmt.Errorf("decode salt: %w", err) + } + derivedKey, err := scrypt.Key([]byte(password), salt, k.Crypto.KDFParams.N, k.Crypto.KDFParams.R, k.Crypto.KDFParams.P, k.Crypto.KDFParams.DKLen) + if err != nil { + return nil, fmt.Errorf("scrypt: %w", err) + } + cipherText, err := hex.DecodeString(k.Crypto.CipherText) + if err != nil { + return nil, fmt.Errorf("decode ciphertext: %w", err) + } + wantMAC, err := hex.DecodeString(k.Crypto.MAC) + if err != nil { + return nil, fmt.Errorf("decode mac: %w", err) + } + // Bee keystore MAC is SHA3-256; Ethereum V3 keyfiles use Keccak-256. + calculatedMAC := sha3.Sum256(append(derivedKey[16:32], cipherText...)) + if !bytes.Equal(calculatedMAC[:], wantMAC) { + calculatedMACEth, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) + if err != nil { + return nil, fmt.Errorf("mac: %w", err) + } + if !bytes.Equal(calculatedMACEth, wantMAC) { + return nil, fmt.Errorf("invalid password or corrupted key") + } + } + iv, err := hex.DecodeString(k.Crypto.CipherParams.IV) + if err != nil { + return nil, fmt.Errorf("decode iv: %w", err) + } + plain, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, fmt.Errorf("decrypt: %w", err) + } + return crypto.DecodeSecp256k1PrivateKey(plain) +} + type keyCripto struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` diff --git a/scripts/genpartitionkeys/main.go b/scripts/genpartitionkeys/main.go new file mode 100644 index 000000000..ee3d7fc89 --- /dev/null +++ b/scripts/genpartitionkeys/main.go @@ -0,0 +1,143 @@ +// 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. + +// Command genpartitionkeys generates encrypted libp2p keys and peer IDs for the +// local-soc-partition beekeeper cluster (group-b soft bootnode). +// +// go run ./scripts/genpartitionkeys/ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/google/uuid" + libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "golang.org/x/crypto/scrypt" +) + +const ( + password = "beekeeper" + scryptN = 1 << 15 + scryptR = 8 + scryptP = 1 + scryptDKLen = 32 +) + +type encryptedKey struct { + Address string `json:"address"` + Crypto struct { + Cipher string `json:"cipher"` + CipherText string `json:"ciphertext"` + CipherParams struct { + IV string `json:"iv"` + } `json:"cipherparams"` + KDF string `json:"kdf"` + KDFParams struct { + N int `json:"n"` + R int `json:"r"` + P int `json:"p"` + DKLen int `json:"dklen"` + Salt string `json:"salt"` + } `json:"kdfparams"` + MAC string `json:"mac"` + } `json:"crypto"` + Version int `json:"version"` + ID string `json:"id"` +} + +func main() { + for _, name := range []string{"group-b-0", "group-b-1"} { + priv, err := crypto.GenerateSecp256k1Key() + if err != nil { + panic(err) + } + keyJSON, err := encryptKey(priv, password) + if err != nil { + panic(err) + } + pid, err := peerID(priv) + if err != nil { + panic(err) + } + fmt.Printf("NAME=%s\nPEER=%s\nKEY=%s\n\n", name, pid, keyJSON) + } +} + +func peerID(priv *ecdsa.PrivateKey) (string, error) { + raw, err := crypto.EncodeSecp256k1PrivateKey(priv) + if err != nil { + return "", err + } + libKey, err := libp2pcrypto.UnmarshalSecp256k1PrivateKey(raw) + if err != nil { + return "", err + } + id, err := peer.IDFromPrivateKey(libKey) + if err != nil { + return "", err + } + return id.String(), nil +} + +func encryptKey(k *ecdsa.PrivateKey, pass string) (string, error) { + data, err := crypto.EncodeSecp256k1PrivateKey(k) + if err != nil { + return "", err + } + salt := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + return "", err + } + derivedKey, err := scrypt.Key([]byte(pass), salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return "", err + } + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + block, err := aes.NewCipher(derivedKey[:16]) + if err != nil { + return "", err + } + cipherText := make([]byte, len(data)) + cipher.NewCTR(block, iv).XORKeyStream(cipherText, data) + mac, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) + if err != nil { + return "", err + } + addr, err := crypto.NewEthereumAddress(k.PublicKey) + if err != nil { + return "", err + } + ek := encryptedKey{ + Address: hex.EncodeToString(addr), + Version: 3, + ID: uuid.NewString(), + } + ek.Crypto.Cipher = "aes-128-ctr" + ek.Crypto.CipherText = hex.EncodeToString(cipherText) + ek.Crypto.CipherParams.IV = hex.EncodeToString(iv) + ek.Crypto.KDF = "scrypt" + ek.Crypto.KDFParams.N = scryptN + ek.Crypto.KDFParams.R = scryptR + ek.Crypto.KDFParams.P = scryptP + ek.Crypto.KDFParams.DKLen = scryptDKLen + ek.Crypto.KDFParams.Salt = hex.EncodeToString(salt) + ek.Crypto.MAC = hex.EncodeToString(mac) + out, err := json.Marshal(ek) + if err != nil { + return "", err + } + return string(out), nil +} From 0c96d9758e19dddd73557623b7cb89b04c792087 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 29 Jul 2026 11:58:43 +0300 Subject: [PATCH 02/21] feat(check): add socconvergence check for cluster-wide SWIP-101 validation --- config/soc-partition.yaml | 3 +- go.mod | 32 +- go.sum | 143 ++++--- pkg/check/socconvergence/socconvergence.go | 377 ++++++++++++++++++ .../socconvergence/socconvergence_test.go | 34 ++ pkg/config/check.go | 24 ++ pkg/config/soc_partition_test.go | 8 +- 7 files changed, 525 insertions(+), 96 deletions(-) create mode 100644 pkg/check/socconvergence/socconvergence.go create mode 100644 pkg/check/socconvergence/socconvergence_test.go diff --git a/config/soc-partition.yaml b/config/soc-partition.yaml index 29be78492..f075faa13 100644 --- a/config/soc-partition.yaml +++ b/config/soc-partition.yaml @@ -64,6 +64,7 @@ bee-configs: bee-soc-partition-group-b: _inherit: "bee-local-dns" full-node: true - bootnodes: /dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR + bootnode: + - /dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR target-neighborhood: "11111100" welcome-message: "soc-partition group-b" diff --git a/go.mod b/go.mod index 700e4440b..5ef5b8516 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.26 replace github.com/codahale/hdrhistogram => github.com/HdrHistogram/hdrhistogram-go v1.1.2 +replace github.com/ethersphere/bee/v2 => ../bee + require ( - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.3 github.com/ethersphere/bee/v2 v2.7.0 github.com/ethersphere/bmt v0.1.4 github.com/ethersphere/ethproxy v0.0.5 @@ -15,7 +17,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/ipshipyard/p2p-forge v0.7.0 - github.com/libp2p/go-libp2p v0.46.0 + github.com/libp2p/go-libp2p v0.48.0 github.com/miekg/dns v1.1.66 github.com/multiformats/go-multiaddr v0.16.1 github.com/multiformats/go-multibase v0.2.0 @@ -50,8 +52,9 @@ require ( github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect + github.com/codahale/hdrhistogram v0.0.0-00010101000000-000000000000 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect @@ -59,7 +62,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/ethersphere/go-sw3-abi v0.6.9 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -98,7 +101,7 @@ require ( github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-netroute v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -120,21 +123,18 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.11 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtcp v1.2.16 // indirect github.com/pion/rtp v1.8.19 // indirect github.com/pion/sctp v1.8.39 // indirect - github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/stun v0.6.1 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/stun/v3 v3.1.1 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/turn/v4 v4.0.2 // indirect @@ -154,7 +154,7 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/uber/jaeger-lib v2.2.0+incompatible // indirect @@ -165,9 +165,9 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect diff --git a/go.sum b/go.sum index 180777887..b56d843b9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= @@ -15,6 +21,7 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= @@ -57,8 +64,8 @@ github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDd github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= @@ -78,6 +85,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= @@ -86,14 +95,12 @@ github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapw github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= -github.com/ethersphere/bee/v2 v2.7.0 h1:faaB3WP60SNrZvBCz2xh5sSIc9ka3Cz/ApWyedqBvQA= -github.com/ethersphere/bee/v2 v2.7.0/go.mod h1:6Hhfttnet1vQmSydC9bivOKtUmaCarT3Mqi9Slj2tkg= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= github.com/ethersphere/bmt v0.1.4 h1:+rkWYNtMgDx6bkNqGdWu+U9DgGI1rRZplpSW3YhBr1Q= github.com/ethersphere/bmt v0.1.4/go.mod h1:Yd8ft1U69WDuHevZc/rwPxUv1rzPSMpMnS6xbU53aY8= github.com/ethersphere/ethproxy v0.0.5 h1:j5Mkm45jqmkET6NwGaJtaxOSFbhoAfOKzHiwHl6DBT0= @@ -106,6 +113,7 @@ github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeD github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -128,6 +136,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -154,12 +163,14 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -217,6 +228,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -247,24 +259,24 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.46.0 h1:0T2yvIKpZ3DVYCuPOFxPD1layhRU486pj9rSlGWYnDM= -github.com/libp2p/go-libp2p v0.46.0/go.mod h1:TbIDnpDjBLa7isdgYpbxozIVPBTmM/7qKOJP4SFySrQ= +github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= +github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= -github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/marcopolo/simnet v0.0.1 h1:rSMslhPz6q9IvJeFWDoMGxMIrlsbXau3NkuIXHGJxfg= -github.com/marcopolo/simnet v0.0.1/go.mod h1:WDaQkgLAjqDUEBAOXz22+1j6wXKfGlC5sD5XWt3ddOs= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -321,6 +333,7 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= @@ -336,40 +349,35 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= -github.com/pion/dtls/v3 v3.0.11/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= -github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= @@ -399,8 +407,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= -github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -446,14 +454,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -470,7 +476,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -481,7 +486,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -492,14 +496,14 @@ gitlab.com/nolash/go-mockbytes v0.0.7 h1:9XVFpEfY67kGBVJve3uV19kzqORdlo7V+q09OE6 gitlab.com/nolash/go-mockbytes v0.0.7/go.mod h1:KKOpNTT39j2Eo+P6uUTOncntfeKY6AFh/2CxuD5MpgE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -519,25 +523,30 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -546,12 +555,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -560,11 +563,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -574,57 +576,47 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= @@ -657,6 +649,7 @@ lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= resenje.org/x v0.6.0 h1:afn9E4XhglF4y9Kq0VH5tdSyjnsVKxiYgB6HFj7ebss= resenje.org/x v0.6.0/go.mod h1:qgwe4MCzh57EkkMDurg24ug7HHfZtAjtBkmCihNmOpM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/pkg/check/socconvergence/socconvergence.go b/pkg/check/socconvergence/socconvergence.go new file mode 100644 index 000000000..d54c21d4a --- /dev/null +++ b/pkg/check/socconvergence/socconvergence.go @@ -0,0 +1,377 @@ +// 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 socconvergence + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/storage" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +// Options represents check options +type Options struct { + GasPrice string + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + RequestTimeout time.Duration + SyncRetryInterval time.Duration +} + +// NewDefaultOptions returns new default options +func NewDefaultOptions() Options { + return Options{ + GasPrice: "", + PostageTTL: 24 * time.Hour, + PostageDepth: 16, + PostageLabel: "soc-convergence-label", + RequestTimeout: 5 * time.Minute, + SyncRetryInterval: 2 * time.Second, + } +} + +// compile check whether Check implements interface +var _ beekeeper.Action = (*Check)(nil) + +// Check instance. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + logger: logger, + } +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + rnd := random.PseudoGenerator(time.Now().UnixNano()) + fullNodeClients, err := cluster.ShuffledFullNodeClients(ctx, rnd) + if err != nil { + return fmt.Errorf("shuffled full node clients: %w", err) + } + + if len(fullNodeClients) < 2 { + return fmt.Errorf("socconvergence test requires at least 2 full nodes") + } + + node0 := fullNodeClients[0] + node1 := fullNodeClients[1] + + c.logger.Infof("socconvergence: using node0 '%s' and node1 '%s' (total full nodes: %d)", + node0.Name(), node1.Name(), len(fullNodeClients)) + + // Shared signer for same owner address + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return fmt.Errorf("generate secp256k1 key: %w", err) + } + signer := crypto.NewDefaultSigner(privKey) + pubKey, err := signer.PublicKey() + if err != nil { + return fmt.Errorf("signer public key: %w", err) + } + ownerBytes, err := crypto.NewEthereumAddress(*pubKey) + if err != nil { + return fmt.Errorf("ethereum address: %w", err) + } + ownerHex := hex.EncodeToString(ownerBytes) + + // Create postage batch on node0 + batchID0, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + c.logger.Infof("socconvergence: created batchID %s on node %s", batchID0, node0.Name()) + + // ------------------------------------------------------------------------- + // Scenario 1: Divergent SOC Tie-Break Convergence (SWIP-101) + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 1: Divergent SOC Tie-Break Convergence ---") + if err := c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { + return fmt.Errorf("scenario 1 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 2: Multi-Batch Divergent SOC Convergence + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 2: Multi-Batch Divergent SOC Convergence ---") + batchID1, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + if err := c.testMultiBatchDivergence(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 2 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 3: Identical Payload Checksum Matching + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 3: Identical Payload Checksum Matching ---") + if err := c.testIdenticalSOCChecksum(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { + return fmt.Errorf("scenario 3 failed: %w", err) + } + + c.logger.Infof("socconvergence: all integration test scenarios passed successfully!") + return nil +} + +func (c *Check) testDivergentSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + // Create 2 divergent payloads wrapping different CACs + payload1 := []byte("soc-convergence-payload-alpha-101") + payload2 := []byte("soc-convergence-payload-beta-101") + + ch1, err := cac.New(payload1) + if err != nil { + return fmt.Errorf("cac 1: %w", err) + } + ch2, err := cac.New(payload2) + if err != nil { + return fmt.Errorf("cac 2: %w", err) + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return fmt.Errorf("sign soc 1: %w", err) + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return fmt.Errorf("sign soc 2: %w", err) + } + + // Extract signatures + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + socAddress := soc1.Address() + c.logger.Infof("divergent soc address: %s", socAddress.String()) + + // Determine theoretical tie-break winner (lexicographically lower wrapped CAC) + wins, err := storage.DivergentChunkWins(soc1, soc2) + if err != nil { + return fmt.Errorf("divergent chunk wins comparison: %w", err) + } + + var expectedWinnerPayload []byte + var winnerName string + if wins { + expectedWinnerPayload = ch2.Data() + winnerName = "soc2 (payload2)" + } else { + expectedWinnerPayload = ch1.Data() + winnerName = "soc1 (payload1)" + } + c.logger.Infof("theoretical tie-break winner: %s", winnerName) + + // Upload soc1 to node0 + ref1, err := node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc1 to node %s: %w", node0.Name(), err) + } + c.logger.Infof("uploaded soc1 to node %s: ref %s", node0.Name(), ref1.String()) + + // Upload soc2 to node1 + ref2, err := node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc2 to node %s: %w", node1.Name(), err) + } + c.logger.Infof("uploaded soc2 to node %s: ref %s", node1.Name(), ref2.String()) + + // Wait for cluster pullsync convergence across all full nodes + c.logger.Infof("waiting for pullsync cluster convergence on address %s...", socAddress.String()) + return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) +} + +func (c *Check) testMultiBatchDivergence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + payload1 := []byte("multi-batch-payload-gamma-201") + payload2 := []byte("multi-batch-payload-delta-202") + + ch1, err := cac.New(payload1) + if err != nil { + return err + } + ch2, err := cac.New(payload2) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + socAddress := soc1.Address() + + wins, err := storage.DivergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedWinnerPayload []byte + if wins { + expectedWinnerPayload = ch2.Data() + } else { + expectedWinnerPayload = ch1.Data() + } + + // Upload to node0 under batchID0 + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return fmt.Errorf("upload soc1 multi-batch: %w", err) + } + + // Upload to node1 under batchID1 + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return fmt.Errorf("upload soc2 multi-batch: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) +} + +func (c *Check) testIdenticalSOCChecksum( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + payload := []byte("identical-soc-checksum-payload-301") + ch, err := cac.New(payload) + if err != nil { + return err + } + + sch, err := soc.New(idBytes, ch).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(sch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + socAddress := sch.Address() + + // Upload exact same SOC to node0 and node1 + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return fmt.Errorf("upload identical to node0: %w", err) + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return fmt.Errorf("upload identical to node1: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, socAddress, ch.Data(), o) +} + +func (c *Check) assertClusterConvergence( + ctx context.Context, + nodes []*bee.Client, + address swarm.Address, + expectedPayload []byte, + o Options, +) error { + ticker := time.NewTicker(o.SyncRetryInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) + case <-ticker.C: + allConverged := true + for _, node := range nodes { + retrieved, err := node.DownloadChunk(ctx, address, "", nil) + if err != nil { + allConverged = false + c.logger.Debugf("node %s: download %s error: %v", node.Name(), address.String(), err) + break + } + if !bytes.Contains(retrieved, expectedPayload) && !bytes.Equal(retrieved, expectedPayload) { + allConverged = false + c.logger.Debugf("node %s: data mismatch on %s", node.Name(), address.String()) + break + } + } + + if allConverged { + c.logger.Infof("all %d full nodes converged on address %s with expected payload!", len(nodes), address.String()) + return nil + } + } + } +} + +func randomID() ([]byte, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/check/socconvergence/socconvergence_test.go b/pkg/check/socconvergence/socconvergence_test.go new file mode 100644 index 000000000..2f8bbb2e5 --- /dev/null +++ b/pkg/check/socconvergence/socconvergence_test.go @@ -0,0 +1,34 @@ +// 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 socconvergence_test + +import ( + "io" + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/check/socconvergence" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestNewDefaultOptions(t *testing.T) { + opts := socconvergence.NewDefaultOptions() + if opts.PostageTTL != 24*time.Hour { + t.Fatalf("expected PostageTTL 24h, got %v", opts.PostageTTL) + } + if opts.PostageDepth != 16 { + t.Fatalf("expected PostageDepth 16, got %d", opts.PostageDepth) + } + if opts.RequestTimeout != 5*time.Minute { + t.Fatalf("expected RequestTimeout 5m, got %v", opts.RequestTimeout) + } +} + +func TestNewCheck(t *testing.T) { + chk := socconvergence.NewCheck(logging.New(io.Discard, 0)) + if chk == nil { + t.Fatal("expected non-nil check") + } +} diff --git a/pkg/config/check.go b/pkg/config/check.go index e6e65eb97..ad8b6d10d 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -35,6 +35,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/settlements" "github.com/ethersphere/beekeeper/pkg/check/smoke" "github.com/ethersphere/beekeeper/pkg/check/soc" + "github.com/ethersphere/beekeeper/pkg/check/socconvergence" "github.com/ethersphere/beekeeper/pkg/check/stake" "github.com/ethersphere/beekeeper/pkg/check/withdraw" "github.com/ethersphere/beekeeper/pkg/logging" @@ -529,6 +530,29 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "socconvergence": { + NewAction: socconvergence.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + GasPrice *string `yaml:"gas-price"` + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := socconvergence.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "postage": { NewAction: postage.NewCheck, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { diff --git a/pkg/config/soc_partition_test.go b/pkg/config/soc_partition_test.go index 79b484abb..d7472f3f8 100644 --- a/pkg/config/soc_partition_test.go +++ b/pkg/config/soc_partition_test.go @@ -59,7 +59,7 @@ func TestSOCPartitionClusterConfig(t *testing.T) { t.Fatalf("group-a bee config: %+v", a) } b, ok := cfg.BeeConfigs["bee-soc-partition-group-b"] - if !ok || b.Bootnodes == nil || *b.Bootnodes == "" { + if !ok || b.Bootnodes == nil || len(*b.Bootnodes) == 0 { t.Fatalf("group-b must have non-empty bootnodes: %+v", b) } if b.TargetNeighborhood == nil || *b.TargetNeighborhood != "11111100" { @@ -67,10 +67,10 @@ func TestSOCPartitionClusterConfig(t *testing.T) { } exported := b.Export() - if exported.TargetNeighborhood != "11111100" { - t.Fatalf("export target neighborhood: %q", exported.TargetNeighborhood) + if exported.TargetNeighborhood == nil || *exported.TargetNeighborhood != "11111100" { + t.Fatalf("export target neighborhood: %v", exported.TargetNeighborhood) } - if exported.Bootnodes == "" { + if exported.Bootnodes == nil || len(*exported.Bootnodes) == 0 { t.Fatal("export bootnodes empty") } } From f96d918983c54e1c86ebbd84ac949a675c752d83 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 29 Jul 2026 14:11:32 +0300 Subject: [PATCH 03/21] build(deps): remove local bee replace directive from go.mod --- go.mod | 3 --- go.sum | 36 ++++-------------------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index 5ef5b8516..62e6a8dcc 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,6 @@ go 1.26 replace github.com/codahale/hdrhistogram => github.com/HdrHistogram/hdrhistogram-go v1.1.2 -replace github.com/ethersphere/bee/v2 => ../bee - require ( github.com/ethereum/go-ethereum v1.17.3 github.com/ethersphere/bee/v2 v2.7.0 @@ -52,7 +50,6 @@ require ( github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect - github.com/codahale/hdrhistogram v0.0.0-00010101000000-000000000000 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect diff --git a/go.sum b/go.sum index b56d843b9..4cf9e0ef2 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= @@ -21,7 +19,6 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= @@ -101,6 +98,8 @@ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= +github.com/ethersphere/bee/v2 v2.7.0 h1:faaB3WP60SNrZvBCz2xh5sSIc9ka3Cz/ApWyedqBvQA= +github.com/ethersphere/bee/v2 v2.7.0/go.mod h1:6Hhfttnet1vQmSydC9bivOKtUmaCarT3Mqi9Slj2tkg= github.com/ethersphere/bmt v0.1.4 h1:+rkWYNtMgDx6bkNqGdWu+U9DgGI1rRZplpSW3YhBr1Q= github.com/ethersphere/bmt v0.1.4/go.mod h1:Yd8ft1U69WDuHevZc/rwPxUv1rzPSMpMnS6xbU53aY8= github.com/ethersphere/ethproxy v0.0.5 h1:j5Mkm45jqmkET6NwGaJtaxOSFbhoAfOKzHiwHl6DBT0= @@ -113,7 +112,6 @@ github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeD github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -136,7 +134,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -163,14 +160,12 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -228,7 +223,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -333,7 +327,6 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= @@ -500,8 +493,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -523,7 +516,6 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -533,18 +525,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= @@ -566,7 +548,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -595,10 +576,7 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -608,15 +586,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= @@ -649,7 +622,6 @@ lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= resenje.org/x v0.6.0 h1:afn9E4XhglF4y9Kq0VH5tdSyjnsVKxiYgB6HFj7ebss= resenje.org/x v0.6.0/go.mod h1:qgwe4MCzh57EkkMDurg24ug7HHfZtAjtBkmCihNmOpM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 3a58006638e4c7023d6c3dc37ca8a4363c0a1df6 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 29 Jul 2026 14:18:10 +0300 Subject: [PATCH 04/21] refactor(socconvergence): replace storage.DivergentChunkWins with local helper --- pkg/check/socconvergence/socconvergence.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/check/socconvergence/socconvergence.go b/pkg/check/socconvergence/socconvergence.go index d54c21d4a..8e9855b92 100644 --- a/pkg/check/socconvergence/socconvergence.go +++ b/pkg/check/socconvergence/socconvergence.go @@ -15,7 +15,6 @@ import ( "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/ethersphere/bee/v2/pkg/soc" - "github.com/ethersphere/bee/v2/pkg/storage" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee" "github.com/ethersphere/beekeeper/pkg/beekeeper" @@ -186,7 +185,7 @@ func (c *Check) testDivergentSOCTieBreak( c.logger.Infof("divergent soc address: %s", socAddress.String()) // Determine theoretical tie-break winner (lexicographically lower wrapped CAC) - wins, err := storage.DivergentChunkWins(soc1, soc2) + wins, err := divergentChunkWins(soc1, soc2) if err != nil { return fmt.Errorf("divergent chunk wins comparison: %w", err) } @@ -261,7 +260,7 @@ func (c *Check) testMultiBatchDivergence( sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) socAddress := soc1.Address() - wins, err := storage.DivergentChunkWins(soc1, soc2) + wins, err := divergentChunkWins(soc1, soc2) if err != nil { return err } @@ -375,3 +374,17 @@ func randomID() ([]byte, error) { } return key, nil } + +// divergentChunkWins reports whether ch2 wins over ch1 by having a +// lexicographically lower wrapped CAC address. +func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { + s1, err := soc.FromChunk(ch1) + if err != nil { + return false, fmt.Errorf("soc 1 from chunk: %w", err) + } + s2, err := soc.FromChunk(ch2) + if err != nil { + return false, fmt.Errorf("soc 2 from chunk: %w", err) + } + return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil +} From d5b86575c6c33586a9ea735a6c3207bc02d5cc1c Mon Sep 17 00:00:00 2001 From: 0xBECEDA Date: Wed, 29 Jul 2026 14:37:24 +0200 Subject: [PATCH 05/21] fix: scenarios description --- pkg/check/chunkconvergence/SCENARIOS.md | 91 +++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pkg/check/chunkconvergence/SCENARIOS.md diff --git a/pkg/check/chunkconvergence/SCENARIOS.md b/pkg/check/chunkconvergence/SCENARIOS.md new file mode 100644 index 000000000..f67299267 --- /dev/null +++ b/pkg/check/chunkconvergence/SCENARIOS.md @@ -0,0 +1,91 @@ +# Chunk convergence check scenarios + +Check: `ci-chunk-convergence` (`chunk-convergence`) + +**Common setup:** pick the closest full-node pair, upload conflicting chunks to the two nodes, wait for sync, then assert both nodes agree on the expected outcome. + +--- + +## 1. Divergent SOC, identical stamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | same (identical stamp on both uploads) | +| Stamp index | same | +| Stamp timestamp | same | + +**Expected:** the SOC with the lexicographically lower wrapped CAC wins. + +--- + +## 2. SOC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different (same index, different timestamp → different stamp hash) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 3. SOC, different index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different | +| Stamp index | different (two envelopes) | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 4. CAC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | different (two CACs in the same postage bucket) | +| Batch | same | +| Stamp | different (signed over different addresses + different timestamps) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher-timestamp CAC is present; the lower-timestamp CAC is absent. + +--- + +## 5. SOC, cross-batch, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | different | +| Stamp | different | +| Stamp index | different (independent envelopes in different batches) | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 6. SOC, cross-batch, equal timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | different | +| Stamp | different | +| Stamp index | different (independent envelopes; equality is not forced) | +| Stamp timestamp | same | + +**Expected:** the stamp with the lexicographically lower stamp hash wins. + +**Note:** equal timestamp does not mean the same stamp. The two claims are distinct postage stamps (different batches / stamp hashes) that share only the timestamp value. From 4efa20225aef5635a83e7220c6ba991717312a4c Mon Sep 17 00:00:00 2001 From: sbacked Date: Wed, 29 Jul 2026 14:37:24 +0200 Subject: [PATCH 06/21] fix: scenarios description --- pkg/check/chunkconvergence/SCENARIOS.md | 91 +++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pkg/check/chunkconvergence/SCENARIOS.md diff --git a/pkg/check/chunkconvergence/SCENARIOS.md b/pkg/check/chunkconvergence/SCENARIOS.md new file mode 100644 index 000000000..f67299267 --- /dev/null +++ b/pkg/check/chunkconvergence/SCENARIOS.md @@ -0,0 +1,91 @@ +# Chunk convergence check scenarios + +Check: `ci-chunk-convergence` (`chunk-convergence`) + +**Common setup:** pick the closest full-node pair, upload conflicting chunks to the two nodes, wait for sync, then assert both nodes agree on the expected outcome. + +--- + +## 1. Divergent SOC, identical stamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | same (identical stamp on both uploads) | +| Stamp index | same | +| Stamp timestamp | same | + +**Expected:** the SOC with the lexicographically lower wrapped CAC wins. + +--- + +## 2. SOC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different (same index, different timestamp → different stamp hash) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 3. SOC, different index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different | +| Stamp index | different (two envelopes) | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 4. CAC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | different (two CACs in the same postage bucket) | +| Batch | same | +| Stamp | different (signed over different addresses + different timestamps) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher-timestamp CAC is present; the lower-timestamp CAC is absent. + +--- + +## 5. SOC, cross-batch, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | different | +| Stamp | different | +| Stamp index | different (independent envelopes in different batches) | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 6. SOC, cross-batch, equal timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | different | +| Stamp | different | +| Stamp index | different (independent envelopes; equality is not forced) | +| Stamp timestamp | same | + +**Expected:** the stamp with the lexicographically lower stamp hash wins. + +**Note:** equal timestamp does not mean the same stamp. The two claims are distinct postage stamps (different batches / stamp hashes) that share only the timestamp value. From e2a095240592c4cfc70356bdf6e44c81e1aad658 Mon Sep 17 00:00:00 2001 From: sbackend Date: Tue, 4 Aug 2026 00:31:34 +0200 Subject: [PATCH 07/21] feat(chunk-convergence): replace scenario 5 with CAC equal-timestamp tie-break MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop isolated scenario 7 wiring; keep scenarios 1–5 with CAC same-index equal timestamp as the address tie-break case. --- pkg/check/chunkconvergence/SCENARIOS.md | 47 ++-- .../chunkconvergence/chunkconvergence.go | 253 +++++++++++------- 2 files changed, 180 insertions(+), 120 deletions(-) diff --git a/pkg/check/chunkconvergence/SCENARIOS.md b/pkg/check/chunkconvergence/SCENARIOS.md index f67299267..a698e6685 100644 --- a/pkg/check/chunkconvergence/SCENARIOS.md +++ b/pkg/check/chunkconvergence/SCENARIOS.md @@ -62,30 +62,23 @@ Check: `ci-chunk-convergence` (`chunk-convergence`) --- -## 5. SOC, cross-batch, higher timestamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | same | -| Batch | different | -| Stamp | different | -| Stamp index | different (independent envelopes in different batches) | -| Stamp timestamp | different | - -**Expected:** the higher timestamp wins. - ---- - -## 6. SOC, cross-batch, equal timestamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | same | -| Batch | different | -| Stamp | different | -| Stamp index | different (independent envelopes; equality is not forced) | -| Stamp timestamp | same | - -**Expected:** the stamp with the lexicographically lower stamp hash wins. - -**Note:** equal timestamp does not mean the same stamp. The two claims are distinct postage stamps (different batches / stamp hashes) that share only the timestamp value. +## 5. CAC, same index, equal timestamp + +| Field | Same / different | +| --------------- | -------------------------------------------------------------------------------- | +| Address | different (two CACs in the same postage bucket) | +| Batch | same | +| Stamp | same batch / index / timestamp; signatures differ (each stamp is bound to its CAC address) | +| Stamp index | same | +| Stamp timestamp | same | + +**Expected:** the lexicographically lower CAC address wins; the higher-address CAC is absent. + +```bash +# from beekeeper repo, with cluster local-dns up and context k3d-bee +./dist/beekeeper check \ + --config config/beekeeper-local.yaml \ + --config-dir config \ + --cluster-name local-dns \ + --checks ci-chunk-convergence +``` diff --git a/pkg/check/chunkconvergence/chunkconvergence.go b/pkg/check/chunkconvergence/chunkconvergence.go index c9f1de0de..b11f2c413 100644 --- a/pkg/check/chunkconvergence/chunkconvergence.go +++ b/pkg/check/chunkconvergence/chunkconvergence.go @@ -105,11 +105,8 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any {"4 cac same index higher timestamp", func() error { return c.scenarioCACSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) }}, - {"5 soc cross-batch higher timestamp", func() error { - return c.scenarioSOCCrossBatchTimestamp(ctx, cluster, neighborhood, o) - }}, - {"6 soc cross-batch equal timestamp", func() error { - return c.scenarioSOCEqualTimestampTieBreak(ctx, issuer, signer, neighborhood, batchID, o, o.SyncWait) + {"5 cac same index equal timestamp", func() error { + return c.scenarioCACSameIndexEqualTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) }}, } @@ -338,91 +335,88 @@ func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.C return nil } -func (c *Check) scenarioSOCCrossBatchTimestamp(ctx context.Context, cluster orchestration.Cluster, neighborhood []*bee.Client, o Options) error { - clients, err := cluster.NodesClients(ctx) +func (c *Check) scenarioCACSameIndexEqualTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) if err != nil { return err } - var issuerA, issuerB *bee.Client - for _, cl := range clients { - if issuerA == nil { - issuerA = cl - continue - } - if issuerB == nil && cl.Name() != issuerA.Name() { - issuerB = cl - break - } - } - if issuerA == nil || issuerB == nil { - return fmt.Errorf("need two nodes to create two batches") + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err } - - batchA, err := issuerA.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-a-"+hex.EncodeToString(randomBytes(3))) + batchBytes, err := hex.DecodeString(batchID) if err != nil { - return fmt.Errorf("batch A: %w", err) + return err } - batchB, err := issuerB.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-b-"+hex.EncodeToString(randomBytes(3))) + // Same batch, stamp index and timestamp; signatures differ (each stamp is + // bound to its CAC address). At equal timestamp the lower address wins. + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) if err != nil { - return fmt.Errorf("batch B: %w", err) + return fmt.Errorf("envelope for index: %w", err) } + index := append([]byte{}, env.Index()...) + ts := uint64ToBytes(binary.BigEndian.Uint64(env.Timestamp())) - signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + stampLow, err := signStamp(batchSigner, batchBytes, index, ts, low.Address()) if err != nil { return err } - older, err := buildSOC(signer, id, append([]byte("soc-xbatch-older-"), randomBytes(4)...)) + stampHigh, err := signStamp(batchSigner, batchBytes, index, ts, high.Address()) if err != nil { return err } - newer, err := buildSOC(signer, id, append([]byte("soc-xbatch-newer-"), randomBytes(4)...)) - if err != nil { + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s equal_ts address tie-break", low.Address(), high.Address(), hex.EncodeToString(index)) + // Upload loser (higher address) first, then winner (lower address). + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], high, low); err != nil { return err } + time.Sleep(syncWait) - stampOld, err := issuerA.CreateEnvelope(ctx, addr, batchA) - if err != nil { - return fmt.Errorf("envelope A: %w", err) - } - stampNew, err := issuerB.CreateEnvelope(ctx, addr, batchB) - if err != nil { - return fmt.Errorf("envelope B: %w", err) - } - for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { - stampNew, err = issuerB.CreateEnvelope(ctx, addr, batchB) + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, low.Address()) if err != nil { - return fmt.Errorf("envelope B retry: %w", err) + return err + } + hasLose, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing lower-address CAC %s", n.Name(), low.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has higher-address CAC %s", n.Name(), high.Address()) } } - older = older.WithStamp(stampOld) - newer = newer.WithStamp(stampNew) - oldBytes, _ := stampOld.MarshalBinary() - newBytes, _ := stampNew.MarshalBinary() - - c.logger.Infof("soc=%s batchA=%s… batchB=%s…", addr, batchA[:8], batchB[:8]) - if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { - return err - } - time.Sleep(o.SyncWait) - return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) + return nil } -func (c *Check) scenarioSOCEqualTimestampTieBreak(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { - // Two batches from the same issuer (bootnode) so one key can sign stamps for both. - batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-eqts-"+hex.EncodeToString(randomBytes(3))) +// scenarioSOCStaleStampRedeliveryRace races three divergent SOCs at one address: +// +// SOC₁ under stamp S (batch A, lower timestamp) → uploaded to Sam +// SOC₂ under stamp T (batch B, higher timestamp) → uploaded to Sara +// SOC₃ reuses stamp S with payload P₃ whose wrapped CAC beats P₁ and P₂ → +// uploaded to both nodes in parallel with the SOC₁/SOC₂ uploads +// +// After sync, every neighborhood node must hold P₃ (wrapped-CAC winner under +// the reused stamp S / resolveDivergence). +func (c *Check) scenarioSOCStaleStampRedeliveryRace(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { + sam, sara := neighborhood[0], neighborhood[1] + + batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-stale-"+hex.EncodeToString(randomBytes(3))) if err != nil { return fmt.Errorf("batch B: %w", err) } - signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + signer, owner, id, addr, err := mineSOC(ctx, sam, sara) if err != nil { return err } - a, err := buildSOC(signer, id, append([]byte("soc-eqts-a-"), randomBytes(4)...)) - if err != nil { - return err - } - b, err := buildSOC(signer, id, append([]byte("soc-eqts-b-"), randomBytes(4)...)) + soc1, soc2, soc3, err := orderedStaleRaceSOCs(signer, id) if err != nil { return err } @@ -435,59 +429,132 @@ func (c *Check) scenarioSOCEqualTimestampTieBreak(ctx context.Context, issuer *b if err != nil { return err } - // Valid indexes from real envelopes on each batch; equal timestamps for stampHash tie-break. - envA, err := issuer.CreateEnvelope(ctx, addr, batchID) + + envS, err := issuer.CreateEnvelope(ctx, addr, batchID) if err != nil { - return fmt.Errorf("envelope A: %w", err) + return fmt.Errorf("envelope stamp S: %w", err) } - envB, err := issuer.CreateEnvelope(ctx, addr, batchB) + envT, err := issuer.CreateEnvelope(ctx, addr, batchB) if err != nil { - return fmt.Errorf("envelope B: %w", err) + return fmt.Errorf("envelope stamp T: %w", err) } - ts := binary.BigEndian.Uint64(envA.Timestamp()) - if tsb := binary.BigEndian.Uint64(envB.Timestamp()); tsb > ts { - ts = tsb + tsS := binary.BigEndian.Uint64(envS.Timestamp()) + tsT := binary.BigEndian.Uint64(envT.Timestamp()) + if tsT <= tsS { + tsT = tsS + 1 } - tsBytes := uint64ToBytes(ts) - idxA := append([]byte{}, envA.Index()...) - idxB := append([]byte{}, envB.Index()...) - - stampA, err := signStamp(batchSigner, batchABytes, idxA, tsBytes, addr) + stampS, err := signStamp(batchSigner, batchABytes, append([]byte{}, envS.Index()...), uint64ToBytes(tsS), addr) if err != nil { - return err + return fmt.Errorf("sign stamp S: %w", err) } - stampB, err := signStamp(batchSigner, batchBBytes, idxB, tsBytes, addr) + stampT, err := signStamp(batchSigner, batchBBytes, append([]byte{}, envT.Index()...), uint64ToBytes(tsT), addr) if err != nil { - return err + return fmt.Errorf("sign stamp T: %w", err) } - hashA, err := stampA.Hash() + stampSBytes, err := stampS.MarshalBinary() if err != nil { return err } - hashB, err := stampB.Hash() + stampTBytes, err := stampT.MarshalBinary() if err != nil { return err } - var winner, loser swarm.Chunk - var winStamp, loseStamp []byte - if bytes.Compare(hashA, hashB) < 0 { - winner, loser = a.WithStamp(stampA), b.WithStamp(stampB) - winStamp, _ = stampA.MarshalBinary() - loseStamp, _ = stampB.MarshalBinary() - } else { - winner, loser = b.WithStamp(stampB), a.WithStamp(stampA) - winStamp, _ = stampB.MarshalBinary() - loseStamp, _ = stampA.MarshalBinary() + c.logger.Infof( + "soc=%s stale-stamp race sam=%s sara=%s wrapped_p1=%s wrapped_p2=%s wrapped_p3=%s batchA=%s… batchB=%s… tsS=%d tsT=%d", + addr, sam.Name(), sara.Name(), mustWrapped(soc1), mustWrapped(soc2), mustWrapped(soc3), + batchID[:8], batchB[:8], tsS, tsT, + ) + + type uploadResult struct { + label string + err error } + results := make(chan uploadResult, 4) - c.logger.Infof("soc=%s cross-batch equal_ts lower stampHash wins batchA=%s… batchB=%s…", addr, batchID[:8], batchB[:8]) - // Upload loser first, then winner. - if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, loser, winner, loseStamp, winStamp, true); err != nil { - return err + go func() { + results <- uploadResult{"SOC1→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc1, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC2→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc2, stampTBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc3, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc3, stampSBytes)} + }() + + for range 4 { + r := <-results + if r.err != nil { + c.logger.Infof("upload %s returned: %v", r.label, r.err) + } else { + c.logger.Infof("upload %s ok", r.label) + } } + c.logger.Infof("race uploads finished; waiting %s for neighborhood to settle on P3", syncWait) + time.Sleep(syncWait) - return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, winner.Data()) + + for _, n := range neighborhood { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + c.logger.Errorf("%s download after race: %v", n.Name(), err) + continue + } + switch { + case bytes.Equal(got, soc3.Data()): + c.logger.Infof("%s holds P3 — expected", n.Name()) + case bytes.Equal(got, soc2.Data()): + c.logger.Errorf("%s holds P2 (stamp T) — want P3", n.Name()) + case bytes.Equal(got, soc1.Data()): + c.logger.Errorf("%s holds P1 — want P3", n.Name()) + default: + c.logger.Errorf("%s holds unexpected payload len=%d", n.Name(), len(got)) + } + } + + if err := assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, soc3.Data()); err != nil { + return fmt.Errorf("after stale-stamp race (want P3 on both): %w", err) + } + return nil +} + +// orderedStaleRaceSOCs builds three SOCs at the same address where P₃'s wrapped +// CAC is lexicographically lower than both P₁ and P₂, so resolveDivergence would +// accept P₃ over either stored payload when the stale-stamp guard is absent. +func orderedStaleRaceSOCs(signer crypto.Signer, id []byte) (p1, p2, p3 swarm.Chunk, err error) { + for range 10_000 { + p1, err = buildSOC(signer, id, append([]byte("stale-p1-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p2, err = buildSOC(signer, id, append([]byte("stale-p2-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p3, err = buildSOC(signer, id, append([]byte("stale-p3-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + w1, w2, w3 := mustWrapped(p1).Bytes(), mustWrapped(p2).Bytes(), mustWrapped(p3).Bytes() + if bytes.Compare(w3, w1) < 0 && bytes.Compare(w3, w2) < 0 && + !bytes.Equal(w1, w2) && !bytes.Equal(w1, w3) && !bytes.Equal(w2, w3) { + return p1, p2, p3, nil + } + } + return nil, nil, nil, fmt.Errorf("could not mine P1/P2/P3 with wrapped(P3) lower than both") +} + +func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch swarm.Chunk, stamp []byte) error { + sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { + return fmt.Errorf("upload to %s: %w", n.Name(), err) + } + return nil } // uploadSOCPair stores ch1 on n1 then ch2 on n2. Callers should pass the From 2536fd0e1c35943b742146a42e0875849f5dc38b Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 4 Aug 2026 11:09:41 +0300 Subject: [PATCH 08/21] test(socmatrix): add socmatrix check and multi-stamp precedence scenarios --- config/config.yaml | 10 + config/local.yaml | 10 + config/soc-partition.yaml | 70 ++ pkg/bee/api/api.go | 3 + pkg/bee/api/chunks.go | 6 +- pkg/bee/api/envelope.go | 53 ++ pkg/bee/api/node.go | 8 +- pkg/bee/api/options.go | 1 + pkg/bee/api/soc.go | 21 +- pkg/bee/client.go | 19 +- pkg/check/chunkconvergence/SCENARIOS.md | 84 ++ .../chunkconvergence/chunkconvergence.go | 783 ++++++++++++++++++ pkg/check/socconvergence/socconvergence.go | 477 +++++++++++ .../socconvergence/socconvergence_test.go | 34 + pkg/check/socmatrix/socmatrix.go | 769 +++++++++++++++++ pkg/check/socmatrix/socmatrix_test.go | 34 + pkg/config/check.go | 72 ++ pkg/config/soc_partition_test.go | 76 ++ pkg/orchestration/key.go | 46 + scripts/genpartitionkeys/main.go | 143 ++++ 20 files changed, 2708 insertions(+), 11 deletions(-) create mode 100644 config/soc-partition.yaml create mode 100644 pkg/bee/api/envelope.go create mode 100644 pkg/check/chunkconvergence/SCENARIOS.md create mode 100644 pkg/check/chunkconvergence/chunkconvergence.go create mode 100644 pkg/check/socconvergence/socconvergence.go create mode 100644 pkg/check/socconvergence/socconvergence_test.go create mode 100644 pkg/check/socmatrix/socmatrix.go create mode 100644 pkg/check/socmatrix/socmatrix_test.go create mode 100644 pkg/config/soc_partition_test.go create mode 100644 scripts/genpartitionkeys/main.go diff --git a/config/config.yaml b/config/config.yaml index d214b8447..c2487912c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -355,6 +355,16 @@ checks: request-timeout: 5m timeout: 5m type: soc + chunk-convergence: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: chunk-convergence + sync-wait: 45s + request-timeout: 15m + password: beekeeper + timeout: 20m + type: chunk-convergence stake: type: stake timeout: 5m diff --git a/config/local.yaml b/config/local.yaml index d389ee2b3..03043d2dc 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -382,6 +382,16 @@ checks: request-timeout: 5m timeout: 5m type: soc + ci-chunk-convergence: + options: + postage-ttl: 24h + postage-depth: 21 + postage-label: chunk-convergence + sync-wait: 45s + request-timeout: 15m + password: beekeeper + timeout: 20m + type: chunk-convergence ci-content-availability: type: content-availability timeout: 5m diff --git a/config/soc-partition.yaml b/config/soc-partition.yaml new file mode 100644 index 000000000..f075faa13 --- /dev/null +++ b/config/soc-partition.yaml @@ -0,0 +1,70 @@ +# local-soc-partition — 5-node cluster for divergent-SOC / pullsync convergence tests. +# +# Topology (before the cross-group /connect): +# +# bootnode-0 +# │ +# group-a-0 ── group-a-1 group-b-0 ── group-b-1 +# (bootnodes → bootnode) (bootnodes → group-b-0 only) +# ✕ no P2P between A and B +# +# All four full nodes share target-neighborhood=11111100 so their overlays are +# mined into the same Swarm neighborhood. After you POST /connect between the +# groups they form one connected neighborhood and can pullsync. +# +# Create: +# beekeeper create bee-cluster --cluster-name=local-soc-partition +# +# Verify isolation (A must not list B overlays in /peers and vice versa), then +# upload conflicting SOC variants to each group. Heal with e.g.: +# curl -X POST "http://group-a-0.localhost:1633/connect/dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR" +# +# Libp2p keys for group-b were generated with: +# go run ./scripts/genpartitionkeys/ + +clusters: + local-soc-partition: + _inherit: "local" + node-groups: + bootnode: + mode: bootnode + bee-config: bootnode-local-dns + config: local-dns + nodes: + - name: bootnode-0 + bootnodes: /dns4/bootnode-0-headless.%s.svc.cluster.local/tcp/1634/p2p/QmaHzvd3iZduu275CMkMVZKwbsjXSyH3GJRj4UvFJApKcb + libp2p-key: '{"address":"28678fe31f09f722d53e77ca2395569f19959fa5","crypto":{"cipher":"aes-128-ctr","ciphertext":"0ff319684c4f8decf9c998047febe3417cfc45832b8bb62fd818183d54cf5d0183bfa021ed95addce3b33e83ce7ee73e926f00eea8241d96b349266a4d299829d3d22db0d536315b52b34db4a6778bfd3ce7631ad7256ea0bb9c50abea9de35d740b6fdc50caf929b1d19494690d9ed649105d02c14f5ec49d","cipherparams":{"iv":"4e9a50fb5852b5e61964f696be78066b"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"4d513e81647e4150bb648ed8d2dda28d460802336bf24d620119eac66ae0c0c4"},"mac":"9ae71db96e5ddc1c214538d42082212bbbe53aeac09fcc3e3a8eff815648331e"},"version":3,"id":"ae3bc991-d89f-405a-9e6a-60e27347e22d"}' + swarm-key: '{"address":"f176839c150e52fe30e5c2b5c648465c6fdfa532","crypto":{"cipher":"aes-128-ctr","ciphertext":"352af096f0fca9dfbd20a6861bde43d988efe7f179e0a9ffd812a285fdcd63b9","cipherparams":{"iv":"613003f1f1bf93430c92629da33f8828"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"ad1d99a4c64c95c26131e079e8c8a82221d58bf66a7ceb767c33a4c376c564b8"},"mac":"cafda1bc8ca0ffc2b22eb69afd1cf5072fd09412243443be1b0c6832f57924b6"},"version":3}' + group-a: + mode: node + bee-config: bee-soc-partition-group-a + config: local-dns + count: 2 + group-b: + mode: node + bee-config: bee-soc-partition-group-b + config: local-dns + nodes: + # Soft bootnode for the B island. Peer ID must match libp2p-key. + - name: group-b-0 + libp2p-key: '{"address":"dbc4584b50b571e4c426048ca30f3bbf31256664","crypto":{"cipher":"aes-128-ctr","ciphertext":"9e0fb2432abc8d5a591e62e9e79bb028564e6de172b3ad3acacceec01c52bf1c","cipherparams":{"iv":"e6421bbcbaee2afe1cde22cb9c87fe6a"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"e707f2cc3b18e98b4901df78afff7d22c42ee8bd5ffb4aa2692c327a406fda48"},"mac":"14516bd67bc56c8722b5784e9de3844a10eefedfcd2746deacd8eba46e834aa9"},"version":3,"id":"a3f5c9e7-e116-4c9b-b605-f007b0d94589"}' + - name: group-b-1 + libp2p-key: '{"address":"f64dc5d1a2233919f903a5582dd1d5b0b24cd255","crypto":{"cipher":"aes-128-ctr","ciphertext":"69eaf4a5a65992b0ac69f0a73c768cff8507e0d3538629cfbf46ce3cc57d6270","cipherparams":{"iv":"13a5007d75c76d353d7ee05913fc6905"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"69b56b0b71cef94a143b8cea095d8c2ee08a27d53cc99e007e8a5a36fd4f1c08"},"mac":"3514368a10020d238af1c5af42b134d1b179e59e4acbb8f4ded938178cdec8eb"},"version":3,"id":"e66e3d13-db28-455b-a02d-98f3d7a6508f"}' + +bee-configs: + # Joins the main bootnode (empty bootnodes → filled from bootnode group). + bee-soc-partition-group-a: + _inherit: "bee-local-dns" + full-node: true + target-neighborhood: "11111100" + welcome-message: "soc-partition group-a" + + # Isolated from the main bootnode: dials group-b-0 instead (soft bootnode). + # Non-empty bootnodes is required so beekeeper does not inject bootnode-0. + bee-soc-partition-group-b: + _inherit: "bee-local-dns" + full-node: true + bootnode: + - /dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR + target-neighborhood: "11111100" + welcome-message: "soc-partition group-b" diff --git a/pkg/bee/api/api.go b/pkg/bee/api/api.go index f943fc33b..48158fdf2 100644 --- a/pkg/bee/api/api.go +++ b/pkg/bee/api/api.go @@ -17,6 +17,7 @@ const ( apiVersion = "v1" contentType = "application/json, text/plain, */*; charset=utf-8" postageStampBatchHeader = "Swarm-Postage-Batch-Id" + postageStampHeader = "Swarm-Postage-Stamp" deferredUploadHeader = "Swarm-Deferred-Upload" swarmAct = "Swarm-Act" swarmActHistoryAddress = "Swarm-Act-History-Address" @@ -46,6 +47,7 @@ type Client struct { Bytes *BytesService Chunks *ChunksService Dirs *DirsService + Envelope *EnvelopeService Feed *FeedService Files *FilesService Node *NodeService @@ -96,6 +98,7 @@ func newClient(apiURL *url.URL, httpClient *http.Client) (c *Client) { c.Bytes = (*BytesService)(&c.service) c.Chunks = (*ChunksService)(&c.service) c.Dirs = (*DirsService)(&c.service) + c.Envelope = (*EnvelopeService)(&c.service) c.Feed = (*FeedService)(&c.service) c.Files = (*FilesService)(&c.service) c.Node = (*NodeService)(&c.service) diff --git a/pkg/bee/api/chunks.go b/pkg/bee/api/chunks.go index e384b6e3d..7d257feb6 100644 --- a/pkg/bee/api/chunks.go +++ b/pkg/bee/api/chunks.go @@ -36,7 +36,11 @@ func (c *ChunksService) Upload(ctx context.Context, data []byte, o UploadOptions if o.Direct { h.Add(deferredUploadHeader, "false") } - h.Add(postageStampBatchHeader, o.BatchID) + if o.Stamp != "" { + h.Add(postageStampHeader, o.Stamp) + } else { + h.Add(postageStampBatchHeader, o.BatchID) + } err := c.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/chunks", h, bytes.NewReader(data), &resp) return resp, err } diff --git a/pkg/bee/api/envelope.go b/pkg/bee/api/envelope.go new file mode 100644 index 000000000..0021f9361 --- /dev/null +++ b/pkg/bee/api/envelope.go @@ -0,0 +1,53 @@ +package api + +import ( + "context" + "encoding/hex" + "fmt" + "net/http" + + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/swarm" +) + +// EnvelopeService represents Bee's postage envelope service. +type EnvelopeService service + +// EnvelopeResponse is the JSON body returned by POST /envelope/{address}. +type EnvelopeResponse struct { + Issuer string `json:"issuer"` + Index string `json:"index"` + Timestamp string `json:"timestamp"` + Signature string `json:"signature"` +} + +// Create requests a postage stamp for the given chunk address from the node's +// stamper for batchID. The stamp is returned as a postage.Stamp ready for upload. +func (e *EnvelopeService) Create(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + h := http.Header{} + h.Add(postageStampBatchHeader, batchID) + + var resp EnvelopeResponse + err := e.client.requestWithHeader(ctx, http.MethodPost, "/"+apiVersion+"/envelope/"+addr.String(), h, nil, &resp) + if err != nil { + return nil, err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return nil, fmt.Errorf("decode batch id: %w", err) + } + index, err := hex.DecodeString(resp.Index) + if err != nil { + return nil, fmt.Errorf("decode stamp index: %w", err) + } + ts, err := hex.DecodeString(resp.Timestamp) + if err != nil { + return nil, fmt.Errorf("decode stamp timestamp: %w", err) + } + sig, err := hex.DecodeString(resp.Signature) + if err != nil { + return nil, fmt.Errorf("decode stamp signature: %w", err) + } + return postage.NewStamp(batchBytes, index, ts, sig), nil +} diff --git a/pkg/bee/api/node.go b/pkg/bee/api/node.go index 789eeccc3..16bf172ab 100644 --- a/pkg/bee/api/node.go +++ b/pkg/bee/api/node.go @@ -80,12 +80,8 @@ func (n *NodeService) Balances(ctx context.Context) (resp Balances, err error) { // HasChunk returns true/false if node has a chunk func (n *NodeService) HasChunk(ctx context.Context, a swarm.Address) (bool, error) { - resp := struct { - Message string `json:"message,omitempty"` - Code int `json:"code,omitempty"` - }{} - - err := n.client.requestJSON(ctx, http.MethodGet, "/chunks/"+a.String(), nil, &resp) + // HEAD must not decode a JSON body (response is empty); nil target avoids EOF. + err := n.client.requestJSON(ctx, http.MethodHead, "/chunks/"+a.String(), nil, nil) if IsHTTPStatusErrorCode(err, http.StatusNotFound) { return false, nil } else if err != nil { diff --git a/pkg/bee/api/options.go b/pkg/bee/api/options.go index a88e7bcd6..a1a81f5de 100644 --- a/pkg/bee/api/options.go +++ b/pkg/bee/api/options.go @@ -10,6 +10,7 @@ type UploadOptions struct { Pin bool Tag uint64 BatchID string + Stamp string // hex-encoded postage stamp; when set, BatchID is ignored Direct bool ActHistoryAddress swarm.Address RLevel *redundancy.Level diff --git a/pkg/bee/api/soc.go b/pkg/bee/api/soc.go index 1fc84bd9b..c8f64f816 100644 --- a/pkg/bee/api/soc.go +++ b/pkg/bee/api/soc.go @@ -9,17 +9,32 @@ import ( "github.com/ethersphere/bee/v2/pkg/swarm" ) -// PSSService represents Bee's PSS service +// SOCService represents Bee's SOC service. type SOCService service +// SocResponse is the JSON body returned by POST /soc. type SocResponse struct { - Reference swarm.Address + Reference swarm.Address `json:"reference"` } -// Sends a PSS message to a recipienct with a specific topic +// UploadSOC uploads a single owner chunk using a postage batch ID. func (p *SOCService) UploadSOC(ctx context.Context, owner, ID, signature string, data io.Reader, batchID string) (*SocResponse, error) { h := http.Header{} h.Add(postageStampBatchHeader, batchID) + h.Add(deferredUploadHeader, "false") + url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) + + resp := SocResponse{} + return &resp, p.client.requestWithHeader(ctx, http.MethodPost, url, h, data, &resp) +} + +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp +// (hex-encoded Swarm-Postage-Stamp header). Direct upload is forced so the chunk +// enters the reserve immediately for pullsync. +func (p *SOCService) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data io.Reader, stampHex string) (*SocResponse, error) { + h := http.Header{} + h.Add(postageStampHeader, stampHex) + h.Add(deferredUploadHeader, "false") url := fmt.Sprintf("/%s/soc/%s/%s?sig=%s", apiVersion, owner, ID, signature) resp := SocResponse{} diff --git a/pkg/bee/client.go b/pkg/bee/client.go index 711ffeb65..cc8b5e57b 100644 --- a/pkg/bee/client.go +++ b/pkg/bee/client.go @@ -3,6 +3,7 @@ package bee import ( "bytes" "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -17,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee/api" "github.com/ethersphere/beekeeper/pkg/logging" @@ -640,7 +642,7 @@ func (c *Client) SendPSSMessage(ctx context.Context, nodeAddress swarm.Address, return c.api.PSS.SendMessage(ctx, nodeAddress, publicKey, topic, prefix, bytes.NewReader(data), batchID) } -// UploadSOC uploads a single owner chunk to a node with a E +// UploadSOC uploads a single owner chunk to a node with a batch ID. func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, data []byte, batchID string) (swarm.Address, error) { resp, err := c.api.SOC.UploadSOC(ctx, owner, ID, signature, bytes.NewReader(data), batchID) if err != nil { @@ -650,6 +652,21 @@ func (c *Client) UploadSOC(ctx context.Context, owner, ID, signature string, dat return resp.Reference, nil } +// UploadSOCWithStamp uploads a single owner chunk using a precomputed postage stamp. +func (c *Client) UploadSOCWithStamp(ctx context.Context, owner, ID, signature string, data []byte, stamp []byte) (swarm.Address, error) { + resp, err := c.api.SOC.UploadSOCWithStamp(ctx, owner, ID, signature, bytes.NewReader(data), hex.EncodeToString(stamp)) + if err != nil { + return swarm.ZeroAddress, err + } + + return resp.Reference, nil +} + +// CreateEnvelope requests a postage stamp for addr from the node's stamper. +func (c *Client) CreateEnvelope(ctx context.Context, addr swarm.Address, batchID string) (*postage.Stamp, error) { + return c.api.Envelope.Create(ctx, addr, batchID) +} + // Settlements represents Settlements's response type Settlements struct { Settlements []Settlement diff --git a/pkg/check/chunkconvergence/SCENARIOS.md b/pkg/check/chunkconvergence/SCENARIOS.md new file mode 100644 index 000000000..a698e6685 --- /dev/null +++ b/pkg/check/chunkconvergence/SCENARIOS.md @@ -0,0 +1,84 @@ +# Chunk convergence check scenarios + +Check: `ci-chunk-convergence` (`chunk-convergence`) + +**Common setup:** pick the closest full-node pair, upload conflicting chunks to the two nodes, wait for sync, then assert both nodes agree on the expected outcome. + +--- + +## 1. Divergent SOC, identical stamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | same (identical stamp on both uploads) | +| Stamp index | same | +| Stamp timestamp | same | + +**Expected:** the SOC with the lexicographically lower wrapped CAC wins. + +--- + +## 2. SOC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different (same index, different timestamp → different stamp hash) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 3. SOC, different index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | same | +| Batch | same | +| Stamp | different | +| Stamp index | different (two envelopes) | +| Stamp timestamp | different | + +**Expected:** the higher timestamp wins. + +--- + +## 4. CAC, same index, higher timestamp + +| Field | Same / different | +| --------------- | ------------------------------------------------------------------ | +| Address | different (two CACs in the same postage bucket) | +| Batch | same | +| Stamp | different (signed over different addresses + different timestamps) | +| Stamp index | same | +| Stamp timestamp | different | + +**Expected:** the higher-timestamp CAC is present; the lower-timestamp CAC is absent. + +--- + +## 5. CAC, same index, equal timestamp + +| Field | Same / different | +| --------------- | -------------------------------------------------------------------------------- | +| Address | different (two CACs in the same postage bucket) | +| Batch | same | +| Stamp | same batch / index / timestamp; signatures differ (each stamp is bound to its CAC address) | +| Stamp index | same | +| Stamp timestamp | same | + +**Expected:** the lexicographically lower CAC address wins; the higher-address CAC is absent. + +```bash +# from beekeeper repo, with cluster local-dns up and context k3d-bee +./dist/beekeeper check \ + --config config/beekeeper-local.yaml \ + --config-dir config \ + --cluster-name local-dns \ + --checks ci-chunk-convergence +``` diff --git a/pkg/check/chunkconvergence/chunkconvergence.go b/pkg/check/chunkconvergence/chunkconvergence.go new file mode 100644 index 000000000..c6258db1c --- /dev/null +++ b/pkg/check/chunkconvergence/chunkconvergence.go @@ -0,0 +1,783 @@ +package chunkconvergence + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/bee/api" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +const bucketDepth = postage.BucketDepth + +// Options represents check options. +type Options struct { + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + SyncWait time.Duration + RequestTimeout time.Duration + Password string // password for decrypting the batch-issuer Swarm key +} + +// NewDefaultOptions returns new default options. +func NewDefaultOptions() Options { + return Options{ + PostageTTL: 24 * time.Hour, + PostageDepth: 21, + PostageLabel: "chunk-convergence", + SyncWait: 45 * time.Second, + RequestTimeout: 10 * time.Minute, + Password: "beekeeper", + } +} + +var _ beekeeper.Action = (*Check)(nil) + +// Check verifies pullsync convergence for conflicting SOC/CAC stamp claims. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{logger: logger} +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + fullNodes, err := cluster.ShuffledFullNodeClients(ctx, random.PseudoGenerator(time.Now().UnixNano())) + if err != nil { + return fmt.Errorf("full node clients: %w", err) + } + if len(fullNodes) < 2 { + return fmt.Errorf("chunk convergence requires at least 2 full nodes, got %d", len(fullNodes)) + } + + n1, n2, err := pickClosestPair(ctx, fullNodes) + if err != nil { + return err + } + c.logger.Infof("neighborhood upload pair: %s and %s", n1.Name(), n2.Name()) + neighborhood := []*bee.Client{n1, n2} + + issuer, signer, batchID, err := c.setupIssuerBatch(ctx, cluster, o) + if err != nil { + return err + } + c.logger.Infof("batch issuer=%s batch_id=%s", issuer.Name(), batchID) + + scenarios := []struct { + name string + run func() error + }{ + {"1 divergent soc identical stamp", func() error { + return c.scenarioDivergentSOC(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"2 soc same index higher timestamp", func() error { + return c.scenarioSOCSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) + }}, + {"3 soc different index higher timestamp", func() error { + return c.scenarioSOCDifferentIndexTimestamp(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"4 cac same index higher timestamp", func() error { + return c.scenarioCACSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) + }}, + {"5 cac same index equal timestamp", func() error { + return c.scenarioCACSameIndexEqualTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) + }}, + {"6 soc stale stamp redelivery race", func() error { + return c.scenarioSOCStaleStampRedeliveryRace(ctx, issuer, signer, neighborhood, batchID, o, o.SyncWait) + }}, + } + + var errs error + for _, s := range scenarios { + c.logger.Infof("========== %s ==========", s.name) + if err := s.run(); err != nil { + c.logger.Errorf("%s failed: %v", s.name, err) + errs = errors.Join(errs, fmt.Errorf("%s: %w", s.name, err)) + continue + } + c.logger.Infof("%s passed", s.name) + } + return errs +} + +func (c *Check) setupIssuerBatch(ctx context.Context, cluster orchestration.Cluster, o Options) (*bee.Client, crypto.Signer, string, error) { + nodes := cluster.Nodes() + var issuerNode orchestration.Node + var issuerName string + for name, n := range nodes { + if n.SwarmKey() != nil { + issuerNode = n + issuerName = name + break + } + } + if issuerNode == nil { + return nil, nil, "", fmt.Errorf("no node with swarm-key found; bootnode with configured swarm-key is required") + } + + privKey, err := issuerNode.SwarmKey().Decrypt(o.Password) + if err != nil { + return nil, nil, "", fmt.Errorf("decrypt swarm key for %s: %w", issuerName, err) + } + signer := crypto.NewDefaultSigner(privKey) + + clients, err := cluster.NodesClients(ctx) + if err != nil { + return nil, nil, "", fmt.Errorf("nodes clients: %w", err) + } + issuer, ok := clients[issuerName] + if !ok { + return nil, nil, "", fmt.Errorf("client for issuer node %s not found", issuerName) + } + + label := fmt.Sprintf("%s-%s", o.PostageLabel, hex.EncodeToString(randomBytes(4))) + batchID, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, label) + if err != nil { + return nil, nil, "", fmt.Errorf("create batch on %s: %w", issuerName, err) + } + return issuer, signer, batchID, nil +} + +func (c *Check) scenarioDivergentSOC(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + low, high, err := orderedSOCs(signer, id) + if err != nil { + return err + } + stamp, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope: %w", err) + } + stampBytes, err := stamp.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof("soc=%s wrapped_low=%s wrapped_high=%s", addr, mustWrapped(low), mustWrapped(high)) + // Identical stamp: second upload may lose the wrapped-CAC tie-break if the + // winner already synced. Require only the first (winning) upload. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, low, high, stampBytes, stampBytes, false); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, low.Data()) +} + +func (c *Check) scenarioSOCSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-same-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-same-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // CreateEnvelope yields a postage-valid index; reuse it with two timestamps. + env, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + stampOld, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), addr) + if err != nil { + return fmt.Errorf("sign older stamp: %w", err) + } + stampNew, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), addr) + if err != nil { + return fmt.Errorf("sign newer stamp: %w", err) + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s index=%s", addr, hex.EncodeToString(index)) + // Older first, then newer — avoids racing a losing upload after the winner synced. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioSOCDifferentIndexTimestamp(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-diff-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-diff-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + stampOld, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope older: %w", err) + } + stampNew, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer: %w", err) + } + for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { + stampNew, err = issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer retry: %w", err) + } + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s idx_old=%s idx_new=%s", addr, hex.EncodeToString(stampOld.Index()), hex.EncodeToString(stampNew.Index())) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Valid index for this postage bucket (both CACs share the bucket). + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + // higher timestamp on high address chunk so timestamp rule (not address tie-break) decides + stampLow, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s", low.Address(), high.Address(), hex.EncodeToString(index)) + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], low, high); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing higher-timestamp CAC %s", n.Name(), high.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has lower-timestamp CAC %s", n.Name(), low.Address()) + } + } + return nil +} + +func (c *Check) scenarioCACSameIndexEqualTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Same batch, stamp index and timestamp; signatures differ (each stamp is + // bound to its CAC address). At equal timestamp the lower address wins. + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := uint64ToBytes(binary.BigEndian.Uint64(env.Timestamp())) + + stampLow, err := signStamp(batchSigner, batchBytes, index, ts, low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, ts, high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s equal_ts address tie-break", low.Address(), high.Address(), hex.EncodeToString(index)) + // Upload loser (higher address) first, then winner (lower address). + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], high, low); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing lower-address CAC %s", n.Name(), low.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has higher-address CAC %s", n.Name(), high.Address()) + } + } + return nil +} + +// scenarioSOCStaleStampRedeliveryRace races three divergent SOCs at one address: +// +// SOC₁ under stamp S (batch A, lower timestamp) → uploaded to Sam +// SOC₂ under stamp T (batch B, higher timestamp) → uploaded to Sara +// SOC₃ reuses stamp S with payload P₃ whose wrapped CAC beats P₁ and P₂ → +// uploaded to both nodes in parallel with the SOC₁/SOC₂ uploads +// +// After sync, every neighborhood node must hold P₃ (wrapped-CAC winner under +// the reused stamp S / resolveDivergence). +func (c *Check) scenarioSOCStaleStampRedeliveryRace(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { + sam, sara := neighborhood[0], neighborhood[1] + + batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-stale-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch B: %w", err) + } + + signer, owner, id, addr, err := mineSOC(ctx, sam, sara) + if err != nil { + return err + } + soc1, soc2, soc3, err := orderedStaleRaceSOCs(signer, id) + if err != nil { + return err + } + + batchABytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + batchBBytes, err := hex.DecodeString(batchB) + if err != nil { + return err + } + + envS, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope stamp S: %w", err) + } + envT, err := issuer.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope stamp T: %w", err) + } + tsS := binary.BigEndian.Uint64(envS.Timestamp()) + tsT := binary.BigEndian.Uint64(envT.Timestamp()) + if tsS <= tsT { + tsS = tsT + 1 + } + stampS, err := signStamp(batchSigner, batchABytes, append([]byte{}, envS.Index()...), uint64ToBytes(tsS), addr) + if err != nil { + return fmt.Errorf("sign stamp S: %w", err) + } + stampT, err := signStamp(batchSigner, batchBBytes, append([]byte{}, envT.Index()...), uint64ToBytes(tsT), addr) + if err != nil { + return fmt.Errorf("sign stamp T: %w", err) + } + stampSBytes, err := stampS.MarshalBinary() + if err != nil { + return err + } + stampTBytes, err := stampT.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof( + "soc=%s stale-stamp race sam=%s sara=%s wrapped_p1=%s wrapped_p2=%s wrapped_p3=%s batchA=%s… batchB=%s… tsS=%d tsT=%d", + addr, sam.Name(), sara.Name(), mustWrapped(soc1), mustWrapped(soc2), mustWrapped(soc3), + batchID[:8], batchB[:8], tsS, tsT, + ) + + type uploadResult struct { + label string + err error + } + results := make(chan uploadResult, 4) + + go func() { + results <- uploadResult{"SOC1→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc1, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC2→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc2, stampTBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc3, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc3, stampSBytes)} + }() + + for range 4 { + r := <-results + if r.err != nil { + c.logger.Infof("upload %s returned: %v", r.label, r.err) + } else { + c.logger.Infof("upload %s ok", r.label) + } + } + c.logger.Infof("race uploads finished; waiting %s for neighborhood to settle on P3", syncWait) + + time.Sleep(syncWait) + + for _, n := range neighborhood { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + c.logger.Errorf("%s download after race: %v", n.Name(), err) + continue + } + switch { + case bytes.Equal(got, soc3.Data()): + c.logger.Infof("%s holds P3 — expected", n.Name()) + case bytes.Equal(got, soc2.Data()): + c.logger.Errorf("%s holds P2 (stamp T) — want P3", n.Name()) + case bytes.Equal(got, soc1.Data()): + c.logger.Errorf("%s holds P1 — want P3", n.Name()) + default: + c.logger.Errorf("%s holds unexpected payload len=%d", n.Name(), len(got)) + } + } + + if err := assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, soc3.Data()); err != nil { + return fmt.Errorf("after stale-stamp race (want P3 on both): %w", err) + } + return nil +} + +// orderedStaleRaceSOCs builds three SOCs at the same address where P₃'s wrapped +// CAC is lexicographically lower than both P₁ and P₂, so resolveDivergence would +// accept P₃ over either stored payload when the stale-stamp guard is absent. +func orderedStaleRaceSOCs(signer crypto.Signer, id []byte) (p1, p2, p3 swarm.Chunk, err error) { + for range 10_000 { + p1, err = buildSOC(signer, id, append([]byte("stale-p1-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p2, err = buildSOC(signer, id, append([]byte("stale-p2-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p3, err = buildSOC(signer, id, append([]byte("stale-p3-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + w1, w2, w3 := mustWrapped(p1).Bytes(), mustWrapped(p2).Bytes(), mustWrapped(p3).Bytes() + if bytes.Compare(w3, w1) < 0 && bytes.Compare(w3, w2) < 0 && + !bytes.Equal(w1, w2) && !bytes.Equal(w1, w3) && !bytes.Equal(w2, w3) { + return p1, p2, p3, nil + } + } + return nil, nil, nil, fmt.Errorf("could not mine P1/P2/P3 with wrapped(P3) lower than both") +} + +func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch swarm.Chunk, stamp []byte) error { + sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { + return fmt.Errorf("upload to %s: %w", n.Name(), err) + } + return nil +} + +// uploadSOCPair stores ch1 on n1 then ch2 on n2. Callers should pass the +// expected loser/older chunk first so the winner can overwrite during sync. +// If requireSecond is false, a failed second upload is ignored (tie-break loser). +func uploadSOCPair(ctx context.Context, n1, n2 *bee.Client, owner string, id []byte, ch1, ch2 swarm.Chunk, stamp1, stamp2 []byte, requireSecond bool) error { + sig1 := hex.EncodeToString(ch1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2 := hex.EncodeToString(ch2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped1 := ch1.Data()[swarm.HashSize+swarm.SocSignatureSize:] + wrapped2 := ch2.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + + if _, err := n1.UploadSOCWithStamp(ctx, owner, idHex, sig1, wrapped1, stamp1); err != nil { + return fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + if _, err := n2.UploadSOCWithStamp(ctx, owner, idHex, sig2, wrapped2, stamp2); err != nil { + if requireSecond { + return fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + } + return nil +} + +func uploadChunkPair(ctx context.Context, n1, n2 *bee.Client, ch1, ch2 swarm.Chunk) error { + st1, err := ch1.Stamp().MarshalBinary() + if err != nil { + return err + } + st2, err := ch2.Stamp().MarshalBinary() + if err != nil { + return err + } + if _, err := n1.UploadChunk(ctx, ch1.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st1), Direct: true}); err != nil { + return fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + if _, err := n2.UploadChunk(ctx, ch2.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st2), Direct: true}); err != nil { + return fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + return nil +} + +func assertNeighborhoodPayload(ctx context.Context, logger logging.Logger, nodes []*bee.Client, addr swarm.Address, want []byte) error { + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + if !bytes.Equal(got, want) { + logger.Errorf("node %s payload mismatch want_len=%d got_len=%d", n.Name(), len(want), len(got)) + return fmt.Errorf("node %s: payload mismatch", n.Name()) + } + } + return nil +} + +func pickClosestPair(ctx context.Context, nodes orchestration.ClientList) (*bee.Client, *bee.Client, error) { + overlays := make([]swarm.Address, len(nodes)) + for i, n := range nodes { + a, err := n.Addresses(ctx) + if err != nil { + return nil, nil, fmt.Errorf("addresses for %s: %w", n.Name(), err) + } + overlays[i] = a.Overlay + } + bestI, bestJ, bestPO := 0, 1, -1 + for i := 0; i < len(nodes); i++ { + for j := i + 1; j < len(nodes); j++ { + p := int(swarm.Proximity(overlays[i].Bytes(), overlays[j].Bytes())) + if p > bestPO { + bestPO, bestI, bestJ = p, i, j + } + } + } + if bestPO < 0 { + return nil, nil, fmt.Errorf("could not find a node pair") + } + return nodes[bestI], nodes[bestJ], nil +} + +func mineSOC(ctx context.Context, n1, n2 *bee.Client) (crypto.Signer, string, []byte, swarm.Address, error) { + a1, err := n1.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + a2, err := n2.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + t1, t2 := a1.Overlay, a2.Overlay + minPO := swarm.Proximity(t1.Bytes(), t2.Bytes()) + if minPO > 8 { + minPO = 8 + } + + for { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + signer := crypto.NewDefaultSigner(privKey) + ownerBytes, err := signer.EthereumAddress() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + owner := hex.EncodeToString(ownerBytes.Bytes()) + for range 8192 { + id := make([]byte, swarm.HashSize) + if _, err := crand.Read(id); err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + tmp, err := cac.New([]byte("x")) + if err != nil { + continue + } + ch, err := soc.New(id, tmp).Sign(signer) + if err != nil { + continue + } + a := ch.Address() + if swarm.Proximity(a.Bytes(), t1.Bytes()) >= minPO && + swarm.Proximity(a.Bytes(), t2.Bytes()) >= minPO { + return signer, owner, id, a, nil + } + } + } +} + +func orderedSOCs(signer crypto.Signer, id []byte) (low, high swarm.Chunk, err error) { + a, err := buildSOC(signer, id, []byte("soc-conv-payload-aaa")) + if err != nil { + return nil, nil, err + } + b, err := buildSOC(signer, id, []byte("soc-conv-payload-bbb")) + if err != nil { + return nil, nil, err + } + wa, wb := mustWrapped(a), mustWrapped(b) + if bytes.Compare(wa.Bytes(), wb.Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func buildSOC(signer crypto.Signer, id, payload []byte) (swarm.Chunk, error) { + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + return soc.New(id, ch).Sign(signer) +} + +func orderedCACsInBucket(target swarm.Address) (low, high swarm.Chunk, err error) { + bucket := addressBucket(bucketDepth, target) + a, err := cacInBucket(bucket, append([]byte("cac-conv-aaa-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + b, err := cacInBucket(bucket, append([]byte("cac-conv-bbb-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + for a.Address().Equal(b.Address()) { + b, err = cacInBucket(bucket, randomBytes(32)) + if err != nil { + return nil, nil, err + } + } + if bytes.Compare(a.Address().Bytes(), b.Address().Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func cacInBucket(bucket uint32, seed []byte) (swarm.Chunk, error) { + for range 5_000_000 { + payload := append(append([]byte{}, seed...), randomBytes(16)...) + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + if addressBucket(bucketDepth, ch.Address()) == bucket { + return ch, nil + } + seed = randomBytes(16) + } + return nil, fmt.Errorf("could not mine CAC into bucket %d", bucket) +} + +func addressBucket(depth uint8, addr swarm.Address) uint32 { + return binary.BigEndian.Uint32(addr.Bytes()[:4]) >> (32 - depth) +} + +func signStamp(signer crypto.Signer, batchID, index, ts []byte, addr swarm.Address) (*postage.Stamp, error) { + digest, err := postage.ToSignDigest(addr.Bytes(), batchID, index, ts) + if err != nil { + return nil, err + } + sig, err := signer.Sign(digest) + if err != nil { + return nil, err + } + return postage.NewStamp(batchID, index, ts, sig), nil +} + +func mustWrapped(ch swarm.Chunk) swarm.Address { + s, err := soc.FromChunk(ch) + if err != nil { + return swarm.ZeroAddress + } + return s.WrappedChunk().Address() +} + +func uint64ToBytes(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + _, _ = crand.Read(b) + return b +} diff --git a/pkg/check/socconvergence/socconvergence.go b/pkg/check/socconvergence/socconvergence.go new file mode 100644 index 000000000..562eb67ef --- /dev/null +++ b/pkg/check/socconvergence/socconvergence.go @@ -0,0 +1,477 @@ +// 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 socconvergence + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +// Options represents check options +type Options struct { + GasPrice string + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + RequestTimeout time.Duration + SyncRetryInterval time.Duration +} + +// NewDefaultOptions returns new default options +func NewDefaultOptions() Options { + return Options{ + GasPrice: "", + PostageTTL: 24 * time.Hour, + PostageDepth: 16, + PostageLabel: "soc-convergence-label", + RequestTimeout: 5 * time.Minute, + SyncRetryInterval: 2 * time.Second, + } +} + +// compile check whether Check implements interface +var _ beekeeper.Action = (*Check)(nil) + +// Check instance. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + logger: logger, + } +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + rnd := random.PseudoGenerator(time.Now().UnixNano()) + fullNodeClients, err := cluster.ShuffledFullNodeClients(ctx, rnd) + if err != nil { + return fmt.Errorf("shuffled full node clients: %w", err) + } + + if len(fullNodeClients) < 2 { + return fmt.Errorf("socconvergence test requires at least 2 full nodes") + } + + node0 := fullNodeClients[0] + node1 := fullNodeClients[1] + + c.logger.Infof("socconvergence: using node0 '%s' and node1 '%s' (total full nodes: %d)", + node0.Name(), node1.Name(), len(fullNodeClients)) + + // Shared signer for same owner address + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return fmt.Errorf("generate secp256k1 key: %w", err) + } + signer := crypto.NewDefaultSigner(privKey) + pubKey, err := signer.PublicKey() + if err != nil { + return fmt.Errorf("signer public key: %w", err) + } + ownerBytes, err := crypto.NewEthereumAddress(*pubKey) + if err != nil { + return fmt.Errorf("ethereum address: %w", err) + } + ownerHex := hex.EncodeToString(ownerBytes) + + // Create postage batch on node0 + batchID0, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + c.logger.Infof("socconvergence: created batchID %s on node %s", batchID0, node0.Name()) + + // ------------------------------------------------------------------------- + // Scenario 1: Divergent SOC Tie-Break Convergence (SWIP-101) + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 1: Divergent SOC Tie-Break Convergence ---") + if err := c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { + return fmt.Errorf("scenario 1 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 2: Multi-Batch Divergent SOC Convergence + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 2: Multi-Batch Divergent SOC Convergence ---") + batchID1, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + if err := c.testMultiBatchDivergence(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 2 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 3: Identical Payload Checksum Matching + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 3: Identical Payload Checksum Matching ---") + if err := c.testIdenticalSOCChecksum(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { + return fmt.Errorf("scenario 3 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 4: Multi-Stamp Re-offer Precedence + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 4: Multi-Stamp Re-offer Precedence ---") + if err := c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 4 failed: %w", err) + } + + c.logger.Infof("socconvergence: all integration test scenarios passed successfully!") + return nil +} + +func (c *Check) testDivergentSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + // Create 2 divergent payloads wrapping different CACs + payload1 := []byte("soc-convergence-payload-alpha-101") + payload2 := []byte("soc-convergence-payload-beta-101") + + ch1, err := cac.New(payload1) + if err != nil { + return fmt.Errorf("cac 1: %w", err) + } + ch2, err := cac.New(payload2) + if err != nil { + return fmt.Errorf("cac 2: %w", err) + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return fmt.Errorf("sign soc 1: %w", err) + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return fmt.Errorf("sign soc 2: %w", err) + } + + // Extract signatures + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + socAddress := soc1.Address() + c.logger.Infof("divergent soc address: %s", socAddress.String()) + + // Determine theoretical tie-break winner (lexicographically lower wrapped CAC) + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return fmt.Errorf("divergent chunk wins comparison: %w", err) + } + + var expectedWinnerPayload []byte + var winnerName string + if wins { + expectedWinnerPayload = ch2.Data() + winnerName = "soc2 (payload2)" + } else { + expectedWinnerPayload = ch1.Data() + winnerName = "soc1 (payload1)" + } + c.logger.Infof("theoretical tie-break winner: %s", winnerName) + + // Upload soc1 to node0 + ref1, err := node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc1 to node %s: %w", node0.Name(), err) + } + c.logger.Infof("uploaded soc1 to node %s: ref %s", node0.Name(), ref1.String()) + + // Upload soc2 to node1 + ref2, err := node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc2 to node %s: %w", node1.Name(), err) + } + c.logger.Infof("uploaded soc2 to node %s: ref %s", node1.Name(), ref2.String()) + + // Wait for cluster pullsync convergence across all full nodes + c.logger.Infof("waiting for pullsync cluster convergence on address %s...", socAddress.String()) + return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) +} + +func (c *Check) testMultiBatchDivergence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + payload1 := []byte("multi-batch-payload-gamma-201") + payload2 := []byte("multi-batch-payload-delta-202") + + ch1, err := cac.New(payload1) + if err != nil { + return err + } + ch2, err := cac.New(payload2) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + socAddress := soc1.Address() + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedWinnerPayload []byte + if wins { + expectedWinnerPayload = ch2.Data() + } else { + expectedWinnerPayload = ch1.Data() + } + + // Upload to node0 under batchID0 + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return fmt.Errorf("upload soc1 multi-batch: %w", err) + } + + // Upload to node1 under batchID1 + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return fmt.Errorf("upload soc2 multi-batch: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) +} + +func (c *Check) testIdenticalSOCChecksum( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + payload := []byte("identical-soc-checksum-payload-301") + ch, err := cac.New(payload) + if err != nil { + return err + } + + sch, err := soc.New(idBytes, ch).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(sch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + socAddress := sch.Address() + + // Upload exact same SOC to node0 and node1 + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return fmt.Errorf("upload identical to node0: %w", err) + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return fmt.Errorf("upload identical to node1: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, socAddress, ch.Data(), o) +} + +func (c *Check) testMultiStampReofferPrecedence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + // Create 2 divergent payloads wrapping different CACs + payload1 := []byte("reoffer-precedence-payload-omega-401") + payload2 := []byte("reoffer-precedence-payload-psi-402") + + ch1, err := cac.New(payload1) + if err != nil { + return err + } + ch2, err := cac.New(payload2) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + socAddress := soc1.Address() + + // 1. Upload soc1 under batchID0 to node0 + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return fmt.Errorf("upload soc1 to node0: %w", err) + } + + // 2. Upload soc2 under batchID1 to node1 + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return fmt.Errorf("upload soc2 to node1: %w", err) + } + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedWinnerPayload []byte + if wins { + expectedWinnerPayload = ch2.Data() + } else { + expectedWinnerPayload = ch1.Data() + } + + // Wait for cluster convergence on initial tie-break winner + c.logger.Infof("waiting for initial convergence on address %s...", socAddress.String()) + if err := c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o); err != nil { + return fmt.Errorf("initial convergence failed: %w", err) + } + + // 3. Re-offer soc1 under batchID0 to node0 (re-offering weaker/superseded variant) + c.logger.Infof("re-offering soc1 under batchID0 to node0...") + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + + // Assert cluster STILL converges on expectedWinnerPayload and does NOT revert + c.logger.Infof("asserting cluster stability after re-offer on address %s...", socAddress.String()) + return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) +} + +func (c *Check) assertClusterConvergence( + ctx context.Context, + nodes []*bee.Client, + address swarm.Address, + expectedPayload []byte, + o Options, +) error { + ticker := time.NewTicker(o.SyncRetryInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) + case <-ticker.C: + allConverged := true + for _, node := range nodes { + retrieved, err := node.DownloadChunk(ctx, address, "", nil) + if err != nil { + allConverged = false + c.logger.Debugf("node %s: download %s error: %v", node.Name(), address.String(), err) + break + } + if !bytes.Contains(retrieved, expectedPayload) && !bytes.Equal(retrieved, expectedPayload) { + allConverged = false + c.logger.Debugf("node %s: data mismatch on %s", node.Name(), address.String()) + break + } + } + + if allConverged { + c.logger.Infof("all %d full nodes converged on address %s with expected payload!", len(nodes), address.String()) + return nil + } + } + } +} + +func randomID() ([]byte, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +// divergentChunkWins reports whether ch2 wins over ch1 by having a +// lexicographically lower wrapped CAC address. +func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { + s1, err := soc.FromChunk(ch1) + if err != nil { + return false, fmt.Errorf("soc 1 from chunk: %w", err) + } + s2, err := soc.FromChunk(ch2) + if err != nil { + return false, fmt.Errorf("soc 2 from chunk: %w", err) + } + return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil +} diff --git a/pkg/check/socconvergence/socconvergence_test.go b/pkg/check/socconvergence/socconvergence_test.go new file mode 100644 index 000000000..2f8bbb2e5 --- /dev/null +++ b/pkg/check/socconvergence/socconvergence_test.go @@ -0,0 +1,34 @@ +// 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 socconvergence_test + +import ( + "io" + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/check/socconvergence" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestNewDefaultOptions(t *testing.T) { + opts := socconvergence.NewDefaultOptions() + if opts.PostageTTL != 24*time.Hour { + t.Fatalf("expected PostageTTL 24h, got %v", opts.PostageTTL) + } + if opts.PostageDepth != 16 { + t.Fatalf("expected PostageDepth 16, got %d", opts.PostageDepth) + } + if opts.RequestTimeout != 5*time.Minute { + t.Fatalf("expected RequestTimeout 5m, got %v", opts.RequestTimeout) + } +} + +func TestNewCheck(t *testing.T) { + chk := socconvergence.NewCheck(logging.New(io.Discard, 0)) + if chk == nil { + t.Fatal("expected non-nil check") + } +} diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go new file mode 100644 index 000000000..61f12326c --- /dev/null +++ b/pkg/check/socmatrix/socmatrix.go @@ -0,0 +1,769 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socmatrix + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "time" + + "github.com/ethersphere/bee/v2/pkg/cac" + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/bee/api" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +// Options holds options for the socmatrix check. +type Options struct { + GasPrice string + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + RequestTimeout time.Duration + SyncRetryInterval time.Duration +} + +// NewDefaultOptions returns default options for the check. +func NewDefaultOptions() Options { + return Options{ + GasPrice: "", + PostageTTL: 24 * time.Hour, + PostageDepth: 16, + PostageLabel: "socmatrix-label", + RequestTimeout: 10 * time.Minute, + SyncRetryInterval: 2 * time.Second, + } +} + +// Check instance. +type Check struct { + logger logging.Logger +} + +// NewCheck returns a new check instance. +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + logger: logger, + } +} + +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + + ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) + defer cancel() + + rnd := random.PseudoGenerator(time.Now().UnixNano()) + fullNodeClients, err := cluster.ShuffledFullNodeClients(ctx, rnd) + if err != nil { + return fmt.Errorf("shuffled full node clients: %w", err) + } + + if len(fullNodeClients) < 2 { + return fmt.Errorf("socmatrix test requires at least 2 full nodes") + } + + node0 := fullNodeClients[0] + node1 := fullNodeClients[1] + + c.logger.Infof("socmatrix: running 10-scenario convergence matrix against %d full nodes (node0: '%s', node1: '%s')", + len(fullNodeClients), node0.Name(), node1.Name()) + + // Shared signer for owner 1 + signer1, ownerHex1, err := newTestSigner() + if err != nil { + return err + } + // Signer for owner 2 (multi-owner isolation scenario) + signer2, ownerHex2, err := newTestSigner() + if err != nil { + return err + } + + // Create postage batches on node0 and node1 + batchID0, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + batchID1, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + + // ------------------------------------------------------------------------- + // Scenario 1: Divergent SOC Tie-Break (Equal Timestamp, Same Stamp) + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 1: Divergent SOC Tie-Break (Same Stamp) ---") + if err := c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { + return fmt.Errorf("scenario 1 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 2: Divergent SOC Timestamp Progression (Increasing Timestamp) + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 2: Timestamp Progression (Increasing Timestamp) ---") + if err := c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { + return fmt.Errorf("scenario 2 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 3: Cross-Batch Equal Timestamp Stamp-Hash Precedence + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence ---") + if err := c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 3 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 4: Multi-Stamp Re-Offer Precedence + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 4: Multi-Stamp Re-Offer Precedence ---") + if err := c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 4 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 5: Multi-Batch Sibling Sum Refresh + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 5: Multi-Batch Sibling Sum Refresh ---") + if err := c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { + return fmt.Errorf("scenario 5 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 6: Identical Payload Checksum Matching (Duplicate De-duplication) + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 6: Identical Payload Checksum Matching ---") + if err := c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { + return fmt.Errorf("scenario 6 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 7: CAC vs CAC Stamp Index Collision + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 7: CAC vs CAC Stamp Index Collision ---") + if err := c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, batchID0, o); err != nil { + return fmt.Errorf("scenario 7 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 8: CAC vs SOC Stamp Index Tie-Break + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 8: CAC vs SOC Stamp Index Tie-Break ---") + if err := c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { + return fmt.Errorf("scenario 8 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 9: 3-Payload Sequenced Divergent SOC Chain + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 9: 3-Payload Sequenced Divergent SOC Chain ---") + if err := c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { + return fmt.Errorf("scenario 9 failed: %w", err) + } + + // ------------------------------------------------------------------------- + // Scenario 10: Multi-Owner Independent SOC Isolation + // ------------------------------------------------------------------------- + c.logger.Infof("--- Scenario 10: Multi-Owner Independent SOC Isolation ---") + if err := c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, batchID0, o); err != nil { + return fmt.Errorf("scenario 10 failed: %w", err) + } + + c.logger.Infof("socmatrix: all 10 matrix scenarios completed successfully across %d full nodes!", len(fullNodeClients)) + return nil +} + +func (c *Check) testDivergentSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s1-payload-alpha")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s1-payload-beta")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc1: %w", err) + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload soc2: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testTimestampProgression( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s2-initial-payload")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s2-updated-payload")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return fmt.Errorf("upload initial soc1: %w", err) + } + + time.Sleep(1 * time.Second) + + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return fmt.Errorf("upload updated soc2: %w", err) + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch2.Data(), o) +} + +func (c *Check) testCrossBatchStampHashPrecedence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s3-payload-one")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s3-payload-two")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiStampReofferPrecedence( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s4-payload-alpha")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s4-payload-beta")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + wins, err := divergentChunkWins(soc1, soc2) + if err != nil { + return err + } + var expectedPayload []byte + if wins { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o); err != nil { + return err + } + + // Re-offer soc1 under batchID0 + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiBatchSiblingSumRefresh( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID0, batchID1 string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, err := cac.New([]byte("matrix-s5-initial-multibatch")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s5-updated-multibatch")) + if err != nil { + return err + } + + soc1, err := soc.New(idBytes, ch1).Sign(signer) + if err != nil { + return err + } + soc2, err := soc.New(idBytes, ch2).Sign(signer) + if err != nil { + return err + } + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID1) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch1.Data(), o); err != nil { + return err + } + + time.Sleep(1 * time.Second) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID0) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch2.Data(), o) +} + +func (c *Check) testIdenticalChecksumMatching( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch, err := cac.New([]byte("matrix-s6-identical-payload")) + if err != nil { + return err + } + sch, err := soc.New(idBytes, ch).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(sch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + if err != nil { + return err + } + + return c.assertClusterConvergence(ctx, fullNodes, sch.Address(), ch.Data(), o) +} + +func (c *Check) testCACIndexCollision( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + batchID string, + o Options, +) error { + ch1, err := cac.New([]byte("matrix-s7-cac-payload-one")) + if err != nil { + return err + } + ch2, err := cac.New([]byte("matrix-s7-cac-payload-two")) + if err != nil { + return err + } + + _, err = node0.UploadBytes(ctx, ch1.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return fmt.Errorf("upload cac1: %w", err) + } + _, err = node1.UploadBytes(ctx, ch2.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return fmt.Errorf("upload cac2: %w", err) + } + + if err := c.assertClusterConvergence(ctx, fullNodes, ch1.Address(), ch1.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, ch2.Address(), ch2.Data(), o) +} + +func (c *Check) testCACvsSOCTieBreak( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + chCAC, err := cac.New([]byte("matrix-s8-cac-content")) + if err != nil { + return err + } + socChunk, err := soc.New(idBytes, chCAC).Sign(signer) + if err != nil { + return err + } + + sigHex := hex.EncodeToString(socChunk.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadBytes(ctx, chCAC.Data(), api.UploadOptions{BatchID: batchID}) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, chCAC.Data(), batchID) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, chCAC.Address(), chCAC.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, socChunk.Address(), chCAC.Data(), o) +} + +func (c *Check) testSequencedDivergentChain( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex string, + signer crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + p1 := []byte("matrix-s9-chain-payload-one") + p2 := []byte("matrix-s9-chain-payload-two") + p3 := []byte("matrix-s9-chain-payload-three") + + ch1, _ := cac.New(p1) + ch2, _ := cac.New(p2) + ch3, _ := cac.New(p3) + + soc1, _ := soc.New(idBytes, ch1).Sign(signer) + soc2, _ := soc.New(idBytes, ch2).Sign(signer) + soc3, _ := soc.New(idBytes, ch3).Sign(signer) + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig3Hex := hex.EncodeToString(soc3.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + _, _ = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), batchID) + + // Determine winner among all three + win12, _ := divergentChunkWins(soc1, soc2) + var winner12 swarm.Chunk + if win12 { + winner12 = soc2 + } else { + winner12 = soc1 + } + winFinal, _ := divergentChunkWins(winner12, soc3) + var winningSOC swarm.Chunk + if winFinal { + winningSOC = soc3 + } else { + winningSOC = winner12 + } + + var expectedPayload []byte + if winningSOC.Address().Equal(soc1.Address()) && bytes.Equal(winningSOC.Data(), soc1.Data()) { + expectedPayload = ch1.Data() + } else if winningSOC.Address().Equal(soc2.Address()) && bytes.Equal(winningSOC.Data(), soc2.Data()) { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch3.Data() + } + + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) +} + +func (c *Check) testMultiOwnerSOCIsolation( + ctx context.Context, + fullNodes []*bee.Client, + node0, node1 *bee.Client, + ownerHex1, ownerHex2 string, + signer1, signer2 crypto.Signer, + batchID string, + o Options, +) error { + idBytes, err := randomID() + if err != nil { + return err + } + idHex := hex.EncodeToString(idBytes) + + ch1, _ := cac.New([]byte("matrix-s10-owner1-payload")) + ch2, _ := cac.New([]byte("matrix-s10-owner2-payload")) + + soc1, _ := soc.New(idBytes, ch1).Sign(signer1) + soc2, _ := soc.New(idBytes, ch2).Sign(signer2) + + sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + + _, err = node0.UploadSOC(ctx, ownerHex1, idHex, sig1Hex, ch1.Data(), batchID) + if err != nil { + return err + } + _, err = node1.UploadSOC(ctx, ownerHex2, idHex, sig2Hex, ch2.Data(), batchID) + if err != nil { + return err + } + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), ch1.Data(), o); err != nil { + return err + } + return c.assertClusterConvergence(ctx, fullNodes, soc2.Address(), ch2.Data(), o) +} + +func (c *Check) assertClusterConvergence( + ctx context.Context, + nodes []*bee.Client, + address swarm.Address, + expectedPayload []byte, + o Options, +) error { + ticker := time.NewTicker(o.SyncRetryInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) + case <-ticker.C: + allConverged := true + for _, node := range nodes { + retrieved, err := node.DownloadChunk(ctx, address, "", nil) + if err != nil { + allConverged = false + c.logger.Debugf("node %s: download %s error: %v", node.Name(), address.String(), err) + break + } + if !bytes.Contains(retrieved, expectedPayload) && !bytes.Equal(retrieved, expectedPayload) { + allConverged = false + c.logger.Debugf("node %s: data mismatch on %s", node.Name(), address.String()) + break + } + } + + if allConverged { + c.logger.Infof("all %d full nodes converged on address %s with expected payload!", len(nodes), address.String()) + return nil + } + } + } +} + +func newTestSigner() (crypto.Signer, string, error) { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", err + } + signer := crypto.NewDefaultSigner(privKey) + pubKey, err := signer.PublicKey() + if err != nil { + return nil, "", err + } + ownerBytes, err := crypto.NewEthereumAddress(*pubKey) + if err != nil { + return nil, "", err + } + return signer, hex.EncodeToString(ownerBytes), nil +} + +func randomID() ([]byte, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { + s1, err := soc.FromChunk(ch1) + if err != nil { + return false, fmt.Errorf("soc 1 from chunk: %w", err) + } + s2, err := soc.FromChunk(ch2) + if err != nil { + return false, fmt.Errorf("soc 2 from chunk: %w", err) + } + return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil +} diff --git a/pkg/check/socmatrix/socmatrix_test.go b/pkg/check/socmatrix/socmatrix_test.go new file mode 100644 index 000000000..e1d444086 --- /dev/null +++ b/pkg/check/socmatrix/socmatrix_test.go @@ -0,0 +1,34 @@ +// Copyright 2026 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socmatrix_test + +import ( + "io" + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/check/socmatrix" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestNewDefaultOptions(t *testing.T) { + opts := socmatrix.NewDefaultOptions() + if opts.PostageTTL != 24*time.Hour { + t.Fatalf("expected PostageTTL 24h, got %v", opts.PostageTTL) + } + if opts.PostageDepth != 16 { + t.Fatalf("expected PostageDepth 16, got %d", opts.PostageDepth) + } + if opts.RequestTimeout != 10*time.Minute { + t.Fatalf("expected RequestTimeout 10m, got %v", opts.RequestTimeout) + } +} + +func TestNewCheck(t *testing.T) { + chk := socmatrix.NewCheck(logging.New(io.Discard, 0)) + if chk == nil { + t.Fatal("expected non-nil check") + } +} diff --git a/pkg/config/check.go b/pkg/config/check.go index 541afb7e5..a622754ff 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -12,6 +12,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/autotls" "github.com/ethersphere/beekeeper/pkg/check/balances" "github.com/ethersphere/beekeeper/pkg/check/cashout" + "github.com/ethersphere/beekeeper/pkg/check/chunkconvergence" "github.com/ethersphere/beekeeper/pkg/check/datadurability" "github.com/ethersphere/beekeeper/pkg/check/feed" "github.com/ethersphere/beekeeper/pkg/check/fileretrieval" @@ -34,6 +35,8 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/settlements" "github.com/ethersphere/beekeeper/pkg/check/smoke" "github.com/ethersphere/beekeeper/pkg/check/soc" + "github.com/ethersphere/beekeeper/pkg/check/socconvergence" + "github.com/ethersphere/beekeeper/pkg/check/socmatrix" "github.com/ethersphere/beekeeper/pkg/check/stake" "github.com/ethersphere/beekeeper/pkg/check/withdraw" "github.com/ethersphere/beekeeper/pkg/logging" @@ -528,6 +531,52 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "socconvergence": { + NewAction: socconvergence.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + GasPrice *string `yaml:"gas-price"` + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := socconvergence.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, + "socmatrix": { + NewAction: socmatrix.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + GasPrice *string `yaml:"gas-price"` + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := socmatrix.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "postage": { NewAction: postage.NewCheck, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { @@ -693,6 +742,29 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "chunk-convergence": { + NewAction: chunkconvergence.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + SyncWait *time.Duration `yaml:"sync-wait"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + Password *string `yaml:"password"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := chunkconvergence.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "feed-v1": { NewAction: feed.NewCheckV1, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { diff --git a/pkg/config/soc_partition_test.go b/pkg/config/soc_partition_test.go new file mode 100644 index 000000000..d7472f3f8 --- /dev/null +++ b/pkg/config/soc_partition_test.go @@ -0,0 +1,76 @@ +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/ethersphere/beekeeper/pkg/config" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +func TestSOCPartitionClusterConfig(t *testing.T) { + t.Parallel() + + dir := filepath.Join("..", "..", "config") + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + + var files []config.YamlFile + for _, e := range entries { + if e.IsDir() { + continue + } + ext := filepath.Ext(e.Name()) + if ext != ".yaml" && ext != ".yml" { + continue + } + content, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + t.Fatal(err) + } + files = append(files, config.YamlFile{Name: e.Name(), Content: content}) + } + + cfg, err := config.Read(logging.New(os.Stderr, 0), files) + if err != nil { + t.Fatalf("read config: %v", err) + } + + cluster, ok := cfg.Clusters["local-soc-partition"] + if !ok { + t.Fatal("local-soc-partition cluster missing") + } + groups := cluster.GetNodeGroups() + if _, ok := groups["bootnode"]; !ok { + t.Fatal("bootnode group missing") + } + if g, ok := groups["group-a"]; !ok || g.Count != 2 { + t.Fatalf("group-a: %+v", g) + } + if g, ok := groups["group-b"]; !ok || len(g.Nodes) != 2 { + t.Fatalf("group-b: %+v", g) + } + + a, ok := cfg.BeeConfigs["bee-soc-partition-group-a"] + if !ok || a.TargetNeighborhood == nil || *a.TargetNeighborhood != "11111100" { + t.Fatalf("group-a bee config: %+v", a) + } + b, ok := cfg.BeeConfigs["bee-soc-partition-group-b"] + if !ok || b.Bootnodes == nil || len(*b.Bootnodes) == 0 { + t.Fatalf("group-b must have non-empty bootnodes: %+v", b) + } + if b.TargetNeighborhood == nil || *b.TargetNeighborhood != "11111100" { + t.Fatalf("group-b target neighborhood: %+v", b) + } + + exported := b.Export() + if exported.TargetNeighborhood == nil || *exported.TargetNeighborhood != "11111100" { + t.Fatalf("export target neighborhood: %v", exported.TargetNeighborhood) + } + if exported.Bootnodes == nil || len(*exported.Bootnodes) == 0 { + t.Fatal("export bootnodes empty") + } +} diff --git a/pkg/orchestration/key.go b/pkg/orchestration/key.go index 22c12d5af..056ab7681 100644 --- a/pkg/orchestration/key.go +++ b/pkg/orchestration/key.go @@ -1,6 +1,7 @@ package orchestration import ( + "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdsa" @@ -13,6 +14,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/google/uuid" "golang.org/x/crypto/scrypt" + "golang.org/x/crypto/sha3" "gopkg.in/yaml.v3" ) @@ -69,6 +71,50 @@ func (k *EncryptedKey) StringJSON() (string, error) { return string(data), nil } +// Decrypt decrypts the encrypted Swarm key with the given password and returns +// the secp256k1 private key. The key file format matches Ethereum JSON v3. +func (k *EncryptedKey) Decrypt(password string) (*ecdsa.PrivateKey, error) { + if k == nil { + return nil, fmt.Errorf("nil encrypted key") + } + salt, err := hex.DecodeString(k.Crypto.KDFParams.Salt) + if err != nil { + return nil, fmt.Errorf("decode salt: %w", err) + } + derivedKey, err := scrypt.Key([]byte(password), salt, k.Crypto.KDFParams.N, k.Crypto.KDFParams.R, k.Crypto.KDFParams.P, k.Crypto.KDFParams.DKLen) + if err != nil { + return nil, fmt.Errorf("scrypt: %w", err) + } + cipherText, err := hex.DecodeString(k.Crypto.CipherText) + if err != nil { + return nil, fmt.Errorf("decode ciphertext: %w", err) + } + wantMAC, err := hex.DecodeString(k.Crypto.MAC) + if err != nil { + return nil, fmt.Errorf("decode mac: %w", err) + } + // Bee keystore MAC is SHA3-256; Ethereum V3 keyfiles use Keccak-256. + calculatedMAC := sha3.Sum256(append(derivedKey[16:32], cipherText...)) + if !bytes.Equal(calculatedMAC[:], wantMAC) { + calculatedMACEth, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) + if err != nil { + return nil, fmt.Errorf("mac: %w", err) + } + if !bytes.Equal(calculatedMACEth, wantMAC) { + return nil, fmt.Errorf("invalid password or corrupted key") + } + } + iv, err := hex.DecodeString(k.Crypto.CipherParams.IV) + if err != nil { + return nil, fmt.Errorf("decode iv: %w", err) + } + plain, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, fmt.Errorf("decrypt: %w", err) + } + return crypto.DecodeSecp256k1PrivateKey(plain) +} + type keyCripto struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` diff --git a/scripts/genpartitionkeys/main.go b/scripts/genpartitionkeys/main.go new file mode 100644 index 000000000..ee3d7fc89 --- /dev/null +++ b/scripts/genpartitionkeys/main.go @@ -0,0 +1,143 @@ +// 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. + +// Command genpartitionkeys generates encrypted libp2p keys and peer IDs for the +// local-soc-partition beekeeper cluster (group-b soft bootnode). +// +// go run ./scripts/genpartitionkeys/ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/google/uuid" + libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "golang.org/x/crypto/scrypt" +) + +const ( + password = "beekeeper" + scryptN = 1 << 15 + scryptR = 8 + scryptP = 1 + scryptDKLen = 32 +) + +type encryptedKey struct { + Address string `json:"address"` + Crypto struct { + Cipher string `json:"cipher"` + CipherText string `json:"ciphertext"` + CipherParams struct { + IV string `json:"iv"` + } `json:"cipherparams"` + KDF string `json:"kdf"` + KDFParams struct { + N int `json:"n"` + R int `json:"r"` + P int `json:"p"` + DKLen int `json:"dklen"` + Salt string `json:"salt"` + } `json:"kdfparams"` + MAC string `json:"mac"` + } `json:"crypto"` + Version int `json:"version"` + ID string `json:"id"` +} + +func main() { + for _, name := range []string{"group-b-0", "group-b-1"} { + priv, err := crypto.GenerateSecp256k1Key() + if err != nil { + panic(err) + } + keyJSON, err := encryptKey(priv, password) + if err != nil { + panic(err) + } + pid, err := peerID(priv) + if err != nil { + panic(err) + } + fmt.Printf("NAME=%s\nPEER=%s\nKEY=%s\n\n", name, pid, keyJSON) + } +} + +func peerID(priv *ecdsa.PrivateKey) (string, error) { + raw, err := crypto.EncodeSecp256k1PrivateKey(priv) + if err != nil { + return "", err + } + libKey, err := libp2pcrypto.UnmarshalSecp256k1PrivateKey(raw) + if err != nil { + return "", err + } + id, err := peer.IDFromPrivateKey(libKey) + if err != nil { + return "", err + } + return id.String(), nil +} + +func encryptKey(k *ecdsa.PrivateKey, pass string) (string, error) { + data, err := crypto.EncodeSecp256k1PrivateKey(k) + if err != nil { + return "", err + } + salt := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + return "", err + } + derivedKey, err := scrypt.Key([]byte(pass), salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return "", err + } + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + block, err := aes.NewCipher(derivedKey[:16]) + if err != nil { + return "", err + } + cipherText := make([]byte, len(data)) + cipher.NewCTR(block, iv).XORKeyStream(cipherText, data) + mac, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) + if err != nil { + return "", err + } + addr, err := crypto.NewEthereumAddress(k.PublicKey) + if err != nil { + return "", err + } + ek := encryptedKey{ + Address: hex.EncodeToString(addr), + Version: 3, + ID: uuid.NewString(), + } + ek.Crypto.Cipher = "aes-128-ctr" + ek.Crypto.CipherText = hex.EncodeToString(cipherText) + ek.Crypto.CipherParams.IV = hex.EncodeToString(iv) + ek.Crypto.KDF = "scrypt" + ek.Crypto.KDFParams.N = scryptN + ek.Crypto.KDFParams.R = scryptR + ek.Crypto.KDFParams.P = scryptP + ek.Crypto.KDFParams.DKLen = scryptDKLen + ek.Crypto.KDFParams.Salt = hex.EncodeToString(salt) + ek.Crypto.MAC = hex.EncodeToString(mac) + out, err := json.Marshal(ek) + if err != nil { + return "", err + } + return string(out), nil +} From 74de186558bce686cae0038664d7c604602c20b1 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 4 Aug 2026 13:02:54 +0300 Subject: [PATCH 09/21] test(chunkconvergence): add 503 retry handling in uploadSOC --- pkg/check/chunkconvergence/chunkconvergence.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/check/chunkconvergence/chunkconvergence.go b/pkg/check/chunkconvergence/chunkconvergence.go index c6258db1c..83bf0431b 100644 --- a/pkg/check/chunkconvergence/chunkconvergence.go +++ b/pkg/check/chunkconvergence/chunkconvergence.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "errors" "fmt" + "strings" "time" "github.com/ethersphere/bee/v2/pkg/cac" @@ -554,10 +555,18 @@ func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch s sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] idHex := hex.EncodeToString(id) - if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { - return fmt.Errorf("upload to %s: %w", n.Name(), err) + + for attempt := 0; attempt < 5; attempt++ { + if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { + if strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { + time.Sleep(1 * time.Second) + continue + } + return fmt.Errorf("upload to %s: %w", n.Name(), err) + } + return nil } - return nil + return fmt.Errorf("upload to %s: exceeded 5 retries on 503", n.Name()) } // uploadSOCPair stores ch1 on n1 then ch2 on n2. Callers should pass the From 78843fee148442ddcb1966a59a9ac8d06da31737 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 4 Aug 2026 13:55:55 +0300 Subject: [PATCH 10/21] test(bee): add 503 retry handling in HasChunk and DownloadChunk --- pkg/bee/api/node.go | 22 ++++++++++++++-------- pkg/bee/client.go | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pkg/bee/api/node.go b/pkg/bee/api/node.go index 16bf172ab..ff03b5e9e 100644 --- a/pkg/bee/api/node.go +++ b/pkg/bee/api/node.go @@ -80,15 +80,21 @@ func (n *NodeService) Balances(ctx context.Context) (resp Balances, err error) { // HasChunk returns true/false if node has a chunk func (n *NodeService) HasChunk(ctx context.Context, a swarm.Address) (bool, error) { - // HEAD must not decode a JSON body (response is empty); nil target avoids EOF. - err := n.client.requestJSON(ctx, http.MethodHead, "/chunks/"+a.String(), nil, nil) - if IsHTTPStatusErrorCode(err, http.StatusNotFound) { - return false, nil - } else if err != nil { - return false, err + for attempt := 0; attempt < 5; attempt++ { + err := n.client.requestJSON(ctx, http.MethodHead, "/chunks/"+a.String(), nil, nil) + if IsHTTPStatusErrorCode(err, http.StatusNotFound) { + return false, nil + } + if IsHTTPStatusErrorCode(err, http.StatusServiceUnavailable) { + time.Sleep(1 * time.Second) + continue + } + if err != nil { + return false, err + } + return true, nil } - - return true, nil + return false, fmt.Errorf("has chunk %s: exceeded retries on 503", a.String()) } // Health represents node's health diff --git a/pkg/bee/client.go b/pkg/bee/client.go index cc8b5e57b..2a2a08bd0 100644 --- a/pkg/bee/client.go +++ b/pkg/bee/client.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "slices" + "strings" "sync" "time" @@ -215,13 +216,19 @@ func (c *Client) DownloadBytes(ctx context.Context, a swarm.Address, opts *api.D // DownloadChunk downloads chunk from the node func (c *Client) DownloadChunk(ctx context.Context, a swarm.Address, targets string, opts *api.DownloadOptions) (data []byte, err error) { - r, err := c.api.Chunks.Download(ctx, a, targets, opts) - if err != nil { - return nil, fmt.Errorf("download chunk %s: %w", a, err) + for attempt := 0; attempt < 5; attempt++ { + r, err := c.api.Chunks.Download(ctx, a, targets, opts) + if err != nil { + if api.IsHTTPStatusErrorCode(err, 503) || strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { + time.Sleep(1 * time.Second) + continue + } + return nil, fmt.Errorf("download chunk %s: %w", a, err) + } + defer r.Close() + return io.ReadAll(r) } - defer r.Close() - - return io.ReadAll(r) + return nil, fmt.Errorf("download chunk %s: exceeded retries on 503", a) } // DownloadFileBytes downloads a flie from the node and returns the data. From 9d10bcc854c6641e47867d5c7f5f524d42e4647d Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 7 Aug 2026 12:43:28 +0300 Subject: [PATCH 11/21] fix(config): guard IsNil calls on value structs during config merge --- pkg/config/bee.go | 7 ++----- pkg/config/cluster.go | 9 +++------ pkg/config/config.go | 20 +++++++++++++++----- pkg/config/nodegroup.go | 9 +++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pkg/config/bee.go b/pkg/config/bee.go index ac4c66883..a0dae55b5 100644 --- a/pkg/config/bee.go +++ b/pkg/config/bee.go @@ -17,15 +17,12 @@ type Inheritable interface { // inheritance (_inherit). Export returns just the embedded Config, so neither // _inherit nor any other config-loading detail can leak into the rendered file. type BeeConfig struct { - *Inherit `yaml:",inline"` + Inherit `yaml:",inline"` orchestration.Config `yaml:",inline"` } func (b BeeConfig) GetParentName() string { - if b.Inherit != nil { - return b.ParentName - } - return "" + return b.Inherit.ParentName } // Export returns the Bee flag configuration to be rendered into the node's diff --git a/pkg/config/cluster.go b/pkg/config/cluster.go index 6db5d6992..845414270 100644 --- a/pkg/config/cluster.go +++ b/pkg/config/cluster.go @@ -9,7 +9,7 @@ import ( // Cluster represents cluster configuration type Cluster struct { // parent to inherit settings from - *Inherit `yaml:",inline"` + Inherit `yaml:",inline"` // Cluster configuration Name *string `yaml:"name,omitempty"` Namespace *string `yaml:"namespace"` @@ -24,10 +24,7 @@ type Cluster struct { } func (b Cluster) GetParentName() string { - if b.Inherit != nil { - return b.ParentName - } - return "" + return b.Inherit.ParentName } // ClusterNodeGroup represents node group in the cluster @@ -62,7 +59,7 @@ func (c *Cluster) Export() (o orchestration.ClusterOptions) { for i := 0; i < localVal.NumField(); i++ { localField := localVal.Field(i) - if localField.IsValid() && !localField.IsNil() { + if localField.IsValid() && canIsNil(localField) && !localField.IsNil() { localFieldVal := localVal.Field(i).Elem() localFieldName := localType.Field(i).Name diff --git a/pkg/config/config.go b/pkg/config/config.go index c3089d41c..a6fe3534d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -104,13 +104,14 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { // applied per flag instead of all-or-nothing. if mf.Kind() == reflect.Struct { for j := 0; j < mf.NumField(); j++ { - if mf.Field(j).IsNil() && !pf.Field(j).IsNil() { - mf.Field(j).Set(pf.Field(j)) + fj, pfj := mf.Field(j), pf.Field(j) + if canIsNil(fj) && fj.IsNil() && canIsNil(pfj) && !pfj.IsNil() { + fj.Set(pfj) } } continue } - if mf.IsNil() && !pf.IsNil() { + if canIsNil(mf) && mf.IsNil() && canIsNil(pf) && !pf.IsNil() { mf.Set(pf) } } @@ -121,8 +122,8 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { return v, nil } - for name := range configs { - if _, err := mergeParent(name); err != nil { + for k := range configs { + if _, err := mergeParent(k); err != nil { return nil, err } } @@ -130,6 +131,15 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { return mergedConfigs, nil } +func canIsNil(v reflect.Value) bool { + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return true + default: + return false + } +} + // Read reads given YAML files and unmarshals them into Config func Read(log logging.Logger, yamlFiles []YamlFile) (*Config, error) { c := Config{ diff --git a/pkg/config/nodegroup.go b/pkg/config/nodegroup.go index 8d0af0e6f..bda3d1714 100644 --- a/pkg/config/nodegroup.go +++ b/pkg/config/nodegroup.go @@ -9,7 +9,7 @@ import ( // NodeGroup represents node group configuration type NodeGroup struct { // parent to inherit settings from - *Inherit `yaml:",inline"` + Inherit `yaml:",inline"` // node group configuration Annotations *map[string]string `yaml:"annotations"` Image *string `yaml:"image"` @@ -33,10 +33,7 @@ type NodeGroup struct { } func (b NodeGroup) GetParentName() string { - if b.Inherit != nil { - return b.ParentName - } - return "" + return b.Inherit.ParentName } // Export exports NodeGroup to orchestration.NodeGroupOptions @@ -47,7 +44,7 @@ func (n *NodeGroup) Export() (o orchestration.NodeGroupOptions) { for i := 0; i < localVal.NumField(); i++ { localField := localVal.Field(i) - if localField.IsValid() && !localField.IsNil() { + if localField.IsValid() && canIsNil(localField) && !localField.IsNil() { localFieldVal := localVal.Field(i).Elem() localFieldName := localType.Field(i).Name From da3ed9fdf92979e3cb00e7de557e34699e728182 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 7 Aug 2026 13:10:25 +0300 Subject: [PATCH 12/21] fix(config): simplify selector on embedded Inherit struct for staticcheck QF1008 --- pkg/config/bee.go | 2 +- pkg/config/cluster.go | 2 +- pkg/config/nodegroup.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/config/bee.go b/pkg/config/bee.go index a0dae55b5..b8ebf8ab2 100644 --- a/pkg/config/bee.go +++ b/pkg/config/bee.go @@ -22,7 +22,7 @@ type BeeConfig struct { } func (b BeeConfig) GetParentName() string { - return b.Inherit.ParentName + return b.ParentName } // Export returns the Bee flag configuration to be rendered into the node's diff --git a/pkg/config/cluster.go b/pkg/config/cluster.go index 845414270..d56b6adea 100644 --- a/pkg/config/cluster.go +++ b/pkg/config/cluster.go @@ -24,7 +24,7 @@ type Cluster struct { } func (b Cluster) GetParentName() string { - return b.Inherit.ParentName + return b.ParentName } // ClusterNodeGroup represents node group in the cluster diff --git a/pkg/config/nodegroup.go b/pkg/config/nodegroup.go index bda3d1714..8b33a52af 100644 --- a/pkg/config/nodegroup.go +++ b/pkg/config/nodegroup.go @@ -33,7 +33,7 @@ type NodeGroup struct { } func (b NodeGroup) GetParentName() string { - return b.Inherit.ParentName + return b.ParentName } // Export exports NodeGroup to orchestration.NodeGroupOptions From 6a59db826002060dd9cccb90db24b203a4217793 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:02:13 +0200 Subject: [PATCH 13/21] fix: refactor scenarios Co-authored-by: Cursor --- config/config.yaml | 7 +- config/local.yaml | 13 +- pkg/check/chunkconvergence/SCENARIOS.md | 84 -- .../chunkconvergence/chunkconvergence.go | 792 -------------- pkg/check/socmatrix/socmatrix.go | 969 ++++++++++++++++-- pkg/check/socmatrix/socmatrix_test.go | 6 + pkg/config/check.go | 90 +- 7 files changed, 923 insertions(+), 1038 deletions(-) delete mode 100644 pkg/check/chunkconvergence/SCENARIOS.md delete mode 100644 pkg/check/chunkconvergence/chunkconvergence.go diff --git a/config/config.yaml b/config/config.yaml index c2487912c..f22cad96c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -355,16 +355,17 @@ checks: request-timeout: 5m timeout: 5m type: soc - chunk-convergence: + socmatrix: options: postage-ttl: 24h postage-depth: 21 - postage-label: chunk-convergence + postage-label: socmatrix-label sync-wait: 45s + sync-retry-interval: 2s request-timeout: 15m password: beekeeper timeout: 20m - type: chunk-convergence + type: socmatrix stake: type: stake timeout: 5m diff --git a/config/local.yaml b/config/local.yaml index 03043d2dc..a504e855b 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -382,16 +382,17 @@ checks: request-timeout: 5m timeout: 5m type: soc - ci-chunk-convergence: + ci-socmatrix: options: postage-ttl: 24h postage-depth: 21 - postage-label: chunk-convergence - sync-wait: 45s - request-timeout: 15m + postage-label: socmatrix-label + sync-wait: 45s # wait after upload for push/pull sync before neighborhood assert (precision 11–16) + sync-retry-interval: 2s + request-timeout: 45m password: beekeeper - timeout: 20m - type: chunk-convergence + timeout: 60m + type: socmatrix ci-content-availability: type: content-availability timeout: 5m diff --git a/pkg/check/chunkconvergence/SCENARIOS.md b/pkg/check/chunkconvergence/SCENARIOS.md deleted file mode 100644 index a698e6685..000000000 --- a/pkg/check/chunkconvergence/SCENARIOS.md +++ /dev/null @@ -1,84 +0,0 @@ -# Chunk convergence check scenarios - -Check: `ci-chunk-convergence` (`chunk-convergence`) - -**Common setup:** pick the closest full-node pair, upload conflicting chunks to the two nodes, wait for sync, then assert both nodes agree on the expected outcome. - ---- - -## 1. Divergent SOC, identical stamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | same | -| Batch | same | -| Stamp | same (identical stamp on both uploads) | -| Stamp index | same | -| Stamp timestamp | same | - -**Expected:** the SOC with the lexicographically lower wrapped CAC wins. - ---- - -## 2. SOC, same index, higher timestamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | same | -| Batch | same | -| Stamp | different (same index, different timestamp → different stamp hash) | -| Stamp index | same | -| Stamp timestamp | different | - -**Expected:** the higher timestamp wins. - ---- - -## 3. SOC, different index, higher timestamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | same | -| Batch | same | -| Stamp | different | -| Stamp index | different (two envelopes) | -| Stamp timestamp | different | - -**Expected:** the higher timestamp wins. - ---- - -## 4. CAC, same index, higher timestamp - -| Field | Same / different | -| --------------- | ------------------------------------------------------------------ | -| Address | different (two CACs in the same postage bucket) | -| Batch | same | -| Stamp | different (signed over different addresses + different timestamps) | -| Stamp index | same | -| Stamp timestamp | different | - -**Expected:** the higher-timestamp CAC is present; the lower-timestamp CAC is absent. - ---- - -## 5. CAC, same index, equal timestamp - -| Field | Same / different | -| --------------- | -------------------------------------------------------------------------------- | -| Address | different (two CACs in the same postage bucket) | -| Batch | same | -| Stamp | same batch / index / timestamp; signatures differ (each stamp is bound to its CAC address) | -| Stamp index | same | -| Stamp timestamp | same | - -**Expected:** the lexicographically lower CAC address wins; the higher-address CAC is absent. - -```bash -# from beekeeper repo, with cluster local-dns up and context k3d-bee -./dist/beekeeper check \ - --config config/beekeeper-local.yaml \ - --config-dir config \ - --cluster-name local-dns \ - --checks ci-chunk-convergence -``` diff --git a/pkg/check/chunkconvergence/chunkconvergence.go b/pkg/check/chunkconvergence/chunkconvergence.go deleted file mode 100644 index 83bf0431b..000000000 --- a/pkg/check/chunkconvergence/chunkconvergence.go +++ /dev/null @@ -1,792 +0,0 @@ -package chunkconvergence - -import ( - "bytes" - "context" - crand "crypto/rand" - "encoding/binary" - "encoding/hex" - "errors" - "fmt" - "strings" - "time" - - "github.com/ethersphere/bee/v2/pkg/cac" - "github.com/ethersphere/bee/v2/pkg/crypto" - "github.com/ethersphere/bee/v2/pkg/postage" - "github.com/ethersphere/bee/v2/pkg/soc" - "github.com/ethersphere/bee/v2/pkg/swarm" - "github.com/ethersphere/beekeeper/pkg/bee" - "github.com/ethersphere/beekeeper/pkg/bee/api" - "github.com/ethersphere/beekeeper/pkg/beekeeper" - "github.com/ethersphere/beekeeper/pkg/logging" - "github.com/ethersphere/beekeeper/pkg/orchestration" - "github.com/ethersphere/beekeeper/pkg/random" -) - -const bucketDepth = postage.BucketDepth - -// Options represents check options. -type Options struct { - PostageTTL time.Duration - PostageDepth uint64 - PostageLabel string - SyncWait time.Duration - RequestTimeout time.Duration - Password string // password for decrypting the batch-issuer Swarm key -} - -// NewDefaultOptions returns new default options. -func NewDefaultOptions() Options { - return Options{ - PostageTTL: 24 * time.Hour, - PostageDepth: 21, - PostageLabel: "chunk-convergence", - SyncWait: 45 * time.Second, - RequestTimeout: 10 * time.Minute, - Password: "beekeeper", - } -} - -var _ beekeeper.Action = (*Check)(nil) - -// Check verifies pullsync convergence for conflicting SOC/CAC stamp claims. -type Check struct { - logger logging.Logger -} - -// NewCheck returns a new check instance. -func NewCheck(logger logging.Logger) beekeeper.Action { - return &Check{logger: logger} -} - -func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { - o, ok := opts.(Options) - if !ok { - return fmt.Errorf("invalid options type") - } - - ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) - defer cancel() - - fullNodes, err := cluster.ShuffledFullNodeClients(ctx, random.PseudoGenerator(time.Now().UnixNano())) - if err != nil { - return fmt.Errorf("full node clients: %w", err) - } - if len(fullNodes) < 2 { - return fmt.Errorf("chunk convergence requires at least 2 full nodes, got %d", len(fullNodes)) - } - - n1, n2, err := pickClosestPair(ctx, fullNodes) - if err != nil { - return err - } - c.logger.Infof("neighborhood upload pair: %s and %s", n1.Name(), n2.Name()) - neighborhood := []*bee.Client{n1, n2} - - issuer, signer, batchID, err := c.setupIssuerBatch(ctx, cluster, o) - if err != nil { - return err - } - c.logger.Infof("batch issuer=%s batch_id=%s", issuer.Name(), batchID) - - scenarios := []struct { - name string - run func() error - }{ - {"1 divergent soc identical stamp", func() error { - return c.scenarioDivergentSOC(ctx, issuer, neighborhood, batchID, o.SyncWait) - }}, - {"2 soc same index higher timestamp", func() error { - return c.scenarioSOCSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) - }}, - {"3 soc different index higher timestamp", func() error { - return c.scenarioSOCDifferentIndexTimestamp(ctx, issuer, neighborhood, batchID, o.SyncWait) - }}, - {"4 cac same index higher timestamp", func() error { - return c.scenarioCACSameIndexTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) - }}, - {"5 cac same index equal timestamp", func() error { - return c.scenarioCACSameIndexEqualTimestamp(ctx, issuer, signer, neighborhood, batchID, o.SyncWait) - }}, - {"6 soc stale stamp redelivery race", func() error { - return c.scenarioSOCStaleStampRedeliveryRace(ctx, issuer, signer, neighborhood, batchID, o, o.SyncWait) - }}, - } - - var errs error - for _, s := range scenarios { - c.logger.Infof("========== %s ==========", s.name) - if err := s.run(); err != nil { - c.logger.Errorf("%s failed: %v", s.name, err) - errs = errors.Join(errs, fmt.Errorf("%s: %w", s.name, err)) - continue - } - c.logger.Infof("%s passed", s.name) - } - return errs -} - -func (c *Check) setupIssuerBatch(ctx context.Context, cluster orchestration.Cluster, o Options) (*bee.Client, crypto.Signer, string, error) { - nodes := cluster.Nodes() - var issuerNode orchestration.Node - var issuerName string - for name, n := range nodes { - if n.SwarmKey() != nil { - issuerNode = n - issuerName = name - break - } - } - if issuerNode == nil { - return nil, nil, "", fmt.Errorf("no node with swarm-key found; bootnode with configured swarm-key is required") - } - - privKey, err := issuerNode.SwarmKey().Decrypt(o.Password) - if err != nil { - return nil, nil, "", fmt.Errorf("decrypt swarm key for %s: %w", issuerName, err) - } - signer := crypto.NewDefaultSigner(privKey) - - clients, err := cluster.NodesClients(ctx) - if err != nil { - return nil, nil, "", fmt.Errorf("nodes clients: %w", err) - } - issuer, ok := clients[issuerName] - if !ok { - return nil, nil, "", fmt.Errorf("client for issuer node %s not found", issuerName) - } - - label := fmt.Sprintf("%s-%s", o.PostageLabel, hex.EncodeToString(randomBytes(4))) - batchID, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, label) - if err != nil { - return nil, nil, "", fmt.Errorf("create batch on %s: %w", issuerName, err) - } - return issuer, signer, batchID, nil -} - -func (c *Check) scenarioDivergentSOC(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { - signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) - if err != nil { - return err - } - low, high, err := orderedSOCs(signer, id) - if err != nil { - return err - } - stamp, err := issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope: %w", err) - } - stampBytes, err := stamp.MarshalBinary() - if err != nil { - return err - } - - c.logger.Infof("soc=%s wrapped_low=%s wrapped_high=%s", addr, mustWrapped(low), mustWrapped(high)) - // Identical stamp: second upload may lose the wrapped-CAC tie-break if the - // winner already synced. Require only the first (winning) upload. - if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, low, high, stampBytes, stampBytes, false); err != nil { - return err - } - time.Sleep(syncWait) - return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, low.Data()) -} - -func (c *Check) scenarioSOCSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { - signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) - if err != nil { - return err - } - older, err := buildSOC(signer, id, append([]byte("soc-same-idx-older-"), randomBytes(4)...)) - if err != nil { - return err - } - newer, err := buildSOC(signer, id, append([]byte("soc-same-idx-newer-"), randomBytes(4)...)) - if err != nil { - return err - } - - batchBytes, err := hex.DecodeString(batchID) - if err != nil { - return err - } - // CreateEnvelope yields a postage-valid index; reuse it with two timestamps. - env, err := issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope for index: %w", err) - } - index := append([]byte{}, env.Index()...) - ts := binary.BigEndian.Uint64(env.Timestamp()) - stampOld, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), addr) - if err != nil { - return fmt.Errorf("sign older stamp: %w", err) - } - stampNew, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), addr) - if err != nil { - return fmt.Errorf("sign newer stamp: %w", err) - } - older = older.WithStamp(stampOld) - newer = newer.WithStamp(stampNew) - oldBytes, _ := stampOld.MarshalBinary() - newBytes, _ := stampNew.MarshalBinary() - - c.logger.Infof("soc=%s index=%s", addr, hex.EncodeToString(index)) - // Older first, then newer — avoids racing a losing upload after the winner synced. - if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { - return err - } - time.Sleep(syncWait) - return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) -} - -func (c *Check) scenarioSOCDifferentIndexTimestamp(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { - signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) - if err != nil { - return err - } - older, err := buildSOC(signer, id, append([]byte("soc-diff-idx-older-"), randomBytes(4)...)) - if err != nil { - return err - } - newer, err := buildSOC(signer, id, append([]byte("soc-diff-idx-newer-"), randomBytes(4)...)) - if err != nil { - return err - } - - stampOld, err := issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope older: %w", err) - } - stampNew, err := issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope newer: %w", err) - } - for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { - stampNew, err = issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope newer retry: %w", err) - } - } - older = older.WithStamp(stampOld) - newer = newer.WithStamp(stampNew) - oldBytes, _ := stampOld.MarshalBinary() - newBytes, _ := stampNew.MarshalBinary() - - c.logger.Infof("soc=%s idx_old=%s idx_new=%s", addr, hex.EncodeToString(stampOld.Index()), hex.EncodeToString(stampNew.Index())) - if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { - return err - } - time.Sleep(syncWait) - return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) -} - -func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { - addrs, err := neighborhood[0].Addresses(ctx) - if err != nil { - return err - } - low, high, err := orderedCACsInBucket(addrs.Overlay) - if err != nil { - return err - } - batchBytes, err := hex.DecodeString(batchID) - if err != nil { - return err - } - // Valid index for this postage bucket (both CACs share the bucket). - env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) - if err != nil { - return fmt.Errorf("envelope for index: %w", err) - } - index := append([]byte{}, env.Index()...) - ts := binary.BigEndian.Uint64(env.Timestamp()) - // higher timestamp on high address chunk so timestamp rule (not address tie-break) decides - stampLow, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), low.Address()) - if err != nil { - return err - } - stampHigh, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), high.Address()) - if err != nil { - return err - } - low = low.WithStamp(stampLow) - high = high.WithStamp(stampHigh) - - c.logger.Infof("cac_low=%s cac_high=%s index=%s", low.Address(), high.Address(), hex.EncodeToString(index)) - if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], low, high); err != nil { - return err - } - time.Sleep(syncWait) - - for _, n := range neighborhood { - hasWant, err := n.HasChunk(ctx, high.Address()) - if err != nil { - return err - } - hasLose, err := n.HasChunk(ctx, low.Address()) - if err != nil { - return err - } - c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) - if !hasWant { - return fmt.Errorf("node %s missing higher-timestamp CAC %s", n.Name(), high.Address()) - } - if hasLose { - return fmt.Errorf("node %s still has lower-timestamp CAC %s", n.Name(), low.Address()) - } - } - return nil -} - -func (c *Check) scenarioCACSameIndexEqualTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { - addrs, err := neighborhood[0].Addresses(ctx) - if err != nil { - return err - } - low, high, err := orderedCACsInBucket(addrs.Overlay) - if err != nil { - return err - } - batchBytes, err := hex.DecodeString(batchID) - if err != nil { - return err - } - // Same batch, stamp index and timestamp; signatures differ (each stamp is - // bound to its CAC address). At equal timestamp the lower address wins. - env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) - if err != nil { - return fmt.Errorf("envelope for index: %w", err) - } - index := append([]byte{}, env.Index()...) - ts := uint64ToBytes(binary.BigEndian.Uint64(env.Timestamp())) - - stampLow, err := signStamp(batchSigner, batchBytes, index, ts, low.Address()) - if err != nil { - return err - } - stampHigh, err := signStamp(batchSigner, batchBytes, index, ts, high.Address()) - if err != nil { - return err - } - low = low.WithStamp(stampLow) - high = high.WithStamp(stampHigh) - - c.logger.Infof("cac_low=%s cac_high=%s index=%s equal_ts address tie-break", low.Address(), high.Address(), hex.EncodeToString(index)) - // Upload loser (higher address) first, then winner (lower address). - if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], high, low); err != nil { - return err - } - time.Sleep(syncWait) - - for _, n := range neighborhood { - hasWant, err := n.HasChunk(ctx, low.Address()) - if err != nil { - return err - } - hasLose, err := n.HasChunk(ctx, high.Address()) - if err != nil { - return err - } - c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) - if !hasWant { - return fmt.Errorf("node %s missing lower-address CAC %s", n.Name(), low.Address()) - } - if hasLose { - return fmt.Errorf("node %s still has higher-address CAC %s", n.Name(), high.Address()) - } - } - return nil -} - -// scenarioSOCStaleStampRedeliveryRace races three divergent SOCs at one address: -// -// SOC₁ under stamp S (batch A, lower timestamp) → uploaded to Sam -// SOC₂ under stamp T (batch B, higher timestamp) → uploaded to Sara -// SOC₃ reuses stamp S with payload P₃ whose wrapped CAC beats P₁ and P₂ → -// uploaded to both nodes in parallel with the SOC₁/SOC₂ uploads -// -// After sync, every neighborhood node must hold P₃ (wrapped-CAC winner under -// the reused stamp S / resolveDivergence). -func (c *Check) scenarioSOCStaleStampRedeliveryRace(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { - sam, sara := neighborhood[0], neighborhood[1] - - batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-stale-"+hex.EncodeToString(randomBytes(3))) - if err != nil { - return fmt.Errorf("batch B: %w", err) - } - - signer, owner, id, addr, err := mineSOC(ctx, sam, sara) - if err != nil { - return err - } - soc1, soc2, soc3, err := orderedStaleRaceSOCs(signer, id) - if err != nil { - return err - } - - batchABytes, err := hex.DecodeString(batchID) - if err != nil { - return err - } - batchBBytes, err := hex.DecodeString(batchB) - if err != nil { - return err - } - - envS, err := issuer.CreateEnvelope(ctx, addr, batchID) - if err != nil { - return fmt.Errorf("envelope stamp S: %w", err) - } - envT, err := issuer.CreateEnvelope(ctx, addr, batchB) - if err != nil { - return fmt.Errorf("envelope stamp T: %w", err) - } - tsS := binary.BigEndian.Uint64(envS.Timestamp()) - tsT := binary.BigEndian.Uint64(envT.Timestamp()) - if tsS <= tsT { - tsS = tsT + 1 - } - stampS, err := signStamp(batchSigner, batchABytes, append([]byte{}, envS.Index()...), uint64ToBytes(tsS), addr) - if err != nil { - return fmt.Errorf("sign stamp S: %w", err) - } - stampT, err := signStamp(batchSigner, batchBBytes, append([]byte{}, envT.Index()...), uint64ToBytes(tsT), addr) - if err != nil { - return fmt.Errorf("sign stamp T: %w", err) - } - stampSBytes, err := stampS.MarshalBinary() - if err != nil { - return err - } - stampTBytes, err := stampT.MarshalBinary() - if err != nil { - return err - } - - c.logger.Infof( - "soc=%s stale-stamp race sam=%s sara=%s wrapped_p1=%s wrapped_p2=%s wrapped_p3=%s batchA=%s… batchB=%s… tsS=%d tsT=%d", - addr, sam.Name(), sara.Name(), mustWrapped(soc1), mustWrapped(soc2), mustWrapped(soc3), - batchID[:8], batchB[:8], tsS, tsT, - ) - - type uploadResult struct { - label string - err error - } - results := make(chan uploadResult, 4) - - go func() { - results <- uploadResult{"SOC1→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc1, stampSBytes)} - }() - go func() { - results <- uploadResult{"SOC2→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc2, stampTBytes)} - }() - go func() { - results <- uploadResult{"SOC3→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc3, stampSBytes)} - }() - go func() { - results <- uploadResult{"SOC3→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc3, stampSBytes)} - }() - - for range 4 { - r := <-results - if r.err != nil { - c.logger.Infof("upload %s returned: %v", r.label, r.err) - } else { - c.logger.Infof("upload %s ok", r.label) - } - } - c.logger.Infof("race uploads finished; waiting %s for neighborhood to settle on P3", syncWait) - - time.Sleep(syncWait) - - for _, n := range neighborhood { - got, err := n.DownloadChunk(ctx, addr, "", nil) - if err != nil { - c.logger.Errorf("%s download after race: %v", n.Name(), err) - continue - } - switch { - case bytes.Equal(got, soc3.Data()): - c.logger.Infof("%s holds P3 — expected", n.Name()) - case bytes.Equal(got, soc2.Data()): - c.logger.Errorf("%s holds P2 (stamp T) — want P3", n.Name()) - case bytes.Equal(got, soc1.Data()): - c.logger.Errorf("%s holds P1 — want P3", n.Name()) - default: - c.logger.Errorf("%s holds unexpected payload len=%d", n.Name(), len(got)) - } - } - - if err := assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, soc3.Data()); err != nil { - return fmt.Errorf("after stale-stamp race (want P3 on both): %w", err) - } - return nil -} - -// orderedStaleRaceSOCs builds three SOCs at the same address where P₃'s wrapped -// CAC is lexicographically lower than both P₁ and P₂, so resolveDivergence would -// accept P₃ over either stored payload when the stale-stamp guard is absent. -func orderedStaleRaceSOCs(signer crypto.Signer, id []byte) (p1, p2, p3 swarm.Chunk, err error) { - for range 10_000 { - p1, err = buildSOC(signer, id, append([]byte("stale-p1-"), randomBytes(8)...)) - if err != nil { - return nil, nil, nil, err - } - p2, err = buildSOC(signer, id, append([]byte("stale-p2-"), randomBytes(8)...)) - if err != nil { - return nil, nil, nil, err - } - p3, err = buildSOC(signer, id, append([]byte("stale-p3-"), randomBytes(8)...)) - if err != nil { - return nil, nil, nil, err - } - w1, w2, w3 := mustWrapped(p1).Bytes(), mustWrapped(p2).Bytes(), mustWrapped(p3).Bytes() - if bytes.Compare(w3, w1) < 0 && bytes.Compare(w3, w2) < 0 && - !bytes.Equal(w1, w2) && !bytes.Equal(w1, w3) && !bytes.Equal(w2, w3) { - return p1, p2, p3, nil - } - } - return nil, nil, nil, fmt.Errorf("could not mine P1/P2/P3 with wrapped(P3) lower than both") -} - -func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch swarm.Chunk, stamp []byte) error { - sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] - idHex := hex.EncodeToString(id) - - for attempt := 0; attempt < 5; attempt++ { - if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { - if strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { - time.Sleep(1 * time.Second) - continue - } - return fmt.Errorf("upload to %s: %w", n.Name(), err) - } - return nil - } - return fmt.Errorf("upload to %s: exceeded 5 retries on 503", n.Name()) -} - -// uploadSOCPair stores ch1 on n1 then ch2 on n2. Callers should pass the -// expected loser/older chunk first so the winner can overwrite during sync. -// If requireSecond is false, a failed second upload is ignored (tie-break loser). -func uploadSOCPair(ctx context.Context, n1, n2 *bee.Client, owner string, id []byte, ch1, ch2 swarm.Chunk, stamp1, stamp2 []byte, requireSecond bool) error { - sig1 := hex.EncodeToString(ch1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - sig2 := hex.EncodeToString(ch2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - wrapped1 := ch1.Data()[swarm.HashSize+swarm.SocSignatureSize:] - wrapped2 := ch2.Data()[swarm.HashSize+swarm.SocSignatureSize:] - idHex := hex.EncodeToString(id) - - if _, err := n1.UploadSOCWithStamp(ctx, owner, idHex, sig1, wrapped1, stamp1); err != nil { - return fmt.Errorf("upload to %s: %w", n1.Name(), err) - } - if _, err := n2.UploadSOCWithStamp(ctx, owner, idHex, sig2, wrapped2, stamp2); err != nil { - if requireSecond { - return fmt.Errorf("upload to %s: %w", n2.Name(), err) - } - } - return nil -} - -func uploadChunkPair(ctx context.Context, n1, n2 *bee.Client, ch1, ch2 swarm.Chunk) error { - st1, err := ch1.Stamp().MarshalBinary() - if err != nil { - return err - } - st2, err := ch2.Stamp().MarshalBinary() - if err != nil { - return err - } - if _, err := n1.UploadChunk(ctx, ch1.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st1), Direct: true}); err != nil { - return fmt.Errorf("upload to %s: %w", n1.Name(), err) - } - if _, err := n2.UploadChunk(ctx, ch2.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st2), Direct: true}); err != nil { - return fmt.Errorf("upload to %s: %w", n2.Name(), err) - } - return nil -} - -func assertNeighborhoodPayload(ctx context.Context, logger logging.Logger, nodes []*bee.Client, addr swarm.Address, want []byte) error { - for _, n := range nodes { - got, err := n.DownloadChunk(ctx, addr, "", nil) - if err != nil { - return fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) - } - if !bytes.Equal(got, want) { - logger.Errorf("node %s payload mismatch want_len=%d got_len=%d", n.Name(), len(want), len(got)) - return fmt.Errorf("node %s: payload mismatch", n.Name()) - } - } - return nil -} - -func pickClosestPair(ctx context.Context, nodes orchestration.ClientList) (*bee.Client, *bee.Client, error) { - overlays := make([]swarm.Address, len(nodes)) - for i, n := range nodes { - a, err := n.Addresses(ctx) - if err != nil { - return nil, nil, fmt.Errorf("addresses for %s: %w", n.Name(), err) - } - overlays[i] = a.Overlay - } - bestI, bestJ, bestPO := 0, 1, -1 - for i := 0; i < len(nodes); i++ { - for j := i + 1; j < len(nodes); j++ { - p := int(swarm.Proximity(overlays[i].Bytes(), overlays[j].Bytes())) - if p > bestPO { - bestPO, bestI, bestJ = p, i, j - } - } - } - if bestPO < 0 { - return nil, nil, fmt.Errorf("could not find a node pair") - } - return nodes[bestI], nodes[bestJ], nil -} - -func mineSOC(ctx context.Context, n1, n2 *bee.Client) (crypto.Signer, string, []byte, swarm.Address, error) { - a1, err := n1.Addresses(ctx) - if err != nil { - return nil, "", nil, swarm.ZeroAddress, err - } - a2, err := n2.Addresses(ctx) - if err != nil { - return nil, "", nil, swarm.ZeroAddress, err - } - t1, t2 := a1.Overlay, a2.Overlay - minPO := swarm.Proximity(t1.Bytes(), t2.Bytes()) - if minPO > 8 { - minPO = 8 - } - - for { - privKey, err := crypto.GenerateSecp256k1Key() - if err != nil { - return nil, "", nil, swarm.ZeroAddress, err - } - signer := crypto.NewDefaultSigner(privKey) - ownerBytes, err := signer.EthereumAddress() - if err != nil { - return nil, "", nil, swarm.ZeroAddress, err - } - owner := hex.EncodeToString(ownerBytes.Bytes()) - for range 8192 { - id := make([]byte, swarm.HashSize) - if _, err := crand.Read(id); err != nil { - return nil, "", nil, swarm.ZeroAddress, err - } - tmp, err := cac.New([]byte("x")) - if err != nil { - continue - } - ch, err := soc.New(id, tmp).Sign(signer) - if err != nil { - continue - } - a := ch.Address() - if swarm.Proximity(a.Bytes(), t1.Bytes()) >= minPO && - swarm.Proximity(a.Bytes(), t2.Bytes()) >= minPO { - return signer, owner, id, a, nil - } - } - } -} - -func orderedSOCs(signer crypto.Signer, id []byte) (low, high swarm.Chunk, err error) { - a, err := buildSOC(signer, id, []byte("soc-conv-payload-aaa")) - if err != nil { - return nil, nil, err - } - b, err := buildSOC(signer, id, []byte("soc-conv-payload-bbb")) - if err != nil { - return nil, nil, err - } - wa, wb := mustWrapped(a), mustWrapped(b) - if bytes.Compare(wa.Bytes(), wb.Bytes()) < 0 { - return a, b, nil - } - return b, a, nil -} - -func buildSOC(signer crypto.Signer, id, payload []byte) (swarm.Chunk, error) { - ch, err := cac.New(payload) - if err != nil { - return nil, err - } - return soc.New(id, ch).Sign(signer) -} - -func orderedCACsInBucket(target swarm.Address) (low, high swarm.Chunk, err error) { - bucket := addressBucket(bucketDepth, target) - a, err := cacInBucket(bucket, append([]byte("cac-conv-aaa-"), randomBytes(8)...)) - if err != nil { - return nil, nil, err - } - b, err := cacInBucket(bucket, append([]byte("cac-conv-bbb-"), randomBytes(8)...)) - if err != nil { - return nil, nil, err - } - for a.Address().Equal(b.Address()) { - b, err = cacInBucket(bucket, randomBytes(32)) - if err != nil { - return nil, nil, err - } - } - if bytes.Compare(a.Address().Bytes(), b.Address().Bytes()) < 0 { - return a, b, nil - } - return b, a, nil -} - -func cacInBucket(bucket uint32, seed []byte) (swarm.Chunk, error) { - for range 5_000_000 { - payload := append(append([]byte{}, seed...), randomBytes(16)...) - ch, err := cac.New(payload) - if err != nil { - return nil, err - } - if addressBucket(bucketDepth, ch.Address()) == bucket { - return ch, nil - } - seed = randomBytes(16) - } - return nil, fmt.Errorf("could not mine CAC into bucket %d", bucket) -} - -func addressBucket(depth uint8, addr swarm.Address) uint32 { - return binary.BigEndian.Uint32(addr.Bytes()[:4]) >> (32 - depth) -} - -func signStamp(signer crypto.Signer, batchID, index, ts []byte, addr swarm.Address) (*postage.Stamp, error) { - digest, err := postage.ToSignDigest(addr.Bytes(), batchID, index, ts) - if err != nil { - return nil, err - } - sig, err := signer.Sign(digest) - if err != nil { - return nil, err - } - return postage.NewStamp(batchID, index, ts, sig), nil -} - -func mustWrapped(ch swarm.Chunk) swarm.Address { - s, err := soc.FromChunk(ch) - if err != nil { - return swarm.ZeroAddress - } - return s.WrappedChunk().Address() -} - -func uint64ToBytes(v uint64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, v) - return b -} - -func randomBytes(n int) []byte { - b := make([]byte, n) - _, _ = crand.Read(b) - return b -} diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go index 61f12326c..cba33a6e4 100644 --- a/pkg/check/socmatrix/socmatrix.go +++ b/pkg/check/socmatrix/socmatrix.go @@ -7,13 +7,17 @@ package socmatrix import ( "bytes" "context" - "crypto/rand" + crand "crypto/rand" + "encoding/binary" "encoding/hex" + "errors" "fmt" + "strings" "time" "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" + "github.com/ethersphere/bee/v2/pkg/postage" "github.com/ethersphere/bee/v2/pkg/soc" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/beekeeper/pkg/bee" @@ -24,25 +28,31 @@ import ( "github.com/ethersphere/beekeeper/pkg/random" ) +const bucketDepth = postage.BucketDepth + // Options holds options for the socmatrix check. type Options struct { - GasPrice string PostageTTL time.Duration PostageDepth uint64 PostageLabel string RequestTimeout time.Duration SyncRetryInterval time.Duration + // SyncWait is used by precision (former chunk-convergence) scenarios. + SyncWait time.Duration + // Password decrypts the batch-issuer Swarm key for precision stamp resigning. + Password string } // NewDefaultOptions returns default options for the check. func NewDefaultOptions() Options { return Options{ - GasPrice: "", PostageTTL: 24 * time.Hour, PostageDepth: 16, PostageLabel: "socmatrix-label", RequestTimeout: 10 * time.Minute, SyncRetryInterval: 2 * time.Second, + SyncWait: 45 * time.Second, + Password: "beekeeper", } } @@ -80,7 +90,7 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any node0 := fullNodeClients[0] node1 := fullNodeClients[1] - c.logger.Infof("socmatrix: running 10-scenario convergence matrix against %d full nodes (node0: '%s', node1: '%s')", + c.logger.Infof("socmatrix: running API-stamp matrix (scenarios 1-10) against %d full nodes (node0: '%s', node1: '%s')", len(fullNodeClients), node0.Name(), node1.Name()) // Shared signer for owner 1 @@ -104,87 +114,112 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) } - // ------------------------------------------------------------------------- - // Scenario 1: Divergent SOC Tie-Break (Equal Timestamp, Same Stamp) - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 1: Divergent SOC Tie-Break (Same Stamp) ---") - if err := c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { - return fmt.Errorf("scenario 1 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 2: Divergent SOC Timestamp Progression (Increasing Timestamp) - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 2: Timestamp Progression (Increasing Timestamp) ---") - if err := c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { - return fmt.Errorf("scenario 2 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 3: Cross-Batch Equal Timestamp Stamp-Hash Precedence - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence ---") - if err := c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { - return fmt.Errorf("scenario 3 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 4: Multi-Stamp Re-Offer Precedence - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 4: Multi-Stamp Re-Offer Precedence ---") - if err := c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { - return fmt.Errorf("scenario 4 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 5: Multi-Batch Sibling Sum Refresh - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 5: Multi-Batch Sibling Sum Refresh ---") - if err := c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o); err != nil { - return fmt.Errorf("scenario 5 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 6: Identical Payload Checksum Matching (Duplicate De-duplication) - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 6: Identical Payload Checksum Matching ---") - if err := c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { - return fmt.Errorf("scenario 6 failed: %w", err) + // API-stamp matrix (1-10): continue-on-error so later scenarios still run. + var matrixErrs error + runMatrix := func(name string, fn func() error) { + c.logger.Infof("--- %s ---", name) + if err := fn(); err != nil { + c.logger.Errorf("%s failed: %v", name, err) + matrixErrs = errors.Join(matrixErrs, fmt.Errorf("%s: %w", name, err)) + return + } + c.logger.Infof("%s passed", name) + } + + runMatrix("Scenario 1: Divergent SOC Tie-Break (Same Stamp)", func() error { + return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + }) + runMatrix("Scenario 2: Timestamp Progression (Increasing Timestamp)", func() error { + return c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + }) + runMatrix("Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence", func() error { + return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + }) + runMatrix("Scenario 4: Multi-Stamp Re-Offer Precedence", func() error { + return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + }) + runMatrix("Scenario 5: Multi-Batch Sibling Sum Refresh", func() error { + return c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + }) + runMatrix("Scenario 6: Identical Payload Checksum Matching", func() error { + return c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + }) + runMatrix("Scenario 7: CAC vs CAC Stamp Index Collision", func() error { + return c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, batchID0, o) + }) + runMatrix("Scenario 8: CAC vs SOC Stamp Index Tie-Break", func() error { + return c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + }) + runMatrix("Scenario 9: 3-Payload Sequenced Divergent SOC Chain", func() error { + return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + }) + runMatrix("Scenario 10: Multi-Owner Independent SOC Isolation", func() error { + return c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, batchID0, o) + }) + + if matrixErrs != nil { + c.logger.Errorf("socmatrix: scenarios 1-10 finished with errors across %d full nodes: %v", len(fullNodeClients), matrixErrs) + } else { + c.logger.Infof("socmatrix: scenarios 1-10 completed successfully across %d full nodes", len(fullNodeClients)) } // ------------------------------------------------------------------------- - // Scenario 7: CAC vs CAC Stamp Index Collision + // Precision scenarios 11-16 (former chunk-convergence) — same semantics. + // Continue-on-error aggregation matches former chunk-convergence Run(). // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 7: CAC vs CAC Stamp Index Collision ---") - if err := c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, batchID0, o); err != nil { - return fmt.Errorf("scenario 7 failed: %w", err) + n1, n2, err := pickClosestPair(ctx, fullNodeClients) + if err != nil { + return err } + c.logger.Infof("precision neighborhood upload pair: %s and %s", n1.Name(), n2.Name()) + neighborhood := []*bee.Client{n1, n2} - // ------------------------------------------------------------------------- - // Scenario 8: CAC vs SOC Stamp Index Tie-Break - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 8: CAC vs SOC Stamp Index Tie-Break ---") - if err := c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { - return fmt.Errorf("scenario 8 failed: %w", err) + issuer, batchSigner, batchID, err := c.setupIssuerBatch(ctx, cluster, o) + if err != nil { + return err } - - // ------------------------------------------------------------------------- - // Scenario 9: 3-Payload Sequenced Divergent SOC Chain - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 9: 3-Payload Sequenced Divergent SOC Chain ---") - if err := c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o); err != nil { - return fmt.Errorf("scenario 9 failed: %w", err) + c.logger.Infof("precision batch issuer=%s batch_id=%s", issuer.Name(), batchID) + + precision := []struct { + name string + run func() error + }{ + {"11 precision divergent soc identical stamp", func() error { + return c.scenarioDivergentSOC(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"12 precision soc same index higher timestamp", func() error { + return c.scenarioSOCSameIndexTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"13 precision soc different index higher timestamp", func() error { + return c.scenarioSOCDifferentIndexTimestamp(ctx, issuer, neighborhood, batchID, o.SyncWait) + }}, + {"14 precision cac same index higher timestamp", func() error { + return c.scenarioCACSameIndexTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"15 precision cac same index equal timestamp", func() error { + return c.scenarioCACSameIndexEqualTimestamp(ctx, issuer, batchSigner, neighborhood, batchID, o.SyncWait) + }}, + {"16 precision soc stale stamp redelivery race", func() error { + return c.scenarioSOCStaleStampRedeliveryRace(ctx, issuer, batchSigner, neighborhood, batchID, o, o.SyncWait) + }}, + } + + var errs error + for _, s := range precision { + c.logger.Infof("--- Scenario %s ---", s.name) + if err := s.run(); err != nil { + c.logger.Errorf("%s failed: %v", s.name, err) + errs = errors.Join(errs, fmt.Errorf("%s: %w", s.name, err)) + continue + } + c.logger.Infof("%s passed", s.name) } - - // ------------------------------------------------------------------------- - // Scenario 10: Multi-Owner Independent SOC Isolation - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 10: Multi-Owner Independent SOC Isolation ---") - if err := c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, batchID0, o); err != nil { - return fmt.Errorf("scenario 10 failed: %w", err) + errs = errors.Join(matrixErrs, errs) + if errs != nil { + return errs } - c.logger.Infof("socmatrix: all 10 matrix scenarios completed successfully across %d full nodes!", len(fullNodeClients)) + c.logger.Infof("socmatrix: all 16 scenarios completed successfully") return nil } @@ -235,11 +270,13 @@ func (c *Check) testDivergentSOCTieBreak( expectedPayload = ch1.Data() } + // Stamp issuer lives only on the batch owner (node0). Upload both payloads + // there; assertClusterConvergence still checks all full nodes after sync. _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) if err != nil { return fmt.Errorf("upload soc1: %w", err) } - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) if err != nil { return fmt.Errorf("upload soc2: %w", err) } @@ -290,7 +327,7 @@ func (c *Check) testTimestampProgression( time.Sleep(1 * time.Second) - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) if err != nil { return fmt.Errorf("upload updated soc2: %w", err) } @@ -510,7 +547,7 @@ func (c *Check) testIdenticalChecksumMatching( if err != nil { return err } - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) if err != nil { return err } @@ -534,11 +571,11 @@ func (c *Check) testCACIndexCollision( return err } - _, err = node0.UploadBytes(ctx, ch1.Data(), api.UploadOptions{BatchID: batchID}) + _, err = node0.UploadChunk(ctx, ch1.Data(), api.UploadOptions{BatchID: batchID}) if err != nil { return fmt.Errorf("upload cac1: %w", err) } - _, err = node1.UploadBytes(ctx, ch2.Data(), api.UploadOptions{BatchID: batchID}) + _, err = node0.UploadChunk(ctx, ch2.Data(), api.UploadOptions{BatchID: batchID}) if err != nil { return fmt.Errorf("upload cac2: %w", err) } @@ -575,11 +612,11 @@ func (c *Check) testCACvsSOCTieBreak( sigHex := hex.EncodeToString(socChunk.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - _, err = node0.UploadBytes(ctx, chCAC.Data(), api.UploadOptions{BatchID: batchID}) + _, err = node0.UploadChunk(ctx, chCAC.Data(), api.UploadOptions{BatchID: batchID}) if err != nil { return err } - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, chCAC.Data(), batchID) + _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, chCAC.Data(), batchID) if err != nil { return err } @@ -622,7 +659,7 @@ func (c *Check) testSequencedDivergentChain( sig3Hex := hex.EncodeToString(soc3.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) - _, _ = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), batchID) // Determine winner among all three @@ -681,7 +718,7 @@ func (c *Check) testMultiOwnerSOCIsolation( if err != nil { return err } - _, err = node1.UploadSOC(ctx, ownerHex2, idHex, sig2Hex, ch2.Data(), batchID) + _, err = node0.UploadSOC(ctx, ownerHex2, idHex, sig2Hex, ch2.Data(), batchID) if err != nil { return err } @@ -702,11 +739,21 @@ func (c *Check) assertClusterConvergence( ticker := time.NewTicker(o.SyncRetryInterval) defer ticker.Stop() + // Bound wait by sync budget, not the parent check timeout (often 15–45m). + waitFor := 5 * o.SyncWait + if waitFor < 2*time.Minute { + waitFor = 2 * time.Minute + } + deadline := time.Now().Add(waitFor) + for { select { case <-ctx.Done(): return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) case <-ticker.C: + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for cluster to converge on %s", address.String()) + } allConverged := true for _, node := range nodes { retrieved, err := node.DownloadChunk(ctx, address, "", nil) @@ -749,7 +796,7 @@ func newTestSigner() (crypto.Signer, string, error) { func randomID() ([]byte, error) { key := make([]byte, 32) - _, err := rand.Read(key) + _, err := crand.Read(key) if err != nil { return nil, err } @@ -767,3 +814,755 @@ func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { } return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil } + +// --------------------------------------------------------------------------- +// Precision scenarios (former chunk-convergence) and helpers +// --------------------------------------------------------------------------- + +func (c *Check) setupIssuerBatch(ctx context.Context, cluster orchestration.Cluster, o Options) (*bee.Client, crypto.Signer, string, error) { + nodes := cluster.Nodes() + var issuerNode orchestration.Node + var issuerName string + for name, n := range nodes { + if n.SwarmKey() != nil { + issuerNode = n + issuerName = name + break + } + } + if issuerNode == nil { + return nil, nil, "", fmt.Errorf("no node with swarm-key found; bootnode with configured swarm-key is required") + } + + privKey, err := issuerNode.SwarmKey().Decrypt(o.Password) + if err != nil { + return nil, nil, "", fmt.Errorf("decrypt swarm key for %s: %w", issuerName, err) + } + signer := crypto.NewDefaultSigner(privKey) + + clients, err := cluster.NodesClients(ctx) + if err != nil { + return nil, nil, "", fmt.Errorf("nodes clients: %w", err) + } + issuer, ok := clients[issuerName] + if !ok { + return nil, nil, "", fmt.Errorf("client for issuer node %s not found", issuerName) + } + + label := fmt.Sprintf("%s-%s", o.PostageLabel, hex.EncodeToString(randomBytes(4))) + batchID, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, label) + if err != nil { + return nil, nil, "", fmt.Errorf("create batch on %s: %w", issuerName, err) + } + return issuer, signer, batchID, nil +} + +func (c *Check) scenarioDivergentSOC(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + low, high, err := orderedSOCs(signer, id) + if err != nil { + return err + } + stamp, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope: %w", err) + } + stampBytes, err := stamp.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof("soc=%s wrapped_low=%s wrapped_high=%s", addr, mustWrapped(low), mustWrapped(high)) + // Identical stamp: losing upload may fail the wrapped-CAC tie-break. + // Require only the first (winning) upload to succeed. + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, low, high, stampBytes, stampBytes, false); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, low.Data()) +} + +func (c *Check) scenarioSOCSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-same-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-same-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // CreateEnvelope yields a postage-valid index; reuse it with two timestamps. + env, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + stampOld, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), addr) + if err != nil { + return fmt.Errorf("sign older stamp: %w", err) + } + stampNew, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), addr) + if err != nil { + return fmt.Errorf("sign newer stamp: %w", err) + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s index=%s", addr, hex.EncodeToString(index)) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioSOCDifferentIndexTimestamp(ctx context.Context, issuer *bee.Client, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + signer, owner, id, addr, err := mineSOC(ctx, neighborhood[0], neighborhood[1]) + if err != nil { + return err + } + older, err := buildSOC(signer, id, append([]byte("soc-diff-idx-older-"), randomBytes(4)...)) + if err != nil { + return err + } + newer, err := buildSOC(signer, id, append([]byte("soc-diff-idx-newer-"), randomBytes(4)...)) + if err != nil { + return err + } + + stampOld, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope older: %w", err) + } + stampNew, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer: %w", err) + } + for binary.BigEndian.Uint64(stampNew.Timestamp()) <= binary.BigEndian.Uint64(stampOld.Timestamp()) { + stampNew, err = issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope newer retry: %w", err) + } + } + older = older.WithStamp(stampOld) + newer = newer.WithStamp(stampNew) + oldBytes, _ := stampOld.MarshalBinary() + newBytes, _ := stampNew.MarshalBinary() + + c.logger.Infof("soc=%s idx_old=%s idx_new=%s", addr, hex.EncodeToString(stampOld.Index()), hex.EncodeToString(stampNew.Index())) + if err := uploadSOCPair(ctx, neighborhood[0], neighborhood[1], owner, id, older, newer, oldBytes, newBytes, true); err != nil { + return err + } + time.Sleep(syncWait) + return assertNeighborhoodPayload(ctx, c.logger, neighborhood, addr, newer.Data()) +} + +func (c *Check) scenarioCACSameIndexTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Valid index for this postage bucket (both CACs share the bucket). + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := binary.BigEndian.Uint64(env.Timestamp()) + // higher timestamp on high address chunk so timestamp rule (not address tie-break) decides + stampLow, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts), low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, uint64ToBytes(ts+1), high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s", low.Address(), high.Address(), hex.EncodeToString(index)) + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], low, high); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing higher-timestamp CAC %s", n.Name(), high.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has lower-timestamp CAC %s", n.Name(), low.Address()) + } + } + return nil +} + +func (c *Check) scenarioCACSameIndexEqualTimestamp(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, syncWait time.Duration) error { + addrs, err := neighborhood[0].Addresses(ctx) + if err != nil { + return err + } + low, high, err := orderedCACsInBucket(addrs.Overlay) + if err != nil { + return err + } + batchBytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + // Same batch, stamp index and timestamp; signatures differ (each stamp is + // bound to its CAC address). At equal timestamp the lower address wins. + env, err := issuer.CreateEnvelope(ctx, low.Address(), batchID) + if err != nil { + return fmt.Errorf("envelope for index: %w", err) + } + index := append([]byte{}, env.Index()...) + ts := uint64ToBytes(binary.BigEndian.Uint64(env.Timestamp())) + + stampLow, err := signStamp(batchSigner, batchBytes, index, ts, low.Address()) + if err != nil { + return err + } + stampHigh, err := signStamp(batchSigner, batchBytes, index, ts, high.Address()) + if err != nil { + return err + } + low = low.WithStamp(stampLow) + high = high.WithStamp(stampHigh) + + c.logger.Infof("cac_low=%s cac_high=%s index=%s equal_ts address tie-break", low.Address(), high.Address(), hex.EncodeToString(index)) + // Parallel uploads: loser (higher address) and winner (lower address). + if err := uploadChunkPair(ctx, neighborhood[0], neighborhood[1], high, low); err != nil { + return err + } + time.Sleep(syncWait) + + for _, n := range neighborhood { + hasWant, err := n.HasChunk(ctx, low.Address()) + if err != nil { + return err + } + hasLose, err := n.HasChunk(ctx, high.Address()) + if err != nil { + return err + } + c.logger.Infof("%s has_winner=%v has_loser=%v", n.Name(), hasWant, hasLose) + if !hasWant { + return fmt.Errorf("node %s missing lower-address CAC %s", n.Name(), low.Address()) + } + if hasLose { + return fmt.Errorf("node %s still has higher-address CAC %s", n.Name(), high.Address()) + } + } + return nil +} + +// scenarioSOCStaleStampRedeliveryRace races three divergent SOCs at one address: +// +// SOC₁ under stamp S (batch A) → uploaded to Sam +// SOC₂ under stamp T (batch B) → uploaded to Sara +// SOC₃ reuses stamp S with payload P₃ whose wrapped CAC beats P₁ and P₂ → +// uploaded to both nodes in parallel with the SOC₁/SOC₂ uploads +// +// After sync, every neighborhood node must hold the same payload among P₁/P₂/P₃ +// (which one wins does not matter; agreement does). +func (c *Check) scenarioSOCStaleStampRedeliveryRace(ctx context.Context, issuer *bee.Client, batchSigner crypto.Signer, neighborhood []*bee.Client, batchID string, o Options, syncWait time.Duration) error { + sam, sara := neighborhood[0], neighborhood[1] + + batchB, err := issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-stale-"+hex.EncodeToString(randomBytes(3))) + if err != nil { + return fmt.Errorf("batch B: %w", err) + } + + signer, owner, id, addr, err := mineSOC(ctx, sam, sara) + if err != nil { + return err + } + soc1, soc2, soc3, err := orderedStaleRaceSOCs(signer, id) + if err != nil { + return err + } + + batchABytes, err := hex.DecodeString(batchID) + if err != nil { + return err + } + batchBBytes, err := hex.DecodeString(batchB) + if err != nil { + return err + } + + envS, err := issuer.CreateEnvelope(ctx, addr, batchID) + if err != nil { + return fmt.Errorf("envelope stamp S: %w", err) + } + envT, err := issuer.CreateEnvelope(ctx, addr, batchB) + if err != nil { + return fmt.Errorf("envelope stamp T: %w", err) + } + tsS := binary.BigEndian.Uint64(envS.Timestamp()) + tsT := binary.BigEndian.Uint64(envT.Timestamp()) + if tsS <= tsT { + tsS = tsT + 1 + } + stampS, err := signStamp(batchSigner, batchABytes, append([]byte{}, envS.Index()...), uint64ToBytes(tsS), addr) + if err != nil { + return fmt.Errorf("sign stamp S: %w", err) + } + stampT, err := signStamp(batchSigner, batchBBytes, append([]byte{}, envT.Index()...), uint64ToBytes(tsT), addr) + if err != nil { + return fmt.Errorf("sign stamp T: %w", err) + } + stampSBytes, err := stampS.MarshalBinary() + if err != nil { + return err + } + stampTBytes, err := stampT.MarshalBinary() + if err != nil { + return err + } + + c.logger.Infof( + "soc=%s stale-stamp race sam=%s sara=%s wrapped_p1=%s wrapped_p2=%s wrapped_p3=%s batchA=%s… batchB=%s… tsS=%d tsT=%d", + addr, sam.Name(), sara.Name(), mustWrapped(soc1), mustWrapped(soc2), mustWrapped(soc3), + batchID[:8], batchB[:8], tsS, tsT, + ) + + type uploadResult struct { + label string + err error + } + results := make(chan uploadResult, 4) + + go func() { + results <- uploadResult{"SOC1→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc1, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC2→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc2, stampTBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sam.Name(), uploadSOC(ctx, sam, owner, id, soc3, stampSBytes)} + }() + go func() { + results <- uploadResult{"SOC3→" + sara.Name(), uploadSOC(ctx, sara, owner, id, soc3, stampSBytes)} + }() + + for range 4 { + r := <-results + if r.err != nil { + c.logger.Infof("upload %s returned: %v", r.label, r.err) + } else { + c.logger.Infof("upload %s ok", r.label) + } + } + c.logger.Infof("race uploads finished; waiting %s for neighborhood agreement on any of P1/P2/P3", syncWait) + + time.Sleep(syncWait) + + candidates := []struct { + label string + data []byte + }{ + {"P1", soc1.Data()}, + {"P2", soc2.Data()}, + {"P3", soc3.Data()}, + } + + agreed, err := assertNeighborhoodAgreesAmong(ctx, c.logger, neighborhood, addr, candidates) + if err != nil { + return fmt.Errorf("after stale-stamp race (want same payload on both): %w", err) + } + c.logger.Infof("neighborhood converged on %s", agreed) + return nil +} + +// orderedStaleRaceSOCs builds three SOCs at the same address where P₃'s wrapped +// CAC is lexicographically lower than both P₁ and P₂, so resolveDivergence would +// accept P₃ over either stored payload when the stale-stamp guard is absent. +func orderedStaleRaceSOCs(signer crypto.Signer, id []byte) (p1, p2, p3 swarm.Chunk, err error) { + for range 10_000 { + p1, err = buildSOC(signer, id, append([]byte("stale-p1-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p2, err = buildSOC(signer, id, append([]byte("stale-p2-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + p3, err = buildSOC(signer, id, append([]byte("stale-p3-"), randomBytes(8)...)) + if err != nil { + return nil, nil, nil, err + } + w1, w2, w3 := mustWrapped(p1).Bytes(), mustWrapped(p2).Bytes(), mustWrapped(p3).Bytes() + if bytes.Compare(w3, w1) < 0 && bytes.Compare(w3, w2) < 0 && + !bytes.Equal(w1, w2) && !bytes.Equal(w1, w3) && !bytes.Equal(w2, w3) { + return p1, p2, p3, nil + } + } + return nil, nil, nil, fmt.Errorf("could not mine P1/P2/P3 with wrapped(P3) lower than both") +} + +func uploadSOC(ctx context.Context, n *bee.Client, owner string, id []byte, ch swarm.Chunk, stamp []byte) error { + sig := hex.EncodeToString(ch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + wrapped := ch.Data()[swarm.HashSize+swarm.SocSignatureSize:] + idHex := hex.EncodeToString(id) + + for attempt := 0; attempt < 5; attempt++ { + if _, err := n.UploadSOCWithStamp(ctx, owner, idHex, sig, wrapped, stamp); err != nil { + if strings.Contains(err.Error(), "503") || strings.Contains(err.Error(), "Service Unavailable") { + time.Sleep(1 * time.Second) + continue + } + return fmt.Errorf("upload to %s: %w", n.Name(), err) + } + return nil + } + return fmt.Errorf("upload to %s: exceeded 5 retries on 503", n.Name()) +} + +// uploadSOCPair uploads ch1 to n1 and ch2 to n2 in parallel. +// If requireSecond is false, a failed second upload is ignored (tie-break loser). +func uploadSOCPair(ctx context.Context, n1, n2 *bee.Client, owner string, id []byte, ch1, ch2 swarm.Chunk, stamp1, stamp2 []byte, requireSecond bool) error { + type result struct { + which int // 1 or 2 + err error + } + results := make(chan result, 2) + + go func() { + results <- result{1, uploadSOC(ctx, n1, owner, id, ch1, stamp1)} + }() + go func() { + results <- result{2, uploadSOC(ctx, n2, owner, id, ch2, stamp2)} + }() + + var err1, err2 error + for range 2 { + r := <-results + switch r.which { + case 1: + err1 = r.err + case 2: + err2 = r.err + } + } + if err1 != nil { + return err1 + } + if err2 != nil && requireSecond { + return err2 + } + return nil +} + +// uploadChunkPair uploads ch1 to n1 and ch2 to n2 in parallel. +func uploadChunkPair(ctx context.Context, n1, n2 *bee.Client, ch1, ch2 swarm.Chunk) error { + st1, err := ch1.Stamp().MarshalBinary() + if err != nil { + return err + } + st2, err := ch2.Stamp().MarshalBinary() + if err != nil { + return err + } + + type result struct { + name string + err error + } + results := make(chan result, 2) + + go func() { + _, err := n1.UploadChunk(ctx, ch1.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st1), Direct: true}) + if err != nil { + err = fmt.Errorf("upload to %s: %w", n1.Name(), err) + } + results <- result{n1.Name(), err} + }() + go func() { + _, err := n2.UploadChunk(ctx, ch2.Data(), api.UploadOptions{Stamp: hex.EncodeToString(st2), Direct: true}) + if err != nil { + err = fmt.Errorf("upload to %s: %w", n2.Name(), err) + } + results <- result{n2.Name(), err} + }() + + var errs error + for range 2 { + r := <-results + if r.err != nil { + errs = errors.Join(errs, r.err) + } + } + return errs +} + +func assertNeighborhoodPayload(ctx context.Context, logger logging.Logger, nodes []*bee.Client, addr swarm.Address, want []byte) error { + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + if !bytes.Equal(got, want) { + logger.Errorf("node %s payload mismatch want_len=%d got_len=%d", n.Name(), len(want), len(got)) + return fmt.Errorf("node %s: payload mismatch", n.Name()) + } + } + return nil +} + +// assertNeighborhoodAgreesAmong checks that every node holds the same body and +// that body matches one of the labeled candidates. Returns the winning label. +func assertNeighborhoodAgreesAmong( + ctx context.Context, + logger logging.Logger, + nodes []*bee.Client, + addr swarm.Address, + candidates []struct { + label string + data []byte + }, +) (string, error) { + if len(nodes) == 0 { + return "", fmt.Errorf("no nodes") + } + if len(candidates) == 0 { + return "", fmt.Errorf("no candidates") + } + + matchLabel := func(got []byte) string { + for _, c := range candidates { + if bytes.Equal(got, c.data) { + return c.label + } + } + return "" + } + + var agreed []byte + var agreedLabel string + for _, n := range nodes { + got, err := n.DownloadChunk(ctx, addr, "", nil) + if err != nil { + return "", fmt.Errorf("node %s download %s: %w", n.Name(), addr, err) + } + label := matchLabel(got) + if label == "" { + logger.Errorf("node %s holds unrecognized payload len=%d", n.Name(), len(got)) + return "", fmt.Errorf("node %s: payload not among candidates", n.Name()) + } + logger.Infof("%s holds %s", n.Name(), label) + if agreed == nil { + agreed = got + agreedLabel = label + continue + } + if !bytes.Equal(got, agreed) { + logger.Errorf("node %s holds %s, others hold %s", n.Name(), label, agreedLabel) + return "", fmt.Errorf("node %s: desync (%s vs %s)", n.Name(), label, agreedLabel) + } + } + return agreedLabel, nil +} + +func pickClosestPair(ctx context.Context, nodes orchestration.ClientList) (*bee.Client, *bee.Client, error) { + overlays := make([]swarm.Address, len(nodes)) + for i, n := range nodes { + a, err := n.Addresses(ctx) + if err != nil { + return nil, nil, fmt.Errorf("addresses for %s: %w", n.Name(), err) + } + overlays[i] = a.Overlay + } + bestI, bestJ, bestPO := 0, 1, -1 + for i := 0; i < len(nodes); i++ { + for j := i + 1; j < len(nodes); j++ { + p := int(swarm.Proximity(overlays[i].Bytes(), overlays[j].Bytes())) + if p > bestPO { + bestPO, bestI, bestJ = p, i, j + } + } + } + if bestPO < 0 { + return nil, nil, fmt.Errorf("could not find a node pair") + } + return nodes[bestI], nodes[bestJ], nil +} + +func mineSOC(ctx context.Context, n1, n2 *bee.Client) (crypto.Signer, string, []byte, swarm.Address, error) { + a1, err := n1.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + a2, err := n2.Addresses(ctx) + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + t1, t2 := a1.Overlay, a2.Overlay + minPO := swarm.Proximity(t1.Bytes(), t2.Bytes()) + if minPO > 8 { + minPO = 8 + } + + for { + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + signer := crypto.NewDefaultSigner(privKey) + ownerBytes, err := signer.EthereumAddress() + if err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + owner := hex.EncodeToString(ownerBytes.Bytes()) + for range 8192 { + id := make([]byte, swarm.HashSize) + if _, err := crand.Read(id); err != nil { + return nil, "", nil, swarm.ZeroAddress, err + } + tmp, err := cac.New([]byte("x")) + if err != nil { + continue + } + ch, err := soc.New(id, tmp).Sign(signer) + if err != nil { + continue + } + a := ch.Address() + if swarm.Proximity(a.Bytes(), t1.Bytes()) >= minPO && + swarm.Proximity(a.Bytes(), t2.Bytes()) >= minPO { + return signer, owner, id, a, nil + } + } + } +} + +func orderedSOCs(signer crypto.Signer, id []byte) (low, high swarm.Chunk, err error) { + a, err := buildSOC(signer, id, []byte("soc-conv-payload-aaa")) + if err != nil { + return nil, nil, err + } + b, err := buildSOC(signer, id, []byte("soc-conv-payload-bbb")) + if err != nil { + return nil, nil, err + } + wa, wb := mustWrapped(a), mustWrapped(b) + if bytes.Compare(wa.Bytes(), wb.Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func buildSOC(signer crypto.Signer, id, payload []byte) (swarm.Chunk, error) { + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + return soc.New(id, ch).Sign(signer) +} + +func orderedCACsInBucket(target swarm.Address) (low, high swarm.Chunk, err error) { + bucket := addressBucket(bucketDepth, target) + a, err := cacInBucket(bucket, append([]byte("cac-conv-aaa-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + b, err := cacInBucket(bucket, append([]byte("cac-conv-bbb-"), randomBytes(8)...)) + if err != nil { + return nil, nil, err + } + for a.Address().Equal(b.Address()) { + b, err = cacInBucket(bucket, randomBytes(32)) + if err != nil { + return nil, nil, err + } + } + if bytes.Compare(a.Address().Bytes(), b.Address().Bytes()) < 0 { + return a, b, nil + } + return b, a, nil +} + +func cacInBucket(bucket uint32, seed []byte) (swarm.Chunk, error) { + for range 5_000_000 { + payload := append(append([]byte{}, seed...), randomBytes(16)...) + ch, err := cac.New(payload) + if err != nil { + return nil, err + } + if addressBucket(bucketDepth, ch.Address()) == bucket { + return ch, nil + } + seed = randomBytes(16) + } + return nil, fmt.Errorf("could not mine CAC into bucket %d", bucket) +} + +func addressBucket(depth uint8, addr swarm.Address) uint32 { + return binary.BigEndian.Uint32(addr.Bytes()[:4]) >> (32 - depth) +} + +func signStamp(signer crypto.Signer, batchID, index, ts []byte, addr swarm.Address) (*postage.Stamp, error) { + digest, err := postage.ToSignDigest(addr.Bytes(), batchID, index, ts) + if err != nil { + return nil, err + } + sig, err := signer.Sign(digest) + if err != nil { + return nil, err + } + return postage.NewStamp(batchID, index, ts, sig), nil +} + +func mustWrapped(ch swarm.Chunk) swarm.Address { + s, err := soc.FromChunk(ch) + if err != nil { + return swarm.ZeroAddress + } + return s.WrappedChunk().Address() +} + +func uint64ToBytes(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + _, _ = crand.Read(b) + return b +} diff --git a/pkg/check/socmatrix/socmatrix_test.go b/pkg/check/socmatrix/socmatrix_test.go index e1d444086..1fe938d8d 100644 --- a/pkg/check/socmatrix/socmatrix_test.go +++ b/pkg/check/socmatrix/socmatrix_test.go @@ -24,6 +24,12 @@ func TestNewDefaultOptions(t *testing.T) { if opts.RequestTimeout != 10*time.Minute { t.Fatalf("expected RequestTimeout 10m, got %v", opts.RequestTimeout) } + if opts.SyncWait != 45*time.Second { + t.Fatalf("expected SyncWait 45s, got %v", opts.SyncWait) + } + if opts.Password != "beekeeper" { + t.Fatalf("expected Password beekeeper, got %q", opts.Password) + } } func TestNewCheck(t *testing.T) { diff --git a/pkg/config/check.go b/pkg/config/check.go index eaf1b83a3..ecdb22ca8 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -12,7 +12,6 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/autotls" "github.com/ethersphere/beekeeper/pkg/check/balances" "github.com/ethersphere/beekeeper/pkg/check/cashout" - "github.com/ethersphere/beekeeper/pkg/check/chunkconvergence" "github.com/ethersphere/beekeeper/pkg/check/datadurability" "github.com/ethersphere/beekeeper/pkg/check/feed" "github.com/ethersphere/beekeeper/pkg/check/fileretrieval" @@ -63,6 +62,26 @@ type CheckGlobalConfig struct { GethURL string } +func newSOCMatrixOptions(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` + SyncWait *time.Duration `yaml:"sync-wait"` + Password *string `yaml:"password"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := socmatrix.NewDefaultOptions() + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + return opts, nil +} + // Checks represents all available check types var Checks = map[string]CheckType{ "act": { @@ -554,51 +573,9 @@ var Checks = map[string]CheckType{ return opts, nil }, }, - "socconvergence": { - NewAction: socconvergence.NewCheck, - NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { - checkOpts := new(struct { - GasPrice *string `yaml:"gas-price"` - PostageTTL *time.Duration `yaml:"postage-ttl"` - PostageDepth *uint64 `yaml:"postage-depth"` - PostageLabel *string `yaml:"postage-label"` - RequestTimeout *time.Duration `yaml:"request-timeout"` - SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` - }) - if err := check.Options.Decode(checkOpts); err != nil { - return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) - } - opts := socconvergence.NewDefaultOptions() - - if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { - return nil, fmt.Errorf("applying options: %w", err) - } - - return opts, nil - }, - }, "socmatrix": { - NewAction: socmatrix.NewCheck, - NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { - checkOpts := new(struct { - GasPrice *string `yaml:"gas-price"` - PostageTTL *time.Duration `yaml:"postage-ttl"` - PostageDepth *uint64 `yaml:"postage-depth"` - PostageLabel *string `yaml:"postage-label"` - RequestTimeout *time.Duration `yaml:"request-timeout"` - SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` - }) - if err := check.Options.Decode(checkOpts); err != nil { - return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) - } - opts := socmatrix.NewDefaultOptions() - - if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { - return nil, fmt.Errorf("applying options: %w", err) - } - - return opts, nil - }, + NewAction: socmatrix.NewCheck, + NewOptions: newSOCMatrixOptions, }, "postage": { NewAction: postage.NewCheck, @@ -765,29 +742,6 @@ var Checks = map[string]CheckType{ return opts, nil }, }, - "chunk-convergence": { - NewAction: chunkconvergence.NewCheck, - NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { - checkOpts := new(struct { - PostageTTL *time.Duration `yaml:"postage-ttl"` - PostageDepth *uint64 `yaml:"postage-depth"` - PostageLabel *string `yaml:"postage-label"` - SyncWait *time.Duration `yaml:"sync-wait"` - RequestTimeout *time.Duration `yaml:"request-timeout"` - Password *string `yaml:"password"` - }) - if err := check.Options.Decode(checkOpts); err != nil { - return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) - } - opts := chunkconvergence.NewDefaultOptions() - - if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { - return nil, fmt.Errorf("applying options: %w", err) - } - - return opts, nil - }, - }, "feed-v1": { NewAction: feed.NewCheckV1, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { From 250069c2127412d8ff0cd72e8b74ef1d5207af59 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:14:59 +0200 Subject: [PATCH 14/21] chore: update go.sum --- go.sum | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/go.sum b/go.sum index 4d8912e56..b6a41f328 100644 --- a/go.sum +++ b/go.sum @@ -342,6 +342,7 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= @@ -350,6 +351,7 @@ github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= @@ -367,6 +369,7 @@ github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiA github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= @@ -449,6 +452,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -471,6 +476,7 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -481,6 +487,7 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -524,13 +531,19 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -539,6 +552,12 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -547,6 +566,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -559,21 +580,37 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -582,6 +619,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From de644dcb927d78329310fcb26bfccad3a21a30e7 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:24:53 +0200 Subject: [PATCH 15/21] fix: remove old checks --- pkg/check/socconvergence/socconvergence.go | 477 ------------------ .../socconvergence/socconvergence_test.go | 34 -- pkg/config/check.go | 24 - 3 files changed, 535 deletions(-) delete mode 100644 pkg/check/socconvergence/socconvergence.go delete mode 100644 pkg/check/socconvergence/socconvergence_test.go diff --git a/pkg/check/socconvergence/socconvergence.go b/pkg/check/socconvergence/socconvergence.go deleted file mode 100644 index 562eb67ef..000000000 --- a/pkg/check/socconvergence/socconvergence.go +++ /dev/null @@ -1,477 +0,0 @@ -// 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 socconvergence - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "time" - - "github.com/ethersphere/bee/v2/pkg/cac" - "github.com/ethersphere/bee/v2/pkg/crypto" - "github.com/ethersphere/bee/v2/pkg/soc" - "github.com/ethersphere/bee/v2/pkg/swarm" - "github.com/ethersphere/beekeeper/pkg/bee" - "github.com/ethersphere/beekeeper/pkg/beekeeper" - "github.com/ethersphere/beekeeper/pkg/logging" - "github.com/ethersphere/beekeeper/pkg/orchestration" - "github.com/ethersphere/beekeeper/pkg/random" -) - -// Options represents check options -type Options struct { - GasPrice string - PostageTTL time.Duration - PostageDepth uint64 - PostageLabel string - RequestTimeout time.Duration - SyncRetryInterval time.Duration -} - -// NewDefaultOptions returns new default options -func NewDefaultOptions() Options { - return Options{ - GasPrice: "", - PostageTTL: 24 * time.Hour, - PostageDepth: 16, - PostageLabel: "soc-convergence-label", - RequestTimeout: 5 * time.Minute, - SyncRetryInterval: 2 * time.Second, - } -} - -// compile check whether Check implements interface -var _ beekeeper.Action = (*Check)(nil) - -// Check instance. -type Check struct { - logger logging.Logger -} - -// NewCheck returns a new check instance. -func NewCheck(logger logging.Logger) beekeeper.Action { - return &Check{ - logger: logger, - } -} - -func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { - o, ok := opts.(Options) - if !ok { - return fmt.Errorf("invalid options type") - } - - ctx, cancel := context.WithTimeout(ctx, o.RequestTimeout) - defer cancel() - - rnd := random.PseudoGenerator(time.Now().UnixNano()) - fullNodeClients, err := cluster.ShuffledFullNodeClients(ctx, rnd) - if err != nil { - return fmt.Errorf("shuffled full node clients: %w", err) - } - - if len(fullNodeClients) < 2 { - return fmt.Errorf("socconvergence test requires at least 2 full nodes") - } - - node0 := fullNodeClients[0] - node1 := fullNodeClients[1] - - c.logger.Infof("socconvergence: using node0 '%s' and node1 '%s' (total full nodes: %d)", - node0.Name(), node1.Name(), len(fullNodeClients)) - - // Shared signer for same owner address - privKey, err := crypto.GenerateSecp256k1Key() - if err != nil { - return fmt.Errorf("generate secp256k1 key: %w", err) - } - signer := crypto.NewDefaultSigner(privKey) - pubKey, err := signer.PublicKey() - if err != nil { - return fmt.Errorf("signer public key: %w", err) - } - ownerBytes, err := crypto.NewEthereumAddress(*pubKey) - if err != nil { - return fmt.Errorf("ethereum address: %w", err) - } - ownerHex := hex.EncodeToString(ownerBytes) - - // Create postage batch on node0 - batchID0, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) - if err != nil { - return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) - } - c.logger.Infof("socconvergence: created batchID %s on node %s", batchID0, node0.Name()) - - // ------------------------------------------------------------------------- - // Scenario 1: Divergent SOC Tie-Break Convergence (SWIP-101) - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 1: Divergent SOC Tie-Break Convergence ---") - if err := c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { - return fmt.Errorf("scenario 1 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 2: Multi-Batch Divergent SOC Convergence - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 2: Multi-Batch Divergent SOC Convergence ---") - batchID1, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") - if err != nil { - return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) - } - if err := c.testMultiBatchDivergence(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, batchID1, o); err != nil { - return fmt.Errorf("scenario 2 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 3: Identical Payload Checksum Matching - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 3: Identical Payload Checksum Matching ---") - if err := c.testIdenticalSOCChecksum(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, o); err != nil { - return fmt.Errorf("scenario 3 failed: %w", err) - } - - // ------------------------------------------------------------------------- - // Scenario 4: Multi-Stamp Re-offer Precedence - // ------------------------------------------------------------------------- - c.logger.Infof("--- Scenario 4: Multi-Stamp Re-offer Precedence ---") - if err := c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex, signer, batchID0, batchID1, o); err != nil { - return fmt.Errorf("scenario 4 failed: %w", err) - } - - c.logger.Infof("socconvergence: all integration test scenarios passed successfully!") - return nil -} - -func (c *Check) testDivergentSOCTieBreak( - ctx context.Context, - fullNodes []*bee.Client, - node0, node1 *bee.Client, - ownerHex string, - signer crypto.Signer, - batchID string, - o Options, -) error { - idBytes, err := randomID() - if err != nil { - return err - } - idHex := hex.EncodeToString(idBytes) - - // Create 2 divergent payloads wrapping different CACs - payload1 := []byte("soc-convergence-payload-alpha-101") - payload2 := []byte("soc-convergence-payload-beta-101") - - ch1, err := cac.New(payload1) - if err != nil { - return fmt.Errorf("cac 1: %w", err) - } - ch2, err := cac.New(payload2) - if err != nil { - return fmt.Errorf("cac 2: %w", err) - } - - soc1, err := soc.New(idBytes, ch1).Sign(signer) - if err != nil { - return fmt.Errorf("sign soc 1: %w", err) - } - soc2, err := soc.New(idBytes, ch2).Sign(signer) - if err != nil { - return fmt.Errorf("sign soc 2: %w", err) - } - - // Extract signatures - sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - - socAddress := soc1.Address() - c.logger.Infof("divergent soc address: %s", socAddress.String()) - - // Determine theoretical tie-break winner (lexicographically lower wrapped CAC) - wins, err := divergentChunkWins(soc1, soc2) - if err != nil { - return fmt.Errorf("divergent chunk wins comparison: %w", err) - } - - var expectedWinnerPayload []byte - var winnerName string - if wins { - expectedWinnerPayload = ch2.Data() - winnerName = "soc2 (payload2)" - } else { - expectedWinnerPayload = ch1.Data() - winnerName = "soc1 (payload1)" - } - c.logger.Infof("theoretical tie-break winner: %s", winnerName) - - // Upload soc1 to node0 - ref1, err := node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) - if err != nil { - return fmt.Errorf("upload soc1 to node %s: %w", node0.Name(), err) - } - c.logger.Infof("uploaded soc1 to node %s: ref %s", node0.Name(), ref1.String()) - - // Upload soc2 to node1 - ref2, err := node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) - if err != nil { - return fmt.Errorf("upload soc2 to node %s: %w", node1.Name(), err) - } - c.logger.Infof("uploaded soc2 to node %s: ref %s", node1.Name(), ref2.String()) - - // Wait for cluster pullsync convergence across all full nodes - c.logger.Infof("waiting for pullsync cluster convergence on address %s...", socAddress.String()) - return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) -} - -func (c *Check) testMultiBatchDivergence( - ctx context.Context, - fullNodes []*bee.Client, - node0, node1 *bee.Client, - ownerHex string, - signer crypto.Signer, - batchID0, batchID1 string, - o Options, -) error { - idBytes, err := randomID() - if err != nil { - return err - } - idHex := hex.EncodeToString(idBytes) - - payload1 := []byte("multi-batch-payload-gamma-201") - payload2 := []byte("multi-batch-payload-delta-202") - - ch1, err := cac.New(payload1) - if err != nil { - return err - } - ch2, err := cac.New(payload2) - if err != nil { - return err - } - - soc1, err := soc.New(idBytes, ch1).Sign(signer) - if err != nil { - return err - } - soc2, err := soc.New(idBytes, ch2).Sign(signer) - if err != nil { - return err - } - - sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - socAddress := soc1.Address() - - wins, err := divergentChunkWins(soc1, soc2) - if err != nil { - return err - } - var expectedWinnerPayload []byte - if wins { - expectedWinnerPayload = ch2.Data() - } else { - expectedWinnerPayload = ch1.Data() - } - - // Upload to node0 under batchID0 - _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) - if err != nil { - return fmt.Errorf("upload soc1 multi-batch: %w", err) - } - - // Upload to node1 under batchID1 - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) - if err != nil { - return fmt.Errorf("upload soc2 multi-batch: %w", err) - } - - return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) -} - -func (c *Check) testIdenticalSOCChecksum( - ctx context.Context, - fullNodes []*bee.Client, - node0, node1 *bee.Client, - ownerHex string, - signer crypto.Signer, - batchID string, - o Options, -) error { - idBytes, err := randomID() - if err != nil { - return err - } - idHex := hex.EncodeToString(idBytes) - - payload := []byte("identical-soc-checksum-payload-301") - ch, err := cac.New(payload) - if err != nil { - return err - } - - sch, err := soc.New(idBytes, ch).Sign(signer) - if err != nil { - return err - } - - sigHex := hex.EncodeToString(sch.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - socAddress := sch.Address() - - // Upload exact same SOC to node0 and node1 - _, err = node0.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) - if err != nil { - return fmt.Errorf("upload identical to node0: %w", err) - } - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sigHex, ch.Data(), batchID) - if err != nil { - return fmt.Errorf("upload identical to node1: %w", err) - } - - return c.assertClusterConvergence(ctx, fullNodes, socAddress, ch.Data(), o) -} - -func (c *Check) testMultiStampReofferPrecedence( - ctx context.Context, - fullNodes []*bee.Client, - node0, node1 *bee.Client, - ownerHex string, - signer crypto.Signer, - batchID0, batchID1 string, - o Options, -) error { - idBytes, err := randomID() - if err != nil { - return err - } - idHex := hex.EncodeToString(idBytes) - - // Create 2 divergent payloads wrapping different CACs - payload1 := []byte("reoffer-precedence-payload-omega-401") - payload2 := []byte("reoffer-precedence-payload-psi-402") - - ch1, err := cac.New(payload1) - if err != nil { - return err - } - ch2, err := cac.New(payload2) - if err != nil { - return err - } - - soc1, err := soc.New(idBytes, ch1).Sign(signer) - if err != nil { - return err - } - soc2, err := soc.New(idBytes, ch2).Sign(signer) - if err != nil { - return err - } - - sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - socAddress := soc1.Address() - - // 1. Upload soc1 under batchID0 to node0 - _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) - if err != nil { - return fmt.Errorf("upload soc1 to node0: %w", err) - } - - // 2. Upload soc2 under batchID1 to node1 - _, err = node1.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID1) - if err != nil { - return fmt.Errorf("upload soc2 to node1: %w", err) - } - - wins, err := divergentChunkWins(soc1, soc2) - if err != nil { - return err - } - var expectedWinnerPayload []byte - if wins { - expectedWinnerPayload = ch2.Data() - } else { - expectedWinnerPayload = ch1.Data() - } - - // Wait for cluster convergence on initial tie-break winner - c.logger.Infof("waiting for initial convergence on address %s...", socAddress.String()) - if err := c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o); err != nil { - return fmt.Errorf("initial convergence failed: %w", err) - } - - // 3. Re-offer soc1 under batchID0 to node0 (re-offering weaker/superseded variant) - c.logger.Infof("re-offering soc1 under batchID0 to node0...") - _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID0) - - // Assert cluster STILL converges on expectedWinnerPayload and does NOT revert - c.logger.Infof("asserting cluster stability after re-offer on address %s...", socAddress.String()) - return c.assertClusterConvergence(ctx, fullNodes, socAddress, expectedWinnerPayload, o) -} - -func (c *Check) assertClusterConvergence( - ctx context.Context, - nodes []*bee.Client, - address swarm.Address, - expectedPayload []byte, - o Options, -) error { - ticker := time.NewTicker(o.SyncRetryInterval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timed out waiting for cluster convergence on %s: %w", address.String(), ctx.Err()) - case <-ticker.C: - allConverged := true - for _, node := range nodes { - retrieved, err := node.DownloadChunk(ctx, address, "", nil) - if err != nil { - allConverged = false - c.logger.Debugf("node %s: download %s error: %v", node.Name(), address.String(), err) - break - } - if !bytes.Contains(retrieved, expectedPayload) && !bytes.Equal(retrieved, expectedPayload) { - allConverged = false - c.logger.Debugf("node %s: data mismatch on %s", node.Name(), address.String()) - break - } - } - - if allConverged { - c.logger.Infof("all %d full nodes converged on address %s with expected payload!", len(nodes), address.String()) - return nil - } - } - } -} - -func randomID() ([]byte, error) { - key := make([]byte, 32) - _, err := rand.Read(key) - if err != nil { - return nil, err - } - return key, nil -} - -// divergentChunkWins reports whether ch2 wins over ch1 by having a -// lexicographically lower wrapped CAC address. -func divergentChunkWins(ch1, ch2 swarm.Chunk) (bool, error) { - s1, err := soc.FromChunk(ch1) - if err != nil { - return false, fmt.Errorf("soc 1 from chunk: %w", err) - } - s2, err := soc.FromChunk(ch2) - if err != nil { - return false, fmt.Errorf("soc 2 from chunk: %w", err) - } - return bytes.Compare(s2.WrappedChunk().Address().Bytes(), s1.WrappedChunk().Address().Bytes()) < 0, nil -} diff --git a/pkg/check/socconvergence/socconvergence_test.go b/pkg/check/socconvergence/socconvergence_test.go deleted file mode 100644 index 2f8bbb2e5..000000000 --- a/pkg/check/socconvergence/socconvergence_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// 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 socconvergence_test - -import ( - "io" - "testing" - "time" - - "github.com/ethersphere/beekeeper/pkg/check/socconvergence" - "github.com/ethersphere/beekeeper/pkg/logging" -) - -func TestNewDefaultOptions(t *testing.T) { - opts := socconvergence.NewDefaultOptions() - if opts.PostageTTL != 24*time.Hour { - t.Fatalf("expected PostageTTL 24h, got %v", opts.PostageTTL) - } - if opts.PostageDepth != 16 { - t.Fatalf("expected PostageDepth 16, got %d", opts.PostageDepth) - } - if opts.RequestTimeout != 5*time.Minute { - t.Fatalf("expected RequestTimeout 5m, got %v", opts.RequestTimeout) - } -} - -func TestNewCheck(t *testing.T) { - chk := socconvergence.NewCheck(logging.New(io.Discard, 0)) - if chk == nil { - t.Fatal("expected non-nil check") - } -} diff --git a/pkg/config/check.go b/pkg/config/check.go index ecdb22ca8..63e5ee3a7 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -34,7 +34,6 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/settlements" "github.com/ethersphere/beekeeper/pkg/check/smoke" "github.com/ethersphere/beekeeper/pkg/check/soc" - "github.com/ethersphere/beekeeper/pkg/check/socconvergence" "github.com/ethersphere/beekeeper/pkg/check/socmatrix" "github.com/ethersphere/beekeeper/pkg/check/stake" "github.com/ethersphere/beekeeper/pkg/check/withdraw" @@ -550,29 +549,6 @@ var Checks = map[string]CheckType{ return opts, nil }, }, - "socconvergence": { - NewAction: socconvergence.NewCheck, - NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { - checkOpts := new(struct { - GasPrice *string `yaml:"gas-price"` - PostageTTL *time.Duration `yaml:"postage-ttl"` - PostageDepth *uint64 `yaml:"postage-depth"` - PostageLabel *string `yaml:"postage-label"` - RequestTimeout *time.Duration `yaml:"request-timeout"` - SyncRetryInterval *time.Duration `yaml:"sync-retry-interval"` - }) - if err := check.Options.Decode(checkOpts); err != nil { - return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) - } - opts := socconvergence.NewDefaultOptions() - - if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { - return nil, fmt.Errorf("applying options: %w", err) - } - - return opts, nil - }, - }, "socmatrix": { NewAction: socmatrix.NewCheck, NewOptions: newSOCMatrixOptions, From 0de48a6b2e7288f9b377e5b2435d5388eb63f417 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:29:11 +0200 Subject: [PATCH 16/21] fix: remove dead code --- config/soc-partition.yaml | 70 --------------- pkg/config/soc_partition_test.go | 76 ---------------- scripts/genpartitionkeys/main.go | 143 ------------------------------- 3 files changed, 289 deletions(-) delete mode 100644 config/soc-partition.yaml delete mode 100644 pkg/config/soc_partition_test.go delete mode 100644 scripts/genpartitionkeys/main.go diff --git a/config/soc-partition.yaml b/config/soc-partition.yaml deleted file mode 100644 index f075faa13..000000000 --- a/config/soc-partition.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# local-soc-partition — 5-node cluster for divergent-SOC / pullsync convergence tests. -# -# Topology (before the cross-group /connect): -# -# bootnode-0 -# │ -# group-a-0 ── group-a-1 group-b-0 ── group-b-1 -# (bootnodes → bootnode) (bootnodes → group-b-0 only) -# ✕ no P2P between A and B -# -# All four full nodes share target-neighborhood=11111100 so their overlays are -# mined into the same Swarm neighborhood. After you POST /connect between the -# groups they form one connected neighborhood and can pullsync. -# -# Create: -# beekeeper create bee-cluster --cluster-name=local-soc-partition -# -# Verify isolation (A must not list B overlays in /peers and vice versa), then -# upload conflicting SOC variants to each group. Heal with e.g.: -# curl -X POST "http://group-a-0.localhost:1633/connect/dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR" -# -# Libp2p keys for group-b were generated with: -# go run ./scripts/genpartitionkeys/ - -clusters: - local-soc-partition: - _inherit: "local" - node-groups: - bootnode: - mode: bootnode - bee-config: bootnode-local-dns - config: local-dns - nodes: - - name: bootnode-0 - bootnodes: /dns4/bootnode-0-headless.%s.svc.cluster.local/tcp/1634/p2p/QmaHzvd3iZduu275CMkMVZKwbsjXSyH3GJRj4UvFJApKcb - libp2p-key: '{"address":"28678fe31f09f722d53e77ca2395569f19959fa5","crypto":{"cipher":"aes-128-ctr","ciphertext":"0ff319684c4f8decf9c998047febe3417cfc45832b8bb62fd818183d54cf5d0183bfa021ed95addce3b33e83ce7ee73e926f00eea8241d96b349266a4d299829d3d22db0d536315b52b34db4a6778bfd3ce7631ad7256ea0bb9c50abea9de35d740b6fdc50caf929b1d19494690d9ed649105d02c14f5ec49d","cipherparams":{"iv":"4e9a50fb5852b5e61964f696be78066b"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"4d513e81647e4150bb648ed8d2dda28d460802336bf24d620119eac66ae0c0c4"},"mac":"9ae71db96e5ddc1c214538d42082212bbbe53aeac09fcc3e3a8eff815648331e"},"version":3,"id":"ae3bc991-d89f-405a-9e6a-60e27347e22d"}' - swarm-key: '{"address":"f176839c150e52fe30e5c2b5c648465c6fdfa532","crypto":{"cipher":"aes-128-ctr","ciphertext":"352af096f0fca9dfbd20a6861bde43d988efe7f179e0a9ffd812a285fdcd63b9","cipherparams":{"iv":"613003f1f1bf93430c92629da33f8828"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"ad1d99a4c64c95c26131e079e8c8a82221d58bf66a7ceb767c33a4c376c564b8"},"mac":"cafda1bc8ca0ffc2b22eb69afd1cf5072fd09412243443be1b0c6832f57924b6"},"version":3}' - group-a: - mode: node - bee-config: bee-soc-partition-group-a - config: local-dns - count: 2 - group-b: - mode: node - bee-config: bee-soc-partition-group-b - config: local-dns - nodes: - # Soft bootnode for the B island. Peer ID must match libp2p-key. - - name: group-b-0 - libp2p-key: '{"address":"dbc4584b50b571e4c426048ca30f3bbf31256664","crypto":{"cipher":"aes-128-ctr","ciphertext":"9e0fb2432abc8d5a591e62e9e79bb028564e6de172b3ad3acacceec01c52bf1c","cipherparams":{"iv":"e6421bbcbaee2afe1cde22cb9c87fe6a"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"e707f2cc3b18e98b4901df78afff7d22c42ee8bd5ffb4aa2692c327a406fda48"},"mac":"14516bd67bc56c8722b5784e9de3844a10eefedfcd2746deacd8eba46e834aa9"},"version":3,"id":"a3f5c9e7-e116-4c9b-b605-f007b0d94589"}' - - name: group-b-1 - libp2p-key: '{"address":"f64dc5d1a2233919f903a5582dd1d5b0b24cd255","crypto":{"cipher":"aes-128-ctr","ciphertext":"69eaf4a5a65992b0ac69f0a73c768cff8507e0d3538629cfbf46ce3cc57d6270","cipherparams":{"iv":"13a5007d75c76d353d7ee05913fc6905"},"kdf":"scrypt","kdfparams":{"n":32768,"r":8,"p":1,"dklen":32,"salt":"69b56b0b71cef94a143b8cea095d8c2ee08a27d53cc99e007e8a5a36fd4f1c08"},"mac":"3514368a10020d238af1c5af42b134d1b179e59e4acbb8f4ded938178cdec8eb"},"version":3,"id":"e66e3d13-db28-455b-a02d-98f3d7a6508f"}' - -bee-configs: - # Joins the main bootnode (empty bootnodes → filled from bootnode group). - bee-soc-partition-group-a: - _inherit: "bee-local-dns" - full-node: true - target-neighborhood: "11111100" - welcome-message: "soc-partition group-a" - - # Isolated from the main bootnode: dials group-b-0 instead (soft bootnode). - # Non-empty bootnodes is required so beekeeper does not inject bootnode-0. - bee-soc-partition-group-b: - _inherit: "bee-local-dns" - full-node: true - bootnode: - - /dns4/group-b-0-headless.local.svc.cluster.local/tcp/1634/p2p/16Uiu2HAmU7b6hrGccvAHRaQg6VRf84cAHbgqcQT1145CozfQU4ZR - target-neighborhood: "11111100" - welcome-message: "soc-partition group-b" diff --git a/pkg/config/soc_partition_test.go b/pkg/config/soc_partition_test.go deleted file mode 100644 index d7472f3f8..000000000 --- a/pkg/config/soc_partition_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package config_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/ethersphere/beekeeper/pkg/config" - "github.com/ethersphere/beekeeper/pkg/logging" -) - -func TestSOCPartitionClusterConfig(t *testing.T) { - t.Parallel() - - dir := filepath.Join("..", "..", "config") - entries, err := os.ReadDir(dir) - if err != nil { - t.Fatal(err) - } - - var files []config.YamlFile - for _, e := range entries { - if e.IsDir() { - continue - } - ext := filepath.Ext(e.Name()) - if ext != ".yaml" && ext != ".yml" { - continue - } - content, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - t.Fatal(err) - } - files = append(files, config.YamlFile{Name: e.Name(), Content: content}) - } - - cfg, err := config.Read(logging.New(os.Stderr, 0), files) - if err != nil { - t.Fatalf("read config: %v", err) - } - - cluster, ok := cfg.Clusters["local-soc-partition"] - if !ok { - t.Fatal("local-soc-partition cluster missing") - } - groups := cluster.GetNodeGroups() - if _, ok := groups["bootnode"]; !ok { - t.Fatal("bootnode group missing") - } - if g, ok := groups["group-a"]; !ok || g.Count != 2 { - t.Fatalf("group-a: %+v", g) - } - if g, ok := groups["group-b"]; !ok || len(g.Nodes) != 2 { - t.Fatalf("group-b: %+v", g) - } - - a, ok := cfg.BeeConfigs["bee-soc-partition-group-a"] - if !ok || a.TargetNeighborhood == nil || *a.TargetNeighborhood != "11111100" { - t.Fatalf("group-a bee config: %+v", a) - } - b, ok := cfg.BeeConfigs["bee-soc-partition-group-b"] - if !ok || b.Bootnodes == nil || len(*b.Bootnodes) == 0 { - t.Fatalf("group-b must have non-empty bootnodes: %+v", b) - } - if b.TargetNeighborhood == nil || *b.TargetNeighborhood != "11111100" { - t.Fatalf("group-b target neighborhood: %+v", b) - } - - exported := b.Export() - if exported.TargetNeighborhood == nil || *exported.TargetNeighborhood != "11111100" { - t.Fatalf("export target neighborhood: %v", exported.TargetNeighborhood) - } - if exported.Bootnodes == nil || len(*exported.Bootnodes) == 0 { - t.Fatal("export bootnodes empty") - } -} diff --git a/scripts/genpartitionkeys/main.go b/scripts/genpartitionkeys/main.go deleted file mode 100644 index ee3d7fc89..000000000 --- a/scripts/genpartitionkeys/main.go +++ /dev/null @@ -1,143 +0,0 @@ -// 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. - -// Command genpartitionkeys generates encrypted libp2p keys and peer IDs for the -// local-soc-partition beekeeper cluster (group-b soft bootnode). -// -// go run ./scripts/genpartitionkeys/ -package main - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/ecdsa" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" - - "github.com/ethersphere/bee/v2/pkg/crypto" - "github.com/google/uuid" - libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - "golang.org/x/crypto/scrypt" -) - -const ( - password = "beekeeper" - scryptN = 1 << 15 - scryptR = 8 - scryptP = 1 - scryptDKLen = 32 -) - -type encryptedKey struct { - Address string `json:"address"` - Crypto struct { - Cipher string `json:"cipher"` - CipherText string `json:"ciphertext"` - CipherParams struct { - IV string `json:"iv"` - } `json:"cipherparams"` - KDF string `json:"kdf"` - KDFParams struct { - N int `json:"n"` - R int `json:"r"` - P int `json:"p"` - DKLen int `json:"dklen"` - Salt string `json:"salt"` - } `json:"kdfparams"` - MAC string `json:"mac"` - } `json:"crypto"` - Version int `json:"version"` - ID string `json:"id"` -} - -func main() { - for _, name := range []string{"group-b-0", "group-b-1"} { - priv, err := crypto.GenerateSecp256k1Key() - if err != nil { - panic(err) - } - keyJSON, err := encryptKey(priv, password) - if err != nil { - panic(err) - } - pid, err := peerID(priv) - if err != nil { - panic(err) - } - fmt.Printf("NAME=%s\nPEER=%s\nKEY=%s\n\n", name, pid, keyJSON) - } -} - -func peerID(priv *ecdsa.PrivateKey) (string, error) { - raw, err := crypto.EncodeSecp256k1PrivateKey(priv) - if err != nil { - return "", err - } - libKey, err := libp2pcrypto.UnmarshalSecp256k1PrivateKey(raw) - if err != nil { - return "", err - } - id, err := peer.IDFromPrivateKey(libKey) - if err != nil { - return "", err - } - return id.String(), nil -} - -func encryptKey(k *ecdsa.PrivateKey, pass string) (string, error) { - data, err := crypto.EncodeSecp256k1PrivateKey(k) - if err != nil { - return "", err - } - salt := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - return "", err - } - derivedKey, err := scrypt.Key([]byte(pass), salt, scryptN, scryptR, scryptP, scryptDKLen) - if err != nil { - return "", err - } - iv := make([]byte, aes.BlockSize) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", err - } - block, err := aes.NewCipher(derivedKey[:16]) - if err != nil { - return "", err - } - cipherText := make([]byte, len(data)) - cipher.NewCTR(block, iv).XORKeyStream(cipherText, data) - mac, err := crypto.LegacyKeccak256(append(derivedKey[16:32], cipherText...)) - if err != nil { - return "", err - } - addr, err := crypto.NewEthereumAddress(k.PublicKey) - if err != nil { - return "", err - } - ek := encryptedKey{ - Address: hex.EncodeToString(addr), - Version: 3, - ID: uuid.NewString(), - } - ek.Crypto.Cipher = "aes-128-ctr" - ek.Crypto.CipherText = hex.EncodeToString(cipherText) - ek.Crypto.CipherParams.IV = hex.EncodeToString(iv) - ek.Crypto.KDF = "scrypt" - ek.Crypto.KDFParams.N = scryptN - ek.Crypto.KDFParams.R = scryptR - ek.Crypto.KDFParams.P = scryptP - ek.Crypto.KDFParams.DKLen = scryptDKLen - ek.Crypto.KDFParams.Salt = hex.EncodeToString(salt) - ek.Crypto.MAC = hex.EncodeToString(mac) - out, err := json.Marshal(ek) - if err != nil { - return "", err - } - return string(out), nil -} From 22888a8a76a85be25acd27e30f1bbf617921b9e6 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:43:22 +0200 Subject: [PATCH 17/21] fix: clean up PR --- pkg/check/socmatrix/socmatrix.go | 4 ---- pkg/config/bee.go | 7 +++++-- pkg/config/cluster.go | 9 ++++++--- pkg/config/config.go | 20 +++++--------------- pkg/config/nodegroup.go | 9 ++++++--- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go index cba33a6e4..458e8ceee 100644 --- a/pkg/check/socmatrix/socmatrix.go +++ b/pkg/check/socmatrix/socmatrix.go @@ -163,10 +163,6 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any c.logger.Infof("socmatrix: scenarios 1-10 completed successfully across %d full nodes", len(fullNodeClients)) } - // ------------------------------------------------------------------------- - // Precision scenarios 11-16 (former chunk-convergence) — same semantics. - // Continue-on-error aggregation matches former chunk-convergence Run(). - // ------------------------------------------------------------------------- n1, n2, err := pickClosestPair(ctx, fullNodeClients) if err != nil { return err diff --git a/pkg/config/bee.go b/pkg/config/bee.go index b8ebf8ab2..ac4c66883 100644 --- a/pkg/config/bee.go +++ b/pkg/config/bee.go @@ -17,12 +17,15 @@ type Inheritable interface { // inheritance (_inherit). Export returns just the embedded Config, so neither // _inherit nor any other config-loading detail can leak into the rendered file. type BeeConfig struct { - Inherit `yaml:",inline"` + *Inherit `yaml:",inline"` orchestration.Config `yaml:",inline"` } func (b BeeConfig) GetParentName() string { - return b.ParentName + if b.Inherit != nil { + return b.ParentName + } + return "" } // Export returns the Bee flag configuration to be rendered into the node's diff --git a/pkg/config/cluster.go b/pkg/config/cluster.go index d56b6adea..6db5d6992 100644 --- a/pkg/config/cluster.go +++ b/pkg/config/cluster.go @@ -9,7 +9,7 @@ import ( // Cluster represents cluster configuration type Cluster struct { // parent to inherit settings from - Inherit `yaml:",inline"` + *Inherit `yaml:",inline"` // Cluster configuration Name *string `yaml:"name,omitempty"` Namespace *string `yaml:"namespace"` @@ -24,7 +24,10 @@ type Cluster struct { } func (b Cluster) GetParentName() string { - return b.ParentName + if b.Inherit != nil { + return b.ParentName + } + return "" } // ClusterNodeGroup represents node group in the cluster @@ -59,7 +62,7 @@ func (c *Cluster) Export() (o orchestration.ClusterOptions) { for i := 0; i < localVal.NumField(); i++ { localField := localVal.Field(i) - if localField.IsValid() && canIsNil(localField) && !localField.IsNil() { + if localField.IsValid() && !localField.IsNil() { localFieldVal := localVal.Field(i).Elem() localFieldName := localType.Field(i).Name diff --git a/pkg/config/config.go b/pkg/config/config.go index a6fe3534d..c3089d41c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -104,14 +104,13 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { // applied per flag instead of all-or-nothing. if mf.Kind() == reflect.Struct { for j := 0; j < mf.NumField(); j++ { - fj, pfj := mf.Field(j), pf.Field(j) - if canIsNil(fj) && fj.IsNil() && canIsNil(pfj) && !pfj.IsNil() { - fj.Set(pfj) + if mf.Field(j).IsNil() && !pf.Field(j).IsNil() { + mf.Field(j).Set(pf.Field(j)) } } continue } - if canIsNil(mf) && mf.IsNil() && canIsNil(pf) && !pf.IsNil() { + if mf.IsNil() && !pf.IsNil() { mf.Set(pf) } } @@ -122,8 +121,8 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { return v, nil } - for k := range configs { - if _, err := mergeParent(k); err != nil { + for name := range configs { + if _, err := mergeParent(name); err != nil { return nil, err } } @@ -131,15 +130,6 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { return mergedConfigs, nil } -func canIsNil(v reflect.Value) bool { - switch v.Kind() { - case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: - return true - default: - return false - } -} - // Read reads given YAML files and unmarshals them into Config func Read(log logging.Logger, yamlFiles []YamlFile) (*Config, error) { c := Config{ diff --git a/pkg/config/nodegroup.go b/pkg/config/nodegroup.go index 8b33a52af..8d0af0e6f 100644 --- a/pkg/config/nodegroup.go +++ b/pkg/config/nodegroup.go @@ -9,7 +9,7 @@ import ( // NodeGroup represents node group configuration type NodeGroup struct { // parent to inherit settings from - Inherit `yaml:",inline"` + *Inherit `yaml:",inline"` // node group configuration Annotations *map[string]string `yaml:"annotations"` Image *string `yaml:"image"` @@ -33,7 +33,10 @@ type NodeGroup struct { } func (b NodeGroup) GetParentName() string { - return b.ParentName + if b.Inherit != nil { + return b.ParentName + } + return "" } // Export exports NodeGroup to orchestration.NodeGroupOptions @@ -44,7 +47,7 @@ func (n *NodeGroup) Export() (o orchestration.NodeGroupOptions) { for i := 0; i < localVal.NumField(); i++ { localField := localVal.Field(i) - if localField.IsValid() && canIsNil(localField) && !localField.IsNil() { + if localField.IsValid() && !localField.IsNil() { localFieldVal := localVal.Field(i).Elem() localFieldName := localType.Field(i).Name From 79a86fc933ce4e61ff22e644bac68d243612e7f2 Mon Sep 17 00:00:00 2001 From: sbackend Date: Sun, 9 Aug 2026 18:44:49 +0200 Subject: [PATCH 18/21] fix: update deps --- go.mod | 3 --- go.sum | 41 ----------------------------------------- 2 files changed, 44 deletions(-) diff --git a/go.mod b/go.mod index a4fbcb723..a608ed8a0 100644 --- a/go.mod +++ b/go.mod @@ -120,7 +120,6 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.1.4 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect @@ -132,9 +131,7 @@ require ( github.com/pion/sctp v1.8.39 // indirect github.com/pion/sdp/v3 v3.0.18 // indirect github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.1.5 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/transport/v4 v4.0.2 // indirect github.com/pion/turn/v4 v4.0.2 // indirect diff --git a/go.sum b/go.sum index b6a41f328..346d965ed 100644 --- a/go.sum +++ b/go.sum @@ -342,7 +342,6 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= @@ -351,7 +350,6 @@ github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= @@ -369,13 +367,10 @@ github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiA github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= @@ -452,8 +447,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -476,7 +469,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -487,7 +479,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= @@ -531,19 +522,13 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -552,12 +537,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -566,8 +545,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -580,37 +557,21 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -619,8 +580,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 5a7a926029d11d3ac0ae69547165686c8d32345c Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 12 Aug 2026 10:19:31 +0300 Subject: [PATCH 19/21] fix(check/socmatrix): use precomputed equal-timestamp stamps for scenarios 1, 3, 4, 9 --- pkg/check/socmatrix/socmatrix.go | 133 ++++++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 10 deletions(-) diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go index 458e8ceee..d687179d0 100644 --- a/pkg/check/socmatrix/socmatrix.go +++ b/pkg/check/socmatrix/socmatrix.go @@ -114,6 +114,9 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) } + // Try setting up batch issuer and signer for precomputed equal-timestamp stamp tests + issuer, batchSigner, _, _ := c.setupIssuerBatch(ctx, cluster, o) + // API-stamp matrix (1-10): continue-on-error so later scenarios still run. var matrixErrs error runMatrix := func(name string, fn func() error) { @@ -127,16 +130,16 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any } runMatrix("Scenario 1: Divergent SOC Tie-Break (Same Stamp)", func() error { - return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, issuer, batchSigner, o) }) runMatrix("Scenario 2: Timestamp Progression (Increasing Timestamp)", func() error { return c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) }) runMatrix("Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence", func() error { - return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, issuer, batchSigner, o) }) runMatrix("Scenario 4: Multi-Stamp Re-Offer Precedence", func() error { - return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, issuer, batchSigner, o) }) runMatrix("Scenario 5: Multi-Batch Sibling Sum Refresh", func() error { return c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) @@ -151,7 +154,7 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any return c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) }) runMatrix("Scenario 9: 3-Payload Sequenced Divergent SOC Chain", func() error { - return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, issuer, batchSigner, o) }) runMatrix("Scenario 10: Multi-Owner Independent SOC Isolation", func() error { return c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, batchID0, o) @@ -226,6 +229,8 @@ func (c *Check) testDivergentSOCTieBreak( ownerHex string, signer crypto.Signer, batchID string, + issuer *bee.Client, + batchSigner crypto.Signer, o Options, ) error { idBytes, err := randomID() @@ -266,8 +271,22 @@ func (c *Check) testDivergentSOCTieBreak( expectedPayload = ch1.Data() } - // Stamp issuer lives only on the batch owner (node0). Upload both payloads - // there; assertClusterConvergence still checks all full nodes after sync. + if issuer != nil && batchSigner != nil { + env, err := issuer.CreateEnvelope(ctx, soc1.Address(), batchID) + if err == nil { + stampBytes, err := env.MarshalBinary() + if err == nil { + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stampBytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stampBytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + + // Fallback to API stamp upload if issuer/batchSigner not configured _, err = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) if err != nil { return fmt.Errorf("upload soc1: %w", err) @@ -338,6 +357,8 @@ func (c *Check) testCrossBatchStampHashPrecedence( ownerHex string, signer crypto.Signer, batchID0, batchID1 string, + issuer *bee.Client, + batchSigner crypto.Signer, o Options, ) error { idBytes, err := randomID() @@ -367,6 +388,40 @@ func (c *Check) testCrossBatchStampHashPrecedence( sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + if issuer != nil && batchSigner != nil { + batch0Bytes, err0 := hex.DecodeString(batchID0) + batch1Bytes, err1 := hex.DecodeString(batchID1) + env0, err2 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID0) + env1, err3 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID1) + if err0 == nil && err1 == nil && err2 == nil && err3 == nil { + ts := binary.BigEndian.Uint64(env0.Timestamp()) + stamp0, errS0 := signStamp(batchSigner, batch0Bytes, env0.Index(), uint64ToBytes(ts), soc1.Address()) + stamp1, errS1 := signStamp(batchSigner, batch1Bytes, env1.Index(), uint64ToBytes(ts), soc1.Address()) + if errS0 == nil && errS1 == nil { + hash0, errH0 := stamp0.Hash() + hash1, errH1 := stamp1.Hash() + if errH0 == nil && errH1 == nil { + var expectedPayload []byte + if bytes.Compare(hash1, hash0) < 0 { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + stamp0Bytes, _ := stamp0.MarshalBinary() + stamp1Bytes, _ := stamp1.MarshalBinary() + + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp0: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stamp1Bytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + } + wins, err := divergentChunkWins(soc1, soc2) if err != nil { return err @@ -397,6 +452,8 @@ func (c *Check) testMultiStampReofferPrecedence( ownerHex string, signer crypto.Signer, batchID0, batchID1 string, + issuer *bee.Client, + batchSigner crypto.Signer, o Options, ) error { idBytes, err := randomID() @@ -426,6 +483,47 @@ func (c *Check) testMultiStampReofferPrecedence( sig1Hex := hex.EncodeToString(soc1.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) + if issuer != nil && batchSigner != nil { + batch0Bytes, err0 := hex.DecodeString(batchID0) + batch1Bytes, err1 := hex.DecodeString(batchID1) + env0, err2 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID0) + env1, err3 := issuer.CreateEnvelope(ctx, soc1.Address(), batchID1) + if err0 == nil && err1 == nil && err2 == nil && err3 == nil { + ts := binary.BigEndian.Uint64(env0.Timestamp()) + stamp0, errS0 := signStamp(batchSigner, batch0Bytes, env0.Index(), uint64ToBytes(ts), soc1.Address()) + stamp1, errS1 := signStamp(batchSigner, batch1Bytes, env1.Index(), uint64ToBytes(ts), soc1.Address()) + if errS0 == nil && errS1 == nil { + hash0, errH0 := stamp0.Hash() + hash1, errH1 := stamp1.Hash() + if errH0 == nil && errH1 == nil { + var expectedPayload []byte + if bytes.Compare(hash1, hash0) < 0 { + expectedPayload = ch2.Data() + } else { + expectedPayload = ch1.Data() + } + + stamp0Bytes, _ := stamp0.MarshalBinary() + stamp1Bytes, _ := stamp1.MarshalBinary() + + _, err = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + if err != nil { + return fmt.Errorf("upload soc1 with stamp0: %w", err) + } + _, _ = node1.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stamp1Bytes) + + if err := c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o); err != nil { + return err + } + + // Re-offer soc1 under batchID0 with stamp0Bytes (same stamp timestamp) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stamp0Bytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + } + wins, err := divergentChunkWins(soc1, soc2) if err != nil { return err @@ -630,6 +728,8 @@ func (c *Check) testSequencedDivergentChain( ownerHex string, signer crypto.Signer, batchID string, + issuer *bee.Client, + batchSigner crypto.Signer, o Options, ) error { idBytes, err := randomID() @@ -654,10 +754,6 @@ func (c *Check) testSequencedDivergentChain( sig2Hex := hex.EncodeToString(soc2.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) sig3Hex := hex.EncodeToString(soc3.Data()[swarm.HashSize : swarm.HashSize+swarm.SocSignatureSize]) - _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) - _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) - _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), batchID) - // Determine winner among all three win12, _ := divergentChunkWins(soc1, soc2) var winner12 swarm.Chunk @@ -683,6 +779,23 @@ func (c *Check) testSequencedDivergentChain( expectedPayload = ch3.Data() } + if issuer != nil { + env, err := issuer.CreateEnvelope(ctx, soc1.Address(), batchID) + if err == nil { + stampBytes, err := env.MarshalBinary() + if err == nil { + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), stampBytes) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), stampBytes) + _, _ = node0.UploadSOCWithStamp(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), stampBytes) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) + } + } + } + + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig1Hex, ch1.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig2Hex, ch2.Data(), batchID) + _, _ = node0.UploadSOC(ctx, ownerHex, idHex, sig3Hex, ch3.Data(), batchID) + return c.assertClusterConvergence(ctx, fullNodes, soc1.Address(), expectedPayload, o) } From c2423a73f3180a9762065a6993a07aede2f4235f Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 14 Aug 2026 17:12:57 +0300 Subject: [PATCH 20/21] fix(check/socmatrix): create batches on issuer for equal-timestamp envelope generation --- pkg/check/socmatrix/socmatrix.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go index d687179d0..a1baac24b 100644 --- a/pkg/check/socmatrix/socmatrix.go +++ b/pkg/check/socmatrix/socmatrix.go @@ -104,18 +104,25 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any return err } - // Create postage batches on node0 and node1 - batchID0, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) - if err != nil { - return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) - } - batchID1, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") - if err != nil { - return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) - } - // Try setting up batch issuer and signer for precomputed equal-timestamp stamp tests - issuer, batchSigner, _, _ := c.setupIssuerBatch(ctx, cluster, o) + issuer, batchSigner, batchID0, _ := c.setupIssuerBatch(ctx, cluster, o) + + var batchID1 string + if issuer != nil { + batchID1, err = issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("issuer %s: alt batch id: %w", issuer.Name(), err) + } + } else { + batchID0, err = node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + batchID1, err = node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + } // API-stamp matrix (1-10): continue-on-error so later scenarios still run. var matrixErrs error From 312c790d26ca486120e3141e1c300b226f9d12b7 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 14 Aug 2026 21:27:04 +0300 Subject: [PATCH 21/21] fix(check/socmatrix): map node upload batches and issuer batches distinctly --- pkg/check/socmatrix/socmatrix.go | 48 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/pkg/check/socmatrix/socmatrix.go b/pkg/check/socmatrix/socmatrix.go index a1baac24b..a0a3d92df 100644 --- a/pkg/check/socmatrix/socmatrix.go +++ b/pkg/check/socmatrix/socmatrix.go @@ -104,24 +104,28 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any return err } - // Try setting up batch issuer and signer for precomputed equal-timestamp stamp tests - issuer, batchSigner, batchID0, _ := c.setupIssuerBatch(ctx, cluster, o) + // Create postage batches on node0 and node1 for standard API upload scenarios (2, 5, 6, 7, 8, 10) + node0BatchID, err := node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) + } + node1BatchID, err := node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + if err != nil { + return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) + } + + // Try setting up batch issuer and signer for precomputed equal-timestamp stamp tests (1, 3, 4, 9) + issuer, batchSigner, issuerBatchID0, _ := c.setupIssuerBatch(ctx, cluster, o) - var batchID1 string + var issuerBatchID1 string if issuer != nil { - batchID1, err = issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") + issuerBatchID1, err = issuer.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-issuer-alt") if err != nil { return fmt.Errorf("issuer %s: alt batch id: %w", issuer.Name(), err) } } else { - batchID0, err = node0.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) - if err != nil { - return fmt.Errorf("node %s: batch id: %w", node0.Name(), err) - } - batchID1, err = node1.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel+"-alt") - if err != nil { - return fmt.Errorf("node %s: alt batch id: %w", node1.Name(), err) - } + issuerBatchID0 = node0BatchID + issuerBatchID1 = node1BatchID } // API-stamp matrix (1-10): continue-on-error so later scenarios still run. @@ -137,34 +141,34 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any } runMatrix("Scenario 1: Divergent SOC Tie-Break (Same Stamp)", func() error { - return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, issuer, batchSigner, o) + return c.testDivergentSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuer, batchSigner, o) }) runMatrix("Scenario 2: Timestamp Progression (Increasing Timestamp)", func() error { - return c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + return c.testTimestampProgression(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) }) runMatrix("Scenario 3: Equal Timestamp Cross-Batch Stamp Hash Precedence", func() error { - return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, issuer, batchSigner, o) + return c.testCrossBatchStampHashPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuerBatchID1, issuer, batchSigner, o) }) runMatrix("Scenario 4: Multi-Stamp Re-Offer Precedence", func() error { - return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, issuer, batchSigner, o) + return c.testMultiStampReofferPrecedence(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuerBatchID1, issuer, batchSigner, o) }) runMatrix("Scenario 5: Multi-Batch Sibling Sum Refresh", func() error { - return c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, batchID1, o) + return c.testMultiBatchSiblingSumRefresh(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, node1BatchID, o) }) runMatrix("Scenario 6: Identical Payload Checksum Matching", func() error { - return c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + return c.testIdenticalChecksumMatching(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) }) runMatrix("Scenario 7: CAC vs CAC Stamp Index Collision", func() error { - return c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, batchID0, o) + return c.testCACIndexCollision(ctx, fullNodeClients, node0, node1, node0BatchID, o) }) runMatrix("Scenario 8: CAC vs SOC Stamp Index Tie-Break", func() error { - return c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, o) + return c.testCACvsSOCTieBreak(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, node0BatchID, o) }) runMatrix("Scenario 9: 3-Payload Sequenced Divergent SOC Chain", func() error { - return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, batchID0, issuer, batchSigner, o) + return c.testSequencedDivergentChain(ctx, fullNodeClients, node0, node1, ownerHex1, signer1, issuerBatchID0, issuer, batchSigner, o) }) runMatrix("Scenario 10: Multi-Owner Independent SOC Isolation", func() error { - return c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, batchID0, o) + return c.testMultiOwnerSOCIsolation(ctx, fullNodeClients, node0, node1, ownerHex1, ownerHex2, signer1, signer2, node0BatchID, o) }) if matrixErrs != nil {