Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pkg/steward/steward.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/ethersphere/bee/v2/pkg/file/redundancy"
"github.com/ethersphere/bee/v2/pkg/postage"
"github.com/ethersphere/bee/v2/pkg/replicas"
"github.com/ethersphere/bee/v2/pkg/retrieval"
"github.com/ethersphere/bee/v2/pkg/storage"
"github.com/ethersphere/bee/v2/pkg/storer"
Expand Down Expand Up @@ -77,6 +78,22 @@ func (s *steward) Reupload(ctx context.Context, root swarm.Address, stamper post
)
}

if rLevel != redundancy.NONE {
rootChunk, err := getter.Get(ctx, root)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispersed replicas are keyed on the 32 byte content address, but root here can be an encrypted reference, which carries the decryption key in its trailing 32 bytes, so it should be trimmed before the lookup.

if err != nil {
return errors.Join(fmt.Errorf("get root chunk for dispersed replicas: %w", err), uploaderSession.Cleanup())
}

stamp, err := stamper.Stamp(rootChunk.Address(), rootChunk.Address())
if err != nil {
return errors.Join(fmt.Errorf("stamping root chunk for dispersed replicas: %w", err), uploaderSession.Cleanup())
}

if err := replicas.NewPutter(uploaderSession, rLevel).Put(ctx, rootChunk.WithStamp(stamp)); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should cehck if rootChank is valid content addressed chunk. Also, the replicas need to be stamped, not the root chunk.

return errors.Join(fmt.Errorf("re-uploading dispersed replicas: %w", err), uploaderSession.Cleanup())
}
}

return uploaderSession.Done(root)
}

Expand Down
43 changes: 34 additions & 9 deletions pkg/steward/steward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"bytes"
"context"
"crypto/rand"
"errors"
"sync"
"sync/atomic"
"testing"
Expand All @@ -17,6 +16,7 @@ import (
"github.com/ethersphere/bee/v2/pkg/file/pipeline/builder"
"github.com/ethersphere/bee/v2/pkg/file/redundancy"
postagetesting "github.com/ethersphere/bee/v2/pkg/postage/mock"
"github.com/ethersphere/bee/v2/pkg/soc"
"github.com/ethersphere/bee/v2/pkg/steward"
"github.com/ethersphere/bee/v2/pkg/storage"
"github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore"
Expand Down Expand Up @@ -63,25 +63,36 @@ func TestSteward(t *testing.T) {
}

chunkCount := int(inmem.count.Load())
replicaCount := redundancy.PARANOID.GetReplicaCount()
wantPushed := chunkCount + replicaCount
done := make(chan struct{})
errc := make(chan error, 1)
replicaAddrs := make(map[string]struct{})
var replicaMu sync.Mutex
go func() {
defer close(done)
count := 0
for op := range store.PusherFeed() {
has, err := chunkStore.Has(ctx, op.Chunk.Address())
if err != nil || !has {
if !has {
err = errors.New("chunk not found")
}
// DirectUpload only forwards pushed chunks over the feed; it does not
// persist them. Persist here so the post-reupload assertions (Has,
// IsRetrievable) observe pushed-but-not-yet-locally-known chunks the
// same way a real pushsync round-trip eventually would.
if err := chunkStore.Put(ctx, op.Chunk); err != nil {
select {
case errc <- err:
default:
}
return
}

if sch, err := soc.FromChunk(op.Chunk); err == nil && bytes.Equal(sch.OwnerAddress(), swarm.ReplicasOwner) {
replicaMu.Lock()
replicaAddrs[op.Chunk.Address().String()] = struct{}{}
replicaMu.Unlock()
}

count++
if count == chunkCount {
if count == wantPushed {
return
}
}
Expand Down Expand Up @@ -113,8 +124,22 @@ func TestSteward(t *testing.T) {
}

count := len(localRetrieval.retrievedChunks)
if count != chunkCount {
t.Fatalf("unexpected no of unique chunks retrieved: want %d have %d", chunkCount, count)
// IsRetrievable's root-chunk fetch goes through joiner -> replicas.NewGetter, which
// races the original root address against an initial batch of 2 replica candidate
// addresses before the first success cancels the rest (see replicas/getter.go). With
// real dispersed replicas now present (this is what this fix creates), up to 2 of
// those speculative replica fetches can also succeed and get recorded before
// cancellation lands, on top of the trie chunks retrieved by traversal.
const maxSpeculativeRootFetches = 2
if count < chunkCount || count > chunkCount+maxSpeculativeRootFetches {
t.Fatalf("unexpected no of unique chunks retrieved: want between %d and %d, have %d", chunkCount, chunkCount+maxSpeculativeRootFetches, count)
}

replicaMu.Lock()
gotReplicas := len(replicaAddrs)
replicaMu.Unlock()
if gotReplicas != replicaCount {
t.Fatalf("unexpected no of dispersed replicas re-uploaded: want %d have %d", replicaCount, gotReplicas)
}
}

Expand Down
Loading