Commit 1de167ea authored by Rodrigo Q. Saramago's avatar Rodrigo Q. Saramago Committed by GitHub

Soc package refactor (#1394)

parent 99aaaa79
......@@ -84,7 +84,6 @@ func TestBzz(t *testing.T) {
}
fileMetadataReference, err := pipeWriteAll(bytes.NewReader(fileMetadataBytes), int64(len(fileMetadataBytes)))
if err != nil {
t.Fatal(err)
}
......@@ -171,7 +170,6 @@ func TestBzz(t *testing.T) {
}),
)
})
}
func TestFeedIndirection(t *testing.T) {
......@@ -219,7 +217,7 @@ func TestFeedIndirection(t *testing.T) {
// called from the bzz endpoint. then call the bzz endpoint with
// the pregenerated feed root manifest hash
feedUpdate, _ := toChunk(121212, resp.Reference.Bytes())
feedUpdate := toChunk(t, 121212, resp.Reference.Bytes())
var (
feedChunkAddr = swarm.MustParseHexAddress("891a1d1c8436c792d02fc2e8883fef7ab387eaeaacd25aa9f518be7be7856d54")
......
......@@ -29,9 +29,7 @@ const (
feedMetadataEntryType = "swarm-feed-type"
)
var (
errInvalidFeedUpdate = errors.New("invalid feed update")
)
var errInvalidFeedUpdate = errors.New("invalid feed update")
type feedReferenceResponse struct {
Reference swarm.Address `json:"reference"`
......@@ -176,12 +174,12 @@ func (s *server) feedPostHandler(w http.ResponseWriter, r *http.Request) {
}
func parseFeedUpdate(ch swarm.Chunk) (swarm.Address, int64, error) {
sch, err := soc.FromChunk(ch)
s, err := soc.FromChunk(ch)
if err != nil {
return swarm.ZeroAddress, 0, fmt.Errorf("soc unmarshal: %w", err)
}
update := sch.Chunk.Data()
update := s.WrappedChunk().Data()
// split the timestamp and reference
// possible values right now:
// unencrypted ref: span+timestamp+ref => 8+8+32=48
......
......@@ -16,15 +16,13 @@ import (
"testing"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/cac"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/feeds"
"github.com/ethersphere/bee/pkg/file/loadsave"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/manifest"
"github.com/ethersphere/bee/pkg/soc"
testingsoc "github.com/ethersphere/bee/pkg/soc/testing"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/bee/pkg/storage/mock"
......@@ -84,7 +82,7 @@ func TestFeed_Get(t *testing.T) {
t.Run("with at", func(t *testing.T) {
var (
timestamp = int64(12121212)
ch, _ = toChunk(uint64(timestamp), expReference.Bytes())
ch = toChunk(t, uint64(timestamp), expReference.Bytes())
look = newMockLookup(12, 0, ch, nil, &id{}, &id{})
factory = newMockFactory(look)
idBytes, _ = (&id{}).MarshalBinary()
......@@ -115,7 +113,7 @@ func TestFeed_Get(t *testing.T) {
t.Run("latest", func(t *testing.T) {
var (
timestamp = int64(12121212)
ch, _ = toChunk(uint64(timestamp), expReference.Bytes())
ch = toChunk(t, uint64(timestamp), expReference.Bytes())
look = newMockLookup(-1, 2, ch, nil, &id{}, &id{})
factory = newMockFactory(look)
idBytes, _ = (&id{}).MarshalBinary()
......@@ -237,36 +235,16 @@ func (l *mockLookup) At(_ context.Context, at, after int64) (swarm.Chunk, feeds.
return nil, nil, nil, errors.New("no feed update found")
}
func toChunk(at uint64, payload []byte) (swarm.Chunk, error) {
func toChunk(t *testing.T, at uint64, payload []byte) swarm.Chunk {
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, at)
content := append(ts, payload...)
ch, err := cac.New(content)
if err != nil {
return nil, err
}
id := make([]byte, soc.IdSize)
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
return nil, err
}
signer := crypto.NewDefaultSigner(privKey)
sch := soc.New(id, ch)
if err != nil {
return nil, err
}
err = sch.AddSigner(signer)
if err != nil {
return nil, err
}
return sch.ToChunk()
s := testingsoc.GenerateMockSOC(t, content)
return s.Chunk()
}
type id struct {
}
type id struct{}
func (i *id) MarshalBinary() ([]byte, error) {
return []byte("accd"), nil
......
......@@ -88,7 +88,15 @@ func (s *server) socUploadHandler(w http.ResponseWriter, r *http.Request) {
return
}
chunk, err := soc.NewSignedChunk(id, ch, owner, sig)
ss, err := soc.NewSigned(id, ch, owner, sig)
if err != nil {
s.logger.Debugf("soc upload: address soc error: %v", err)
s.logger.Error("soc upload: address soc error")
jsonhttp.Unauthorized(w, "invalid address")
return
}
sch, err := ss.Chunk()
if err != nil {
s.logger.Debugf("soc upload: read chunk data error: %v", err)
s.logger.Error("soc upload: read chunk data error")
......@@ -96,7 +104,7 @@ func (s *server) socUploadHandler(w http.ResponseWriter, r *http.Request) {
return
}
if !soc.Valid(chunk) {
if !soc.Valid(sch) {
s.logger.Debugf("soc upload: invalid chunk: %v", err)
s.logger.Error("soc upload: invalid chunk")
jsonhttp.Unauthorized(w, "invalid chunk")
......@@ -106,7 +114,7 @@ func (s *server) socUploadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
has, err := s.storer.Has(ctx, chunk.Address())
has, err := s.storer.Has(ctx, sch.Address())
if err != nil {
s.logger.Debugf("soc upload: store has: %v", err)
s.logger.Error("soc upload: store has")
......@@ -119,7 +127,7 @@ func (s *server) socUploadHandler(w http.ResponseWriter, r *http.Request) {
return
}
_, err = s.storer.Put(ctx, requestModePut(r), chunk)
_, err = s.storer.Put(ctx, requestModePut(r), sch)
if err != nil {
s.logger.Debugf("soc upload: chunk write error: %v", err)
s.logger.Error("soc upload: chunk write error")
......@@ -127,5 +135,5 @@ func (s *server) socUploadHandler(w http.ResponseWriter, r *http.Request) {
return
}
jsonhttp.Created(w, chunkAddressResponse{Reference: chunk.Address()})
jsonhttp.Created(w, chunkAddressResponse{Reference: sch.Address()})
}
......@@ -6,7 +6,6 @@ package api_test
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io/ioutil"
......@@ -14,19 +13,19 @@ import (
"testing"
"github.com/ethersphere/bee/pkg/api"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/soc"
testingsoc "github.com/ethersphere/bee/pkg/soc/testing"
statestore "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/storage/mock"
"github.com/ethersphere/bee/pkg/swarm"
"github.com/ethersphere/bee/pkg/tags"
)
func TestSoc(t *testing.T) {
func TestSOC(t *testing.T) {
var (
testData = []byte("foo")
socResource = func(owner, id, sig string) string { return fmt.Sprintf("/soc/%s/%s?sig=%s", owner, id, sig) }
mockStatestore = statestore.NewStateStore()
logger = logging.New(ioutil.Discard, 0)
......@@ -37,7 +36,6 @@ func TestSoc(t *testing.T) {
Tags: tag,
})
)
t.Run("cmpty data", func(t *testing.T) {
jsonhttptest.Request(t, client, http.MethodPost, socResource("8d3766440f0d7b949a5e32995d09619a7f86e632", "bb", "cc"), http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
......@@ -75,16 +73,16 @@ func TestSoc(t *testing.T) {
})
t.Run("signature invalid", func(t *testing.T) {
s, owner, payload := mockSoc(t)
id := make([]byte, soc.IdSize)
s := testingsoc.GenerateMockSOC(t, testData)
// modify the sign
sig := s.Signature()
sig := make([]byte, soc.SignatureSize)
copy(sig, s.Signature)
sig[12] = 0x98
sig[10] = 0x12
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(owner), hex.EncodeToString(id), hex.EncodeToString(sig)), http.StatusUnauthorized,
jsonhttptest.WithRequestBody(bytes.NewReader(payload)),
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(s.Owner), hex.EncodeToString(s.ID), hex.EncodeToString(sig)), http.StatusUnauthorized,
jsonhttptest.WithRequestBody(bytes.NewReader(s.WrappedChunk.Data())),
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: "invalid chunk",
Code: http.StatusUnauthorized,
......@@ -93,55 +91,39 @@ func TestSoc(t *testing.T) {
})
t.Run("ok", func(t *testing.T) {
s, owner, payload := mockSoc(t)
id := make([]byte, soc.IdSize)
s := testingsoc.GenerateMockSOC(t, testData)
sig := s.Signature()
addr, err := s.Address()
if err != nil {
t.Fatal(err)
}
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(owner), hex.EncodeToString(id), hex.EncodeToString(sig)), http.StatusCreated,
jsonhttptest.WithRequestBody(bytes.NewReader(payload)),
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(s.Owner), hex.EncodeToString(s.ID), hex.EncodeToString(s.Signature)), http.StatusCreated,
jsonhttptest.WithRequestBody(bytes.NewReader(s.WrappedChunk.Data())),
jsonhttptest.WithExpectedJSONResponse(api.SocPostResponse{
Reference: addr,
Reference: s.Address(),
}),
)
// try to fetch the same chunk
rsrc := fmt.Sprintf("/chunks/" + addr.String())
rsrc := fmt.Sprintf("/chunks/" + s.Address().String())
resp := request(t, client, http.MethodGet, rsrc, nil, http.StatusOK)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
ch, err := s.ToChunk()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(ch.Data(), data) {
t.Fatal("data retrieved doesnt match uploaded content")
if !bytes.Equal(s.Chunk().Data(), data) {
t.Fatal("data retrieved doesn't match uploaded content")
}
})
t.Run("already exists", func(t *testing.T) {
s, owner, payload := mockSoc(t)
id := make([]byte, soc.IdSize)
s := testingsoc.GenerateMockSOC(t, testData)
sig := s.Signature()
addr, err := s.Address()
if err != nil {
t.Fatal(err)
}
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(owner), hex.EncodeToString(id), hex.EncodeToString(sig)), http.StatusCreated,
jsonhttptest.WithRequestBody(bytes.NewReader(payload)),
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(s.Owner), hex.EncodeToString(s.ID), hex.EncodeToString(s.Signature)), http.StatusCreated,
jsonhttptest.WithRequestBody(bytes.NewReader(s.WrappedChunk.Data())),
jsonhttptest.WithExpectedJSONResponse(api.SocPostResponse{
Reference: addr,
Reference: s.Address(),
}),
)
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(owner), hex.EncodeToString(id), hex.EncodeToString(sig)), http.StatusConflict,
jsonhttptest.WithRequestBody(bytes.NewReader(payload)),
jsonhttptest.Request(t, client, http.MethodPost, socResource(hex.EncodeToString(s.Owner), hex.EncodeToString(s.ID), hex.EncodeToString(s.Signature)), http.StatusConflict,
jsonhttptest.WithRequestBody(bytes.NewReader(s.WrappedChunk.Data())),
jsonhttptest.WithExpectedJSONResponse(
jsonhttp.StatusResponse{
Message: "chunk already exists",
......@@ -149,38 +131,4 @@ func TestSoc(t *testing.T) {
}),
)
})
}
// returns a valid, mocked SOC
func mockSoc(t *testing.T) (*soc.Soc, []byte, []byte) {
// create a valid soc
id := make([]byte, soc.IdSize)
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
foo := "foo"
fooLength := len(foo)
fooBytes := make([]byte, 8+fooLength)
binary.LittleEndian.PutUint64(fooBytes, uint64(fooLength))
copy(fooBytes[8:], foo)
ch := swarm.NewChunk(address, fooBytes)
sch := soc.New(id, ch)
if err != nil {
t.Fatal(err)
}
err = sch.AddSigner(signer)
if err != nil {
t.Fatal(err)
}
_, _ = sch.ToChunk()
return sch, sch.OwnerAddress(), ch.Data()
}
......@@ -112,10 +112,16 @@ func NewUpdate(f *Feed, idx Index, timestamp int64, payload []byte, sig []byte)
return nil, fmt.Errorf("toChunk: %w", err)
}
ch, err := soc.NewSignedChunk(id, cac, f.Owner.Bytes(), sig)
ss, err := soc.NewSigned(id, cac, f.Owner.Bytes(), sig)
if err != nil {
return nil, fmt.Errorf("new signed soc: %w", err)
}
ch, err := ss.Chunk()
if err != nil {
return nil, fmt.Errorf("new chunk: %w", err)
}
if !soc.Valid(ch) {
return nil, storage.ErrInvalidChunk
}
......@@ -135,7 +141,6 @@ func Id(topic []byte, index Index) ([]byte, error) {
}
i := &id{topic, indexBytes}
return i.MarshalBinary()
}
// Address calculates the soc address of a feed update
......@@ -145,9 +150,5 @@ func (u *Update) Address() (swarm.Address, error) {
if err != nil {
return addr, err
}
owner, err := soc.NewOwner(u.Owner[:])
if err != nil {
return addr, err
}
return soc.CreateAddress(i, owner)
return soc.CreateAddress(i, u.Owner[:])
}
......@@ -55,7 +55,7 @@ func FromChunk(ch swarm.Chunk) (uint64, []byte, error) {
if err != nil {
return 0, nil, err
}
cac := s.Chunk
cac := s.WrappedChunk()
if len(cac.Data()) < 16 {
return 0, nil, fmt.Errorf("feed update payload too short")
}
......
......@@ -48,7 +48,8 @@ func (u *Putter) Put(ctx context.Context, i Index, at int64, payload []byte) err
if err != nil {
return err
}
ch, err := soc.NewChunk(id, cac, u.signer)
s := soc.New(id, cac)
ch, err := s.Sign(u.signer)
if err != nil {
return err
}
......
......@@ -5,6 +5,22 @@
package soc
var (
ToSignDigest = toSignDigest
ErrInvalidAddress = errInvalidAddress
Hash = hash
RecoverAddress = recoverAddress
)
// Signature returns the SOC signature.
func (s *SOC) Signature() []byte {
return s.signature
}
// OwnerAddress returns the ethereum address of the SOC owner.
func (s *SOC) OwnerAddress() []byte {
return s.owner
}
// ID returns the SOC id.
func (s *SOC) ID() []byte {
return s.id
}
......@@ -2,14 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package soc provides the single-owner chunk implemenation
// Package soc provides the single-owner chunk implementation
// and validator.
package soc
import (
"bytes"
"errors"
"fmt"
"github.com/ethersphere/bee/pkg/cac"
"github.com/ethersphere/bee/pkg/crypto"
......@@ -22,130 +21,118 @@ const (
minChunkSize = IdSize + SignatureSize + swarm.SpanSize
)
// Id is a soc identifier
type Id []byte
// Owner is a wrapper that enforces valid length address of soc owner.
type Owner struct {
address []byte
}
var (
errInvalidAddress = errors.New("soc: invalid address")
errWrongChunkSize = errors.New("soc: chunk length is less than minimum")
)
// NewOwner creates a new Owner.
func NewOwner(address []byte) (*Owner, error) {
if len(address) != crypto.AddressSize {
return nil, fmt.Errorf("invalid address %x", address)
}
return &Owner{
address: address,
}, nil
}
// ID is a SOC identifier
type ID []byte
// Soc wraps a single soc.
type Soc struct {
id Id
// SOC wraps a content-addressed chunk.
type SOC struct {
id ID
owner []byte // owner is the address in bytes of SOC owner.
signature []byte
signer crypto.Signer
owner *Owner
Chunk swarm.Chunk
chunk swarm.Chunk // wrapped chunk.
}
// NewChunk is a convenience function to create a single-owner chunk ready to be sent
// on the network.
func NewChunk(id Id, ch swarm.Chunk, signer crypto.Signer) (swarm.Chunk, error) {
s := New(id, ch)
err := s.AddSigner(signer)
if err != nil {
return nil, err
// New creates a new SOC representation from arbitrary id and
// a content-addressed chunk.
func New(id ID, ch swarm.Chunk) *SOC {
return &SOC{
id: id,
chunk: ch,
}
return s.ToChunk()
}
// NewChunk is a convenience function to create a single-owner chunk ready to be sent
// on the network.
func NewSignedChunk(id Id, ch swarm.Chunk, owner, sig []byte) (swarm.Chunk, error) {
// NewSigned creates a single-owner chunk based on already signed data.
func NewSigned(id ID, ch swarm.Chunk, owner, sig []byte) (*SOC, error) {
s := New(id, ch)
if len(owner) != crypto.AddressSize {
return nil, errInvalidAddress
}
s.owner = owner
s.signature = sig
o, err := NewOwner(owner)
if err != nil {
return nil, err
return s, nil
}
// address returns the SOC chunk address.
func (s *SOC) address() (swarm.Address, error) {
if len(s.owner) != crypto.AddressSize {
return swarm.ZeroAddress, errInvalidAddress
}
s.owner = o
return CreateAddress(s.id, s.owner)
}
// WrappedChunk returns the chunk wrapped by the SOC.
func (s *SOC) WrappedChunk() swarm.Chunk {
return s.chunk
}
// create chunk
socAddress, err := s.Address()
// Chunk returns the SOC chunk.
func (s *SOC) Chunk() (swarm.Chunk, error) {
socAddress, err := s.address()
if err != nil {
return nil, err
}
return swarm.NewChunk(socAddress, s.toBytes()), nil
}
// New creates a new Soc representation from arbitrary soc id and
// a content-addressed chunk.
//
// By default the span of the soc data is set to the length
// of the payload.
func New(id Id, ch swarm.Chunk) *Soc {
return &Soc{
id: id,
Chunk: ch,
}
}
// WithOwnerAddress provides the possibility of setting the ethereum
// address for the owner of an soc in the absence of a signer.
func (s *Soc) WithOwnerAddress(ownerAddress *Owner) *Soc {
s.owner = ownerAddress
return s
// toBytes is a helper function to convert the SOC data to bytes.
func (s *SOC) toBytes() []byte {
buf := bytes.NewBuffer(nil)
buf.Write(s.id)
buf.Write(s.signature)
buf.Write(s.chunk.Data())
return buf.Bytes()
}
// AddSigner currently sets a single signer for the soc.
//
// This method will overwrite any value set with WithOwnerAddress with
// the address derived from the given signer.
func (s *Soc) AddSigner(signer crypto.Signer) error {
// Sign signs a SOC using the given signer.
// It returns a signed SOC chunk ready for submission to the network.
func (s *SOC) Sign(signer crypto.Signer) (swarm.Chunk, error) {
// create owner
publicKey, err := signer.PublicKey()
if err != nil {
return err
return nil, err
}
ownerAddressBytes, err := crypto.NewEthereumAddress(*publicKey)
if err != nil {
return err
return nil, err
}
ownerAddress, err := NewOwner(ownerAddressBytes)
if err != nil {
return err
if len(ownerAddressBytes) != crypto.AddressSize {
return nil, errInvalidAddress
}
s.signer = signer
s.owner = ownerAddress
return nil
}
s.owner = ownerAddressBytes
// OwnerAddress returns the ethereum address of the signer of the Chunk.
func (s *Soc) OwnerAddress() []byte {
return s.owner.address
}
// generate the data to sign
toSignBytes, err := hash(s.id, s.chunk.Address().Bytes())
if err != nil {
return nil, err
}
// Address returns the soc Chunk address.
func (s *Soc) Address() (swarm.Address, error) {
return CreateAddress(s.id, s.owner)
}
// sign the chunk
signature, err := signer.Sign(toSignBytes)
if err != nil {
return nil, err
}
s.signature = signature
func (s *Soc) Signature() []byte {
return s.signature
return s.Chunk()
}
// FromChunk recreates an Soc representation from swarm.Chunk data.
func FromChunk(sch swarm.Chunk) (*Soc, error) {
// FromChunk recreates a SOC representation from swarm.Chunk data.
func FromChunk(sch swarm.Chunk) (*SOC, error) {
chunkData := sch.Data()
if len(chunkData) < minChunkSize {
return nil, errors.New("less than minimum length")
return nil, errWrongChunkSize
}
// add all the data fields
s := &Soc{}
// add all the data fields to the SOC
s := &SOC{}
cursor := 0
s.id = chunkData[cursor : cursor+IdSize]
s.id = chunkData[cursor:IdSize]
cursor += IdSize
s.signature = chunkData[cursor : cursor+SignatureSize]
......@@ -156,94 +143,48 @@ func FromChunk(sch swarm.Chunk) (*Soc, error) {
return nil, err
}
toSignBytes, err := toSignDigest(s.id, ch.Address().Bytes())
toSignBytes, err := hash(s.id, ch.Address().Bytes())
if err != nil {
return nil, err
}
// recover owner information
recoveredEthereumAddress, err := recoverAddress(s.signature, toSignBytes)
recoveredOwnerAddress, err := recoverAddress(s.signature, toSignBytes)
if err != nil {
return nil, err
}
owner, err := NewOwner(recoveredEthereumAddress)
if err != nil {
return nil, err
if len(recoveredOwnerAddress) != crypto.AddressSize {
return nil, errInvalidAddress
}
s.owner = owner
s.Chunk = ch
s.owner = recoveredOwnerAddress
s.chunk = ch
return s, nil
}
// ToChunk generates a signed chunk payload ready for submission to the swarm network.
//
// The method will fail if no signer has been added.
func (s *Soc) ToChunk() (swarm.Chunk, error) {
var err error
if s.signer == nil {
return nil, errors.New("signer missing")
}
// generate the data to sign
toSignBytes, err := toSignDigest(s.id, s.Chunk.Address().Bytes())
// CreateAddress creates a new SOC address from the id and
// the ethereum address of the owner.
func CreateAddress(id ID, owner []byte) (swarm.Address, error) {
sum, err := hash(id, owner)
if err != nil {
return nil, err
}
// sign the chunk
signature, err := s.signer.Sign(toSignBytes)
if err != nil {
return nil, err
}
s.signature = signature
// create chunk
socAddress, err := s.Address()
if err != nil {
return nil, err
return swarm.ZeroAddress, err
}
return swarm.NewChunk(socAddress, s.toBytes()), nil
}
func (s *Soc) toBytes() []byte {
buf := bytes.NewBuffer(nil)
buf.Write(s.id)
buf.Write(s.signature)
buf.Write(s.Chunk.Data())
return buf.Bytes()
return swarm.NewAddress(sum), nil
}
// toSignDigest creates a digest suitable for signing to represent the soc.
func toSignDigest(id Id, sum []byte) ([]byte, error) {
// hash hashes the given values in order.
func hash(values ...[]byte) ([]byte, error) {
h := swarm.NewHasher()
_, err := h.Write(id)
for _, v := range values {
_, err := h.Write(v)
if err != nil {
return nil, err
}
_, err = h.Write(sum)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// CreateAddress creates a new soc address from the soc id and the ethereum address of the signer
func CreateAddress(id Id, owner *Owner) (swarm.Address, error) {
h := swarm.NewHasher()
_, err := h.Write(id)
if err != nil {
return swarm.ZeroAddress, err
}
_, err = h.Write(owner.address)
if err != nil {
return swarm.ZeroAddress, err
}
sum := h.Sum(nil)
return swarm.NewAddress(sum), nil
}
// recoverOwner returns the ethereum address of the owner of an soc.
// recoverAddress returns the ethereum address of the owner of a SOC.
func recoverAddress(signature, digest []byte) ([]byte, error) {
recoveredPublicKey, err := crypto.Recover(signature, digest)
if err != nil {
......
......@@ -7,118 +7,315 @@ package soc_test
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/pkg/cac"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/swarm"
)
// TestToChunk verifies that the chunk create from the Soc object
// corresponds to the soc spec.
func TestToChunk(t *testing.T) {
privKey, err := crypto.GenerateSecp256k1Key()
func TestNew(t *testing.T) {
payload := []byte("foo")
ch, err := cac.New(payload)
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
id := make([]byte, 32)
id := make([]byte, soc.IdSize)
s := soc.New(id, ch)
// check SOC fields
if !bytes.Equal(s.ID(), id) {
t.Fatalf("id mismatch. got %x want %x", s.ID(), id)
}
chunkData := s.WrappedChunk().Data()
spanBytes := make([]byte, swarm.SpanSize)
binary.LittleEndian.PutUint64(spanBytes, uint64(len(payload)))
if !bytes.Equal(chunkData[:swarm.SpanSize], spanBytes) {
t.Fatalf("span mismatch. got %x want %x", chunkData[:swarm.SpanSize], spanBytes)
}
if !bytes.Equal(chunkData[swarm.SpanSize:], payload) {
t.Fatalf("payload mismatch. got %x want %x", chunkData[swarm.SpanSize:], payload)
}
}
func TestNewSigned(t *testing.T) {
owner := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
// signature of hash(id + chunk address of foo)
sig, err := hex.DecodeString("5acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a053deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c1348311b")
if err != nil {
t.Fatal(err)
}
payload := []byte("foo")
ch, err := cac.New(payload)
if err != nil {
t.Fatal(err)
}
sch, err := soc.NewChunk(id, ch, signer)
id := make([]byte, soc.IdSize)
s, err := soc.NewSigned(id, ch, owner.Bytes(), sig)
if err != nil {
t.Fatal(err)
}
chunkData := sch.Data()
// verify that id, signature, payload is in place
// check signed SOC fields
if !bytes.Equal(s.ID(), id) {
t.Fatalf("id mismatch. got %x want %x", s.ID(), id)
}
if !bytes.Equal(s.OwnerAddress(), owner.Bytes()) {
t.Fatalf("owner mismatch. got %x want %x", s.OwnerAddress(), owner.Bytes())
}
if !bytes.Equal(s.Signature(), sig) {
t.Fatalf("signature mismatch. got %x want %x", s.Signature(), sig)
}
chunkData := s.WrappedChunk().Data()
spanBytes := make([]byte, swarm.SpanSize)
binary.LittleEndian.PutUint64(spanBytes, uint64(len(payload)))
if !bytes.Equal(chunkData[:swarm.SpanSize], spanBytes) {
t.Fatalf("span mismatch. got %x want %x", chunkData[:swarm.SpanSize], spanBytes)
}
if !bytes.Equal(chunkData[swarm.SpanSize:], payload) {
t.Fatalf("payload mismatch. got %x want %x", chunkData[swarm.SpanSize:], payload)
}
}
// TestChunk verifies that the chunk created from the SOC object
// corresponds to the SOC spec.
func TestChunk(t *testing.T) {
owner := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
sig, err := hex.DecodeString("5acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a053deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c1348311b")
if err != nil {
t.Fatal(err)
}
payload := []byte("foo")
ch, err := cac.New(payload)
if err != nil {
t.Fatal(err)
}
id := make([]byte, soc.IdSize)
// creates a new signed SOC
s, err := soc.NewSigned(id, ch, owner.Bytes(), sig)
if err != nil {
t.Fatal(err)
}
sum, err := soc.Hash(id, owner.Bytes())
if err != nil {
t.Fatal(err)
}
expectedSOCAddress := swarm.NewAddress(sum)
// creates SOC chunk
sch, err := s.Chunk()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(sch.Address().Bytes(), expectedSOCAddress.Bytes()) {
t.Fatalf("soc address mismatch. got %x want %x", sch.Address().Bytes(), expectedSOCAddress.Bytes())
}
chunkData := sch.Data()
// verifies that id, signature, payload is in place in the SOC chunk
cursor := 0
if !bytes.Equal(chunkData[cursor:cursor+soc.IdSize], id) {
t.Fatal("id mismatch")
if !bytes.Equal(chunkData[cursor:soc.IdSize], id) {
t.Fatalf("id mismatch. got %x want %x", chunkData[cursor:soc.IdSize], id)
}
cursor += soc.IdSize
signature := chunkData[cursor : cursor+soc.SignatureSize]
if !bytes.Equal(signature, sig) {
t.Fatalf("signature mismatch. got %x want %x", signature, sig)
}
cursor += soc.SignatureSize
spanBytes := make([]byte, 8)
spanBytes := make([]byte, swarm.SpanSize)
binary.LittleEndian.PutUint64(spanBytes, uint64(len(payload)))
if !bytes.Equal(chunkData[cursor:cursor+swarm.SpanSize], spanBytes) {
t.Fatal("span mismatch")
t.Fatalf("span mismatch. got %x want %x", chunkData[cursor:cursor+swarm.SpanSize], spanBytes)
}
cursor += swarm.SpanSize
if !bytes.Equal(chunkData[cursor:], payload) {
t.Fatal("payload mismatch")
t.Fatalf("payload mismatch. got %x want %x", chunkData[cursor:], payload)
}
}
func TestChunkErrorWithoutOwner(t *testing.T) {
payload := []byte("foo")
ch, err := cac.New(payload)
if err != nil {
t.Fatal(err)
}
id := make([]byte, soc.IdSize)
// creates a new soc
s := soc.New(id, ch)
_, err = s.Chunk()
if !errors.Is(err, soc.ErrInvalidAddress) {
t.Fatalf("expect error. got `%v` want `%v`", err, soc.ErrInvalidAddress)
}
}
// TestSign tests whether a soc is correctly signed.
func TestSign(t *testing.T) {
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
// get the public key of the signer that was used
payload := []byte("foo")
ch, err := cac.New(payload)
if err != nil {
t.Fatal(err)
}
id := make([]byte, soc.IdSize)
// creates the soc
s := soc.New(id, ch)
// signs the chunk
sch, err := s.Sign(signer)
if err != nil {
t.Fatal(err)
}
chunkData := sch.Data()
// get signature in the chunk
cursor := soc.IdSize
signature := chunkData[cursor : cursor+soc.SignatureSize]
// get the public key of the signer
publicKey, err := signer.PublicKey()
if err != nil {
t.Fatal(err)
}
ethereumAddress, err := crypto.NewEthereumAddress(*publicKey)
owner, err := crypto.NewEthereumAddress(*publicKey)
if err != nil {
t.Fatal(err)
}
toSignBytes, err := soc.ToSignDigest(id, ch.Address().Bytes())
toSignBytes, err := soc.Hash(id, ch.Address().Bytes())
if err != nil {
t.Fatal(err)
}
// verify owner match
recoveredEthereumAddress, err := soc.RecoverAddress(signature, toSignBytes)
// verifies if the owner matches
recoveredOwner, err := soc.RecoverAddress(signature, toSignBytes)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(recoveredEthereumAddress, ethereumAddress) {
t.Fatalf("address mismatch %x %x", recoveredEthereumAddress, ethereumAddress)
if !bytes.Equal(recoveredOwner, owner) {
t.Fatalf("owner address mismatch. got %x want %x", recoveredOwner, owner)
}
}
// TestFromChunk verifies that valid chunk data deserializes to
// a fully populated Chunk object.
// a fully populated soc object.
func TestFromChunk(t *testing.T) {
privKey, err := crypto.GenerateSecp256k1Key()
socAddress := swarm.MustParseHexAddress("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85")
// signed soc chunk of:
// id: 0
// wrapped chunk of: `foo`
// owner: 0x8d3766440f0d7b949a5e32995d09619a7f86e632
sch := swarm.NewChunk(socAddress, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 205, 56, 79, 235, 193, 51, 183, 178, 69, 229, 221, 198, 45, 130, 210, 205, 237, 145, 130, 210, 113, 97, 38, 205, 136, 68, 80, 154, 246, 90, 5, 61, 235, 65, 130, 8, 2, 127, 84, 142, 62, 136, 52, 58, 246, 248, 74, 135, 114, 251, 60, 235, 192, 161, 131, 58, 14, 167, 236, 12, 19, 72, 49, 27, 3, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111})
cursor := soc.IdSize + soc.SignatureSize
data := sch.Data()
id := data[:soc.IdSize]
sig := data[soc.IdSize:cursor]
chunkData := data[cursor:]
chunkAddress := swarm.MustParseHexAddress("2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48")
ch := swarm.NewChunk(chunkAddress, chunkData)
signedDigest, err := soc.Hash(id, ch.Address().Bytes())
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
id := make([]byte, 32)
ownerAddress, err := soc.RecoverAddress(sig, signedDigest)
if err != nil {
t.Fatal(err)
}
payload := []byte("foo")
ch, err := cac.New(payload)
// attempt to recover soc from signed chunk
recoveredSOC, err := soc.FromChunk(sch)
if err != nil {
t.Fatal(err)
}
sch, err := soc.NewChunk(id, ch, signer)
// owner matching means the address was successfully recovered from
// payload and signature
if !bytes.Equal(recoveredSOC.OwnerAddress(), ownerAddress) {
t.Fatalf("owner address mismatch. got %x want %x", recoveredSOC.OwnerAddress(), ownerAddress)
}
if !bytes.Equal(recoveredSOC.ID(), id) {
t.Fatalf("id mismatch. got %x want %x", recoveredSOC.ID(), id)
}
if !bytes.Equal(recoveredSOC.Signature(), sig) {
t.Fatalf("signature mismatch. got %x want %x", recoveredSOC.Signature(), sig)
}
if !ch.Equal(recoveredSOC.WrappedChunk()) {
t.Fatalf("wrapped chunk mismatch. got %s want %s", recoveredSOC.WrappedChunk().Address(), ch.Address())
}
}
func TestCreateAddress(t *testing.T) {
id := make([]byte, soc.IdSize)
owner := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
socAddress := swarm.MustParseHexAddress("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85")
addr, err := soc.CreateAddress(id, owner.Bytes())
if err != nil {
t.Fatal(err)
}
if !addr.Equal(socAddress) {
t.Fatalf("soc address mismatch. got %s want %s", addr, socAddress)
}
}
u2, err := soc.FromChunk(sch)
func TestRecoverAddress(t *testing.T) {
owner := common.HexToAddress("8d3766440f0d7b949a5e32995d09619a7f86e632")
id := make([]byte, soc.IdSize)
chunkAddress := swarm.MustParseHexAddress("2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48")
signedDigest, err := soc.Hash(id, chunkAddress.Bytes())
if err != nil {
t.Fatal(err)
}
// owner matching means the address was successfully recovered from
// payload and signature
publicKey, err := signer.PublicKey()
sig, err := hex.DecodeString("5acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a053deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c1348311b")
if err != nil {
t.Fatal(err)
}
ownerEthereumAddress, err := crypto.NewEthereumAddress(*publicKey)
// attempt to recover address from signature
addr, err := soc.RecoverAddress(sig, signedDigest)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(ownerEthereumAddress, u2.OwnerAddress()) {
t.Fatalf("owner address mismatch %x %x", ownerEthereumAddress, u2.OwnerAddress())
if !bytes.Equal(addr, owner.Bytes()) {
t.Fatalf("owner address mismatch. got %x want %x", addr, owner.Bytes())
}
}
// Copyright 2021 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 testing
import (
"testing"
"github.com/ethersphere/bee/pkg/cac"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/swarm"
)
// MockSOC defines a mocked SOC with exported fields for easy testing.
type MockSOC struct {
ID soc.ID
Owner []byte
Signature []byte
WrappedChunk swarm.Chunk
}
// Address returns the SOC address of the mocked SOC.
func (ms MockSOC) Address() swarm.Address {
addr, _ := soc.CreateAddress(ms.ID, ms.Owner)
return addr
}
// Chunk returns the SOC chunk of the mocked SOC.
func (ms MockSOC) Chunk() swarm.Chunk {
return swarm.NewChunk(ms.Address(), append(ms.ID, append(ms.Signature, ms.WrappedChunk.Data()...)...))
}
// GenerateMockSOC generates a valid mocked SOC from given data.
func GenerateMockSOC(t *testing.T, data []byte) *MockSOC {
t.Helper()
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
owner, err := signer.EthereumAddress()
if err != nil {
t.Fatal(err)
}
ch, err := cac.New(data)
if err != nil {
t.Fatal(err)
}
id := make([]byte, soc.IdSize)
hasher := swarm.NewHasher()
_, err = hasher.Write(append(id, ch.Address().Bytes()...))
if err != nil {
t.Fatal(err)
}
signature, err := signer.Sign(hasher.Sum(nil))
if err != nil {
t.Fatal(err)
}
return &MockSOC{
ID: id,
Owner: owner.Bytes(),
Signature: signature,
WrappedChunk: ch,
}
}
......@@ -15,7 +15,7 @@ func Valid(ch swarm.Chunk) bool {
return false
}
address, err := s.Address()
address, err := s.address()
if err != nil {
return false
}
......
......@@ -5,57 +5,110 @@
package soc_test
import (
"encoding/binary"
"strings"
"testing"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/soc"
"github.com/ethersphere/bee/pkg/swarm"
)
// TestValidator verifies that the validator can detect both
// valid soc chunks, as well as chunks with invalid data and invalid
// address.
func TestValidator(t *testing.T) {
id := make([]byte, soc.IdSize)
privKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
bmtHashOfFoo := "2387e8e7d8a48c2a9339c97c1dc3461a9a7aa07e994c5cb8b38fd7c1b3e6ea48"
address := swarm.MustParseHexAddress(bmtHashOfFoo)
foo := "foo"
fooLength := len(foo)
fooBytes := make([]byte, 8+fooLength)
binary.LittleEndian.PutUint64(fooBytes, uint64(fooLength))
copy(fooBytes[8:], foo)
ch := swarm.NewChunk(address, fooBytes)
sch, err := soc.NewChunk(id, ch, signer)
if err != nil {
t.Fatal(err)
}
// TestValid verifies that the validator can detect
// valid soc chunks.
func TestValid(t *testing.T) {
socAddress := swarm.MustParseHexAddress("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85")
// signed soc chunk of:
// id: 0
// wrapped chunk of: `foo`
// owner: 0x8d3766440f0d7b949a5e32995d09619a7f86e632
sch := swarm.NewChunk(socAddress, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 205, 56, 79, 235, 193, 51, 183, 178, 69, 229, 221, 198, 45, 130, 210, 205, 237, 145, 130, 210, 113, 97, 38, 205, 136, 68, 80, 154, 246, 90, 5, 61, 235, 65, 130, 8, 2, 127, 84, 142, 62, 136, 52, 58, 246, 248, 74, 135, 114, 251, 60, 235, 192, 161, 131, 58, 14, 167, 236, 12, 19, 72, 49, 27, 3, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111})
// check valid chunk
if !soc.Valid(sch) {
t.Fatal("valid chunk evaluates to invalid")
}
}
// check invalid data
sch.Data()[0] = 0x01
if soc.Valid(sch) {
t.Fatal("chunk with invalid data evaluates to valid")
}
// TestInvalid verifies that the validator can detect chunks
// with invalid data and invalid address.
func TestInvalid(t *testing.T) {
socAddress := swarm.MustParseHexAddress("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85")
// signed soc chunk of:
// id: 0
// wrapped chunk of: `foo`
// owner: 0x8d3766440f0d7b949a5e32995d09619a7f86e632
sch := swarm.NewChunk(socAddress, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 205, 56, 79, 235, 193, 51, 183, 178, 69, 229, 221, 198, 45, 130, 210, 205, 237, 145, 130, 210, 113, 97, 38, 205, 136, 68, 80, 154, 246, 90, 5, 61, 235, 65, 130, 8, 2, 127, 84, 142, 62, 136, 52, 58, 246, 248, 74, 135, 114, 251, 60, 235, 192, 161, 131, 58, 14, 167, 236, 12, 19, 72, 49, 27, 3, 0, 0, 0, 0, 0, 0, 0, 102, 111, 111})
// check invalid address
sch.Data()[0] = 0x00
for _, c := range []struct {
name string
chunk func() swarm.Chunk
}{
{
name: "wrong soc address",
chunk: func() swarm.Chunk {
wrongAddressBytes := sch.Address().Bytes()
wrongAddressBytes[0] = 255 - wrongAddressBytes[0]
wrongAddress := swarm.NewAddress(wrongAddressBytes)
sch = swarm.NewChunk(wrongAddress, sch.Data())
if soc.Valid(sch) {
t.Fatal("chunk with invalid address evaluates to valid")
return swarm.NewChunk(wrongAddress, sch.Data())
},
},
{
name: "invalid data",
chunk: func() swarm.Chunk {
data := make([]byte, len(sch.Data()))
copy(data, sch.Data())
cursor := soc.IdSize + soc.SignatureSize
chunkData := data[cursor:]
chunkData[0] = 0x01
return swarm.NewChunk(socAddress, data)
},
},
{
name: "invalid id",
chunk: func() swarm.Chunk {
data := make([]byte, len(sch.Data()))
copy(data, sch.Data())
id := data[:soc.IdSize]
id[0] = 0x01
return swarm.NewChunk(socAddress, data)
},
},
{
name: "invalid signature",
chunk: func() swarm.Chunk {
data := make([]byte, len(sch.Data()))
copy(data, sch.Data())
// modify signature
cursor := soc.IdSize + soc.SignatureSize
sig := data[soc.IdSize:cursor]
sig[0] = 0x01
return swarm.NewChunk(socAddress, data)
},
},
{
name: "nil data",
chunk: func() swarm.Chunk {
return swarm.NewChunk(socAddress, nil)
},
},
{
name: "small data",
chunk: func() swarm.Chunk {
return swarm.NewChunk(socAddress, []byte("small"))
},
},
{
name: "large data",
chunk: func() swarm.Chunk {
return swarm.NewChunk(socAddress, []byte(strings.Repeat("a", swarm.ChunkSize+swarm.SpanSize+1)))
},
},
} {
t.Run(c.name, func(t *testing.T) {
if soc.Valid(c.chunk()) {
t.Fatal("chunk with invalid data evaluates to valid")
}
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment