forked from mirror/go-ethereum
Merge pull request #17231 from ethersphere/develop
swarm: client-side MRU signatures ; BMT fixes ; network simulation testsrelease/1.8
commit
b536460f8e
@ -0,0 +1,169 @@ |
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Command resource allows the user to create and update signed mutable resource updates
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/utils" |
||||
swarm "github.com/ethereum/go-ethereum/swarm/api/client" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mru" |
||||
"gopkg.in/urfave/cli.v1" |
||||
) |
||||
|
||||
func NewGenericSigner(ctx *cli.Context) mru.Signer { |
||||
return mru.NewGenericSigner(getPrivKey(ctx)) |
||||
} |
||||
|
||||
// swarm resource create <frequency> [--name <name>] [--data <0x Hexdata> [--multihash=false]]
|
||||
// swarm resource update <Manifest Address or ENS domain> <0x Hexdata> [--multihash=false]
|
||||
// swarm resource info <Manifest Address or ENS domain>
|
||||
|
||||
func resourceCreate(ctx *cli.Context) { |
||||
args := ctx.Args() |
||||
|
||||
var ( |
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") |
||||
client = swarm.NewClient(bzzapi) |
||||
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name) |
||||
initialData = ctx.String(SwarmResourceDataOnCreateFlag.Name) |
||||
name = ctx.String(SwarmResourceNameFlag.Name) |
||||
) |
||||
|
||||
if len(args) < 1 { |
||||
fmt.Println("Incorrect number of arguments") |
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1) |
||||
return |
||||
} |
||||
signer := NewGenericSigner(ctx) |
||||
frequency, err := strconv.ParseUint(args[0], 10, 64) |
||||
if err != nil { |
||||
fmt.Printf("Frequency formatting error: %s\n", err.Error()) |
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1) |
||||
return |
||||
} |
||||
|
||||
metadata := mru.ResourceMetadata{ |
||||
Name: name, |
||||
Frequency: frequency, |
||||
Owner: signer.Address(), |
||||
} |
||||
|
||||
var newResourceRequest *mru.Request |
||||
if initialData != "" { |
||||
initialDataBytes, err := hexutil.Decode(initialData) |
||||
if err != nil { |
||||
fmt.Printf("Error parsing data: %s\n", err.Error()) |
||||
cli.ShowCommandHelpAndExit(ctx, "create", 1) |
||||
return |
||||
} |
||||
newResourceRequest, err = mru.NewCreateUpdateRequest(&metadata) |
||||
if err != nil { |
||||
utils.Fatalf("Error creating new resource request: %s", err) |
||||
} |
||||
newResourceRequest.SetData(initialDataBytes, multihash) |
||||
if err = newResourceRequest.Sign(signer); err != nil { |
||||
utils.Fatalf("Error signing resource update: %s", err.Error()) |
||||
} |
||||
} else { |
||||
newResourceRequest, err = mru.NewCreateRequest(&metadata) |
||||
if err != nil { |
||||
utils.Fatalf("Error creating new resource request: %s", err) |
||||
} |
||||
} |
||||
|
||||
manifestAddress, err := client.CreateResource(newResourceRequest) |
||||
if err != nil { |
||||
utils.Fatalf("Error creating resource: %s", err.Error()) |
||||
return |
||||
} |
||||
fmt.Println(manifestAddress) // output manifest address to the user in a single line (useful for other commands to pick up)
|
||||
|
||||
} |
||||
|
||||
func resourceUpdate(ctx *cli.Context) { |
||||
args := ctx.Args() |
||||
|
||||
var ( |
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") |
||||
client = swarm.NewClient(bzzapi) |
||||
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name) |
||||
) |
||||
|
||||
if len(args) < 2 { |
||||
fmt.Println("Incorrect number of arguments") |
||||
cli.ShowCommandHelpAndExit(ctx, "update", 1) |
||||
return |
||||
} |
||||
signer := NewGenericSigner(ctx) |
||||
manifestAddressOrDomain := args[0] |
||||
data, err := hexutil.Decode(args[1]) |
||||
if err != nil { |
||||
utils.Fatalf("Error parsing data: %s", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// Retrieve resource status and metadata out of the manifest
|
||||
updateRequest, err := client.GetResourceMetadata(manifestAddressOrDomain) |
||||
if err != nil { |
||||
utils.Fatalf("Error retrieving resource status: %s", err.Error()) |
||||
} |
||||
|
||||
// set the new data
|
||||
updateRequest.SetData(data, multihash) |
||||
|
||||
// sign update
|
||||
if err = updateRequest.Sign(signer); err != nil { |
||||
utils.Fatalf("Error signing resource update: %s", err.Error()) |
||||
} |
||||
|
||||
// post update
|
||||
err = client.UpdateResource(updateRequest) |
||||
if err != nil { |
||||
utils.Fatalf("Error updating resource: %s", err.Error()) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func resourceInfo(ctx *cli.Context) { |
||||
var ( |
||||
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") |
||||
client = swarm.NewClient(bzzapi) |
||||
) |
||||
args := ctx.Args() |
||||
if len(args) < 1 { |
||||
fmt.Println("Incorrect number of arguments.") |
||||
cli.ShowCommandHelpAndExit(ctx, "info", 1) |
||||
return |
||||
} |
||||
manifestAddressOrDomain := args[0] |
||||
metadata, err := client.GetResourceMetadata(manifestAddressOrDomain) |
||||
if err != nil { |
||||
utils.Fatalf("Error retrieving resource metadata: %s", err.Error()) |
||||
return |
||||
} |
||||
encodedMetadata, err := metadata.MarshalJSON() |
||||
if err != nil { |
||||
utils.Fatalf("Error encoding metadata to JSON for display:%s", err) |
||||
} |
||||
fmt.Println(string(encodedMetadata)) |
||||
} |
@ -0,0 +1,81 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
) |
||||
|
||||
// BucketKey is the type that should be used for keys in simulation buckets.
|
||||
type BucketKey string |
||||
|
||||
// NodeItem returns an item set in ServiceFunc function for a particualar node.
|
||||
func (s *Simulation) NodeItem(id discover.NodeID, key interface{}) (value interface{}, ok bool) { |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
|
||||
if _, ok := s.buckets[id]; !ok { |
||||
return nil, false |
||||
} |
||||
return s.buckets[id].Load(key) |
||||
} |
||||
|
||||
// SetNodeItem sets a new item associated with the node with provided NodeID.
|
||||
// Buckets should be used to avoid managing separate simulation global state.
|
||||
func (s *Simulation) SetNodeItem(id discover.NodeID, key interface{}, value interface{}) { |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
|
||||
s.buckets[id].Store(key, value) |
||||
} |
||||
|
||||
// NodeItems returns a map of items from all nodes that are all set under the
|
||||
// same BucketKey.
|
||||
func (s *Simulation) NodesItems(key interface{}) (values map[discover.NodeID]interface{}) { |
||||
s.mu.RLock() |
||||
defer s.mu.RUnlock() |
||||
|
||||
ids := s.NodeIDs() |
||||
values = make(map[discover.NodeID]interface{}, len(ids)) |
||||
for _, id := range ids { |
||||
if _, ok := s.buckets[id]; !ok { |
||||
continue |
||||
} |
||||
if v, ok := s.buckets[id].Load(key); ok { |
||||
values[id] = v |
||||
} |
||||
} |
||||
return values |
||||
} |
||||
|
||||
// UpNodesItems returns a map of items with the same BucketKey from all nodes that are up.
|
||||
func (s *Simulation) UpNodesItems(key interface{}) (values map[discover.NodeID]interface{}) { |
||||
s.mu.RLock() |
||||
defer s.mu.RUnlock() |
||||
|
||||
ids := s.UpNodeIDs() |
||||
values = make(map[discover.NodeID]interface{}) |
||||
for _, id := range ids { |
||||
if _, ok := s.buckets[id]; !ok { |
||||
continue |
||||
} |
||||
if v, ok := s.buckets[id].Load(key); ok { |
||||
values[id] = v |
||||
} |
||||
} |
||||
return values |
||||
} |
@ -0,0 +1,155 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"sync" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
) |
||||
|
||||
// TestServiceBucket tests all bucket functionalities using subtests.
|
||||
// It constructs a simulation of two nodes by adding items to their buckets
|
||||
// in ServiceFunc constructor, then by SetNodeItem. Testing UpNodesItems
|
||||
// is done by stopping one node and validating availability of its items.
|
||||
func TestServiceBucket(t *testing.T) { |
||||
testKey := "Key" |
||||
testValue := "Value" |
||||
|
||||
sim := New(map[string]ServiceFunc{ |
||||
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
b.Store(testKey, testValue+ctx.Config.ID.String()) |
||||
return newNoopService(), nil, nil |
||||
}, |
||||
}) |
||||
defer sim.Close() |
||||
|
||||
id1, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
id2, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
t.Run("ServiceFunc bucket Store", func(t *testing.T) { |
||||
v, ok := sim.NodeItem(id1, testKey) |
||||
if !ok { |
||||
t.Fatal("bucket item not found") |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
t.Fatal("bucket item value is not string") |
||||
} |
||||
if s != testValue+id1.String() { |
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s) |
||||
} |
||||
|
||||
v, ok = sim.NodeItem(id2, testKey) |
||||
if !ok { |
||||
t.Fatal("bucket item not found") |
||||
} |
||||
s, ok = v.(string) |
||||
if !ok { |
||||
t.Fatal("bucket item value is not string") |
||||
} |
||||
if s != testValue+id2.String() { |
||||
t.Fatalf("expected %q, got %q", testValue+id2.String(), s) |
||||
} |
||||
}) |
||||
|
||||
customKey := "anotherKey" |
||||
customValue := "anotherValue" |
||||
|
||||
t.Run("SetNodeItem", func(t *testing.T) { |
||||
sim.SetNodeItem(id1, customKey, customValue) |
||||
|
||||
v, ok := sim.NodeItem(id1, customKey) |
||||
if !ok { |
||||
t.Fatal("bucket item not found") |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
t.Fatal("bucket item value is not string") |
||||
} |
||||
if s != customValue { |
||||
t.Fatalf("expected %q, got %q", customValue, s) |
||||
} |
||||
|
||||
v, ok = sim.NodeItem(id2, customKey) |
||||
if ok { |
||||
t.Fatal("bucket item should not be found") |
||||
} |
||||
}) |
||||
|
||||
if err := sim.StopNode(id2); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
t.Run("UpNodesItems", func(t *testing.T) { |
||||
items := sim.UpNodesItems(testKey) |
||||
|
||||
v, ok := items[id1] |
||||
if !ok { |
||||
t.Errorf("node 1 item not found") |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
t.Fatal("node 1 item value is not string") |
||||
} |
||||
if s != testValue+id1.String() { |
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s) |
||||
} |
||||
|
||||
v, ok = items[id2] |
||||
if ok { |
||||
t.Errorf("node 2 item should not be found") |
||||
} |
||||
}) |
||||
|
||||
t.Run("NodeItems", func(t *testing.T) { |
||||
items := sim.NodesItems(testKey) |
||||
|
||||
v, ok := items[id1] |
||||
if !ok { |
||||
t.Errorf("node 1 item not found") |
||||
} |
||||
s, ok := v.(string) |
||||
if !ok { |
||||
t.Fatal("node 1 item value is not string") |
||||
} |
||||
if s != testValue+id1.String() { |
||||
t.Fatalf("expected %q, got %q", testValue+id1.String(), s) |
||||
} |
||||
|
||||
v, ok = items[id2] |
||||
if !ok { |
||||
t.Errorf("node 2 item not found") |
||||
} |
||||
s, ok = v.(string) |
||||
if !ok { |
||||
t.Fatal("node 1 item value is not string") |
||||
} |
||||
if s != testValue+id2.String() { |
||||
t.Fatalf("expected %q, got %q", testValue+id2.String(), s) |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,159 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
) |
||||
|
||||
// ConnectToPivotNode connects the node with provided NodeID
|
||||
// to the pivot node, already set by Simulation.SetPivotNode method.
|
||||
// It is useful when constructing a star network topology
|
||||
// when simulation adds and removes nodes dynamically.
|
||||
func (s *Simulation) ConnectToPivotNode(id discover.NodeID) (err error) { |
||||
pid := s.PivotNodeID() |
||||
if pid == nil { |
||||
return ErrNoPivotNode |
||||
} |
||||
return s.connect(*pid, id) |
||||
} |
||||
|
||||
// ConnectToLastNode connects the node with provided NodeID
|
||||
// to the last node that is up, and avoiding connection to self.
|
||||
// It is useful when constructing a chain network topology
|
||||
// when simulation adds and removes nodes dynamically.
|
||||
func (s *Simulation) ConnectToLastNode(id discover.NodeID) (err error) { |
||||
ids := s.UpNodeIDs() |
||||
l := len(ids) |
||||
if l < 2 { |
||||
return nil |
||||
} |
||||
lid := ids[l-1] |
||||
if lid == id { |
||||
lid = ids[l-2] |
||||
} |
||||
return s.connect(lid, id) |
||||
} |
||||
|
||||
// ConnectToRandomNode connects the node with provieded NodeID
|
||||
// to a random node that is up.
|
||||
func (s *Simulation) ConnectToRandomNode(id discover.NodeID) (err error) { |
||||
n := s.randomUpNode(id) |
||||
if n == nil { |
||||
return ErrNodeNotFound |
||||
} |
||||
return s.connect(n.ID, id) |
||||
} |
||||
|
||||
// ConnectNodesFull connects all nodes one to another.
|
||||
// It provides a complete connectivity in the network
|
||||
// which should be rarely needed.
|
||||
func (s *Simulation) ConnectNodesFull(ids []discover.NodeID) (err error) { |
||||
if ids == nil { |
||||
ids = s.UpNodeIDs() |
||||
} |
||||
l := len(ids) |
||||
for i := 0; i < l; i++ { |
||||
for j := i + 1; j < l; j++ { |
||||
err = s.connect(ids[i], ids[j]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ConnectNodesChain connects all nodes in a chain topology.
|
||||
// If ids argument is nil, all nodes that are up will be connected.
|
||||
func (s *Simulation) ConnectNodesChain(ids []discover.NodeID) (err error) { |
||||
if ids == nil { |
||||
ids = s.UpNodeIDs() |
||||
} |
||||
l := len(ids) |
||||
for i := 0; i < l-1; i++ { |
||||
err = s.connect(ids[i], ids[i+1]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ConnectNodesRing connects all nodes in a ring topology.
|
||||
// If ids argument is nil, all nodes that are up will be connected.
|
||||
func (s *Simulation) ConnectNodesRing(ids []discover.NodeID) (err error) { |
||||
if ids == nil { |
||||
ids = s.UpNodeIDs() |
||||
} |
||||
l := len(ids) |
||||
if l < 2 { |
||||
return nil |
||||
} |
||||
for i := 0; i < l-1; i++ { |
||||
err = s.connect(ids[i], ids[i+1]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return s.connect(ids[l-1], ids[0]) |
||||
} |
||||
|
||||
// ConnectNodesStar connects all nodes in a star topology
|
||||
// with the center at provided NodeID.
|
||||
// If ids argument is nil, all nodes that are up will be connected.
|
||||
func (s *Simulation) ConnectNodesStar(id discover.NodeID, ids []discover.NodeID) (err error) { |
||||
if ids == nil { |
||||
ids = s.UpNodeIDs() |
||||
} |
||||
l := len(ids) |
||||
for i := 0; i < l; i++ { |
||||
if id == ids[i] { |
||||
continue |
||||
} |
||||
err = s.connect(id, ids[i]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ConnectNodesStar connects all nodes in a star topology
|
||||
// with the center at already set pivot node.
|
||||
// If ids argument is nil, all nodes that are up will be connected.
|
||||
func (s *Simulation) ConnectNodesStarPivot(ids []discover.NodeID) (err error) { |
||||
id := s.PivotNodeID() |
||||
if id == nil { |
||||
return ErrNoPivotNode |
||||
} |
||||
return s.ConnectNodesStar(*id, ids) |
||||
} |
||||
|
||||
// connect connects two nodes but ignores already connected error.
|
||||
func (s *Simulation) connect(oneID, otherID discover.NodeID) error { |
||||
return ignoreAlreadyConnectedErr(s.Net.Connect(oneID, otherID)) |
||||
} |
||||
|
||||
func ignoreAlreadyConnectedErr(err error) error { |
||||
if err == nil || strings.Contains(err.Error(), "already connected") { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
@ -0,0 +1,306 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
) |
||||
|
||||
func TestConnectToPivotNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
pid, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
sim.SetPivotNode(pid) |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectToPivotNode(id) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if sim.Net.GetConn(id, pid) == nil { |
||||
t.Error("node did not connect to pivot node") |
||||
} |
||||
} |
||||
|
||||
func TestConnectToLastNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
n := 10 |
||||
|
||||
ids, err := sim.AddNodes(n) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectToLastNode(id) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for _, i := range ids[:n-2] { |
||||
if sim.Net.GetConn(id, i) != nil { |
||||
t.Error("node connected to the node that is not the last") |
||||
} |
||||
} |
||||
|
||||
if sim.Net.GetConn(id, ids[n-1]) == nil { |
||||
t.Error("node did not connect to the last node") |
||||
} |
||||
} |
||||
|
||||
func TestConnectToRandomNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
n := 10 |
||||
|
||||
ids, err := sim.AddNodes(n) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectToRandomNode(ids[0]) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
var cc int |
||||
for i := 0; i < n; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
if sim.Net.GetConn(ids[i], ids[j]) != nil { |
||||
cc++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
if cc != 1 { |
||||
t.Errorf("expected one connection, got %v", cc) |
||||
} |
||||
} |
||||
|
||||
func TestConnectNodesFull(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(12) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectNodesFull(ids) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testFull(t, sim, ids) |
||||
} |
||||
|
||||
func testFull(t *testing.T, sim *Simulation, ids []discover.NodeID) { |
||||
n := len(ids) |
||||
var cc int |
||||
for i := 0; i < n; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
if sim.Net.GetConn(ids[i], ids[j]) != nil { |
||||
cc++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
want := n * (n - 1) / 2 |
||||
|
||||
if cc != want { |
||||
t.Errorf("expected %v connection, got %v", want, cc) |
||||
} |
||||
} |
||||
|
||||
func TestConnectNodesChain(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectNodesChain(ids) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testChain(t, sim, ids) |
||||
} |
||||
|
||||
func testChain(t *testing.T, sim *Simulation, ids []discover.NodeID) { |
||||
n := len(ids) |
||||
for i := 0; i < n; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
c := sim.Net.GetConn(ids[i], ids[j]) |
||||
if i == j-1 { |
||||
if c == nil { |
||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
||||
} |
||||
} else { |
||||
if c != nil { |
||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestConnectNodesRing(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
err = sim.ConnectNodesRing(ids) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testRing(t, sim, ids) |
||||
} |
||||
|
||||
func testRing(t *testing.T, sim *Simulation, ids []discover.NodeID) { |
||||
n := len(ids) |
||||
for i := 0; i < n; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
c := sim.Net.GetConn(ids[i], ids[j]) |
||||
if i == j-1 || (i == 0 && j == n-1) { |
||||
if c == nil { |
||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
||||
} |
||||
} else { |
||||
if c != nil { |
||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestConnectToNodesStar(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
centerIndex := 2 |
||||
|
||||
err = sim.ConnectNodesStar(ids[centerIndex], ids) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testStar(t, sim, ids, centerIndex) |
||||
} |
||||
|
||||
func testStar(t *testing.T, sim *Simulation, ids []discover.NodeID, centerIndex int) { |
||||
n := len(ids) |
||||
for i := 0; i < n; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
c := sim.Net.GetConn(ids[i], ids[j]) |
||||
if i == centerIndex || j == centerIndex { |
||||
if c == nil { |
||||
t.Errorf("nodes %v and %v are not connected, but they should be", i, j) |
||||
} |
||||
} else { |
||||
if c != nil { |
||||
t.Errorf("nodes %v and %v are connected, but they should not be", i, j) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestConnectToNodesStarPivot(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if len(sim.Net.Conns) > 0 { |
||||
t.Fatal("no connections should exist after just adding nodes") |
||||
} |
||||
|
||||
pivotIndex := 4 |
||||
|
||||
sim.SetPivotNode(ids[pivotIndex]) |
||||
|
||||
err = sim.ConnectNodesStarPivot(ids) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testStar(t, sim, ids, pivotIndex) |
||||
} |
@ -0,0 +1,157 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
) |
||||
|
||||
// PeerEvent is the type of the channel returned by Simulation.PeerEvents.
|
||||
type PeerEvent struct { |
||||
// NodeID is the ID of node that the event is caught on.
|
||||
NodeID discover.NodeID |
||||
// Event is the event that is caught.
|
||||
Event *p2p.PeerEvent |
||||
// Error is the error that may have happened during event watching.
|
||||
Error error |
||||
} |
||||
|
||||
// PeerEventsFilter defines a filter on PeerEvents to exclude messages with
|
||||
// defined properties. Use PeerEventsFilter methods to set required options.
|
||||
type PeerEventsFilter struct { |
||||
t *p2p.PeerEventType |
||||
protocol *string |
||||
msgCode *uint64 |
||||
} |
||||
|
||||
// NewPeerEventsFilter returns a new PeerEventsFilter instance.
|
||||
func NewPeerEventsFilter() *PeerEventsFilter { |
||||
return &PeerEventsFilter{} |
||||
} |
||||
|
||||
// Type sets the filter to only one peer event type.
|
||||
func (f *PeerEventsFilter) Type(t p2p.PeerEventType) *PeerEventsFilter { |
||||
f.t = &t |
||||
return f |
||||
} |
||||
|
||||
// Protocol sets the filter to only one message protocol.
|
||||
func (f *PeerEventsFilter) Protocol(p string) *PeerEventsFilter { |
||||
f.protocol = &p |
||||
return f |
||||
} |
||||
|
||||
// MsgCode sets the filter to only one msg code.
|
||||
func (f *PeerEventsFilter) MsgCode(c uint64) *PeerEventsFilter { |
||||
f.msgCode = &c |
||||
return f |
||||
} |
||||
|
||||
// PeerEvents returns a channel of events that are captured by admin peerEvents
|
||||
// subscription nodes with provided NodeIDs. Additional filters can be set to ignore
|
||||
// events that are not relevant.
|
||||
func (s *Simulation) PeerEvents(ctx context.Context, ids []discover.NodeID, filters ...*PeerEventsFilter) <-chan PeerEvent { |
||||
eventC := make(chan PeerEvent) |
||||
|
||||
for _, id := range ids { |
||||
s.shutdownWG.Add(1) |
||||
go func(id discover.NodeID) { |
||||
defer s.shutdownWG.Done() |
||||
|
||||
client, err := s.Net.GetNode(id).Client() |
||||
if err != nil { |
||||
eventC <- PeerEvent{NodeID: id, Error: err} |
||||
return |
||||
} |
||||
events := make(chan *p2p.PeerEvent) |
||||
sub, err := client.Subscribe(ctx, "admin", events, "peerEvents") |
||||
if err != nil { |
||||
eventC <- PeerEvent{NodeID: id, Error: err} |
||||
return |
||||
} |
||||
defer sub.Unsubscribe() |
||||
|
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
if err := ctx.Err(); err != nil { |
||||
select { |
||||
case eventC <- PeerEvent{NodeID: id, Error: err}: |
||||
case <-s.Done(): |
||||
} |
||||
} |
||||
return |
||||
case <-s.Done(): |
||||
return |
||||
case e := <-events: |
||||
match := len(filters) == 0 // if there are no filters match all events
|
||||
for _, f := range filters { |
||||
if f.t != nil && *f.t != e.Type { |
||||
continue |
||||
} |
||||
if f.protocol != nil && *f.protocol != e.Protocol { |
||||
continue |
||||
} |
||||
if f.msgCode != nil && e.MsgCode != nil && *f.msgCode != *e.MsgCode { |
||||
continue |
||||
} |
||||
// all filter parameters matched, break the loop
|
||||
match = true |
||||
break |
||||
} |
||||
if match { |
||||
select { |
||||
case eventC <- PeerEvent{NodeID: id, Event: e}: |
||||
case <-ctx.Done(): |
||||
if err := ctx.Err(); err != nil { |
||||
select { |
||||
case eventC <- PeerEvent{NodeID: id, Error: err}: |
||||
case <-s.Done(): |
||||
} |
||||
} |
||||
return |
||||
case <-s.Done(): |
||||
return |
||||
} |
||||
} |
||||
case err := <-sub.Err(): |
||||
if err != nil { |
||||
select { |
||||
case eventC <- PeerEvent{NodeID: id, Error: err}: |
||||
case <-ctx.Done(): |
||||
if err := ctx.Err(); err != nil { |
||||
select { |
||||
case eventC <- PeerEvent{NodeID: id, Error: err}: |
||||
case <-s.Done(): |
||||
} |
||||
} |
||||
return |
||||
case <-s.Done(): |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}(id) |
||||
} |
||||
|
||||
return eventC |
||||
} |
@ -0,0 +1,104 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
// TestPeerEvents creates simulation, adds two nodes,
|
||||
// register for peer events, connects nodes in a chain
|
||||
// and waits for the number of connection events to
|
||||
// be received.
|
||||
func TestPeerEvents(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodes(2) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
||||
defer cancel() |
||||
events := sim.PeerEvents(ctx, sim.NodeIDs()) |
||||
|
||||
// two nodes -> two connection events
|
||||
expectedEventCount := 2 |
||||
|
||||
var wg sync.WaitGroup |
||||
wg.Add(expectedEventCount) |
||||
|
||||
go func() { |
||||
for e := range events { |
||||
if e.Error != nil { |
||||
if e.Error == context.Canceled { |
||||
return |
||||
} |
||||
t.Error(e.Error) |
||||
continue |
||||
} |
||||
wg.Done() |
||||
} |
||||
}() |
||||
|
||||
err = sim.ConnectNodesChain(sim.NodeIDs()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
wg.Wait() |
||||
} |
||||
|
||||
func TestPeerEventsTimeout(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodes(2) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) |
||||
defer cancel() |
||||
events := sim.PeerEvents(ctx, sim.NodeIDs()) |
||||
|
||||
done := make(chan struct{}) |
||||
go func() { |
||||
for e := range events { |
||||
if e.Error == context.Canceled { |
||||
return |
||||
} |
||||
if e.Error == context.DeadlineExceeded { |
||||
close(done) |
||||
return |
||||
} else { |
||||
t.Fatal(e.Error) |
||||
} |
||||
} |
||||
}() |
||||
|
||||
select { |
||||
case <-time.After(time.Second): |
||||
t.Error("no context deadline received") |
||||
case <-done: |
||||
// all good, context deadline detected
|
||||
} |
||||
} |
@ -0,0 +1,140 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation_test |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
"github.com/ethereum/go-ethereum/swarm/network" |
||||
"github.com/ethereum/go-ethereum/swarm/network/simulation" |
||||
) |
||||
|
||||
// Every node can have a Kademlia associated using the node bucket under
|
||||
// BucketKeyKademlia key. This allows to use WaitTillHealthy to block until
|
||||
// all nodes have the their Kadmlias healthy.
|
||||
func ExampleSimulation_WaitTillHealthy() { |
||||
sim := simulation.New(map[string]simulation.ServiceFunc{ |
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
addr := network.NewAddrFromNodeID(ctx.Config.ID) |
||||
hp := network.NewHiveParams() |
||||
hp.Discovery = false |
||||
config := &network.BzzConfig{ |
||||
OverlayAddr: addr.Over(), |
||||
UnderlayAddr: addr.Under(), |
||||
HiveParams: hp, |
||||
} |
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams()) |
||||
// store kademlia in node's bucket under BucketKeyKademlia
|
||||
// so that it can be found by WaitTillHealthy method.
|
||||
b.Store(simulation.BucketKeyKademlia, kad) |
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil |
||||
}, |
||||
}) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodesAndConnectRing(10) |
||||
if err != nil { |
||||
// handle error properly...
|
||||
panic(err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) |
||||
defer cancel() |
||||
ill, err := sim.WaitTillHealthy(ctx, 2) |
||||
if err != nil { |
||||
// inspect the latest detected not healthy kademlias
|
||||
for id, kad := range ill { |
||||
fmt.Println("Node", id) |
||||
fmt.Println(kad.String()) |
||||
} |
||||
// handle error...
|
||||
} |
||||
|
||||
// continue with the test
|
||||
} |
||||
|
||||
// Watch all peer events in the simulation network, buy receiving from a channel.
|
||||
func ExampleSimulation_PeerEvents() { |
||||
sim := simulation.New(nil) |
||||
defer sim.Close() |
||||
|
||||
events := sim.PeerEvents(context.Background(), sim.NodeIDs()) |
||||
|
||||
go func() { |
||||
for e := range events { |
||||
if e.Error != nil { |
||||
log.Error("peer event", "err", e.Error) |
||||
continue |
||||
} |
||||
log.Info("peer event", "node", e.NodeID, "peer", e.Event.Peer, "msgcode", e.Event.MsgCode) |
||||
} |
||||
}() |
||||
} |
||||
|
||||
// Detect when a nodes drop a peer.
|
||||
func ExampleSimulation_PeerEvents_disconnections() { |
||||
sim := simulation.New(nil) |
||||
defer sim.Close() |
||||
|
||||
disconnections := sim.PeerEvents( |
||||
context.Background(), |
||||
sim.NodeIDs(), |
||||
simulation.NewPeerEventsFilter().Type(p2p.PeerEventTypeDrop), |
||||
) |
||||
|
||||
go func() { |
||||
for d := range disconnections { |
||||
if d.Error != nil { |
||||
log.Error("peer drop", "err", d.Error) |
||||
continue |
||||
} |
||||
log.Warn("peer drop", "node", d.NodeID, "peer", d.Event.Peer) |
||||
} |
||||
}() |
||||
} |
||||
|
||||
// Watch multiple types of events or messages. In this case, they differ only
|
||||
// by MsgCode, but filters can be set for different types or protocols, too.
|
||||
func ExampleSimulation_PeerEvents_multipleFilters() { |
||||
sim := simulation.New(nil) |
||||
defer sim.Close() |
||||
|
||||
msgs := sim.PeerEvents( |
||||
context.Background(), |
||||
sim.NodeIDs(), |
||||
// Watch when bzz messages 1 and 4 are received.
|
||||
simulation.NewPeerEventsFilter().Type(p2p.PeerEventTypeMsgRecv).Protocol("bzz").MsgCode(1), |
||||
simulation.NewPeerEventsFilter().Type(p2p.PeerEventTypeMsgRecv).Protocol("bzz").MsgCode(4), |
||||
) |
||||
|
||||
go func() { |
||||
for m := range msgs { |
||||
if m.Error != nil { |
||||
log.Error("bzz message", "err", m.Error) |
||||
continue |
||||
} |
||||
log.Info("bzz message", "node", m.NodeID, "peer", m.Event.Peer) |
||||
} |
||||
}() |
||||
} |
@ -0,0 +1,63 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations" |
||||
) |
||||
|
||||
// Package defaults.
|
||||
var ( |
||||
DefaultHTTPSimAddr = ":8888" |
||||
) |
||||
|
||||
//`With`(builder) pattern constructor for Simulation to
|
||||
//start with a HTTP server
|
||||
func (s *Simulation) WithServer(addr string) *Simulation { |
||||
//assign default addr if nothing provided
|
||||
if addr == "" { |
||||
addr = DefaultHTTPSimAddr |
||||
} |
||||
log.Info(fmt.Sprintf("Initializing simulation server on %s...", addr)) |
||||
//initialize the HTTP server
|
||||
s.handler = simulations.NewServer(s.Net) |
||||
s.runC = make(chan struct{}) |
||||
//add swarm specific routes to the HTTP server
|
||||
s.addSimulationRoutes() |
||||
s.httpSrv = &http.Server{ |
||||
Addr: addr, |
||||
Handler: s.handler, |
||||
} |
||||
go s.httpSrv.ListenAndServe() |
||||
return s |
||||
} |
||||
|
||||
//register additional HTTP routes
|
||||
func (s *Simulation) addSimulationRoutes() { |
||||
s.handler.POST("/runsim", s.RunSimulation) |
||||
} |
||||
|
||||
// StartNetwork starts all nodes in the network
|
||||
func (s *Simulation) RunSimulation(w http.ResponseWriter, req *http.Request) { |
||||
log.Debug("RunSimulation endpoint running") |
||||
s.runC <- struct{}{} |
||||
w.WriteHeader(http.StatusOK) |
||||
} |
@ -0,0 +1,104 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net/http" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
) |
||||
|
||||
func TestSimulationWithHTTPServer(t *testing.T) { |
||||
log.Debug("Init simulation") |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) |
||||
defer cancel() |
||||
|
||||
sim := New( |
||||
map[string]ServiceFunc{ |
||||
"noop": func(_ *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
return newNoopService(), nil, nil |
||||
}, |
||||
}).WithServer(DefaultHTTPSimAddr) |
||||
defer sim.Close() |
||||
log.Debug("Done.") |
||||
|
||||
_, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
log.Debug("Starting sim round and let it time out...") |
||||
//first test that running without sending to the channel will actually
|
||||
//block the simulation, so let it time out
|
||||
result := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error { |
||||
log.Debug("Just start the sim without any action and wait for the timeout") |
||||
//ensure with a Sleep that simulation doesn't terminate before the timeout
|
||||
time.Sleep(2 * time.Second) |
||||
return nil |
||||
}) |
||||
|
||||
if result.Error != nil { |
||||
if result.Error.Error() == "context deadline exceeded" { |
||||
log.Debug("Expected timeout error received") |
||||
} else { |
||||
t.Fatal(result.Error) |
||||
} |
||||
} |
||||
|
||||
//now run it again and send the expected signal on the waiting channel,
|
||||
//then close the simulation
|
||||
log.Debug("Starting sim round and wait for frontend signal...") |
||||
//this time the timeout should be long enough so that it doesn't kick in too early
|
||||
ctx, cancel2 := context.WithTimeout(context.Background(), 5*time.Second) |
||||
defer cancel2() |
||||
go sendRunSignal(t) |
||||
result = sim.Run(ctx, func(ctx context.Context, sim *Simulation) error { |
||||
log.Debug("This run waits for the run signal from `frontend`...") |
||||
//ensure with a Sleep that simulation doesn't terminate before the signal is received
|
||||
time.Sleep(2 * time.Second) |
||||
return nil |
||||
}) |
||||
if result.Error != nil { |
||||
t.Fatal(result.Error) |
||||
} |
||||
log.Debug("Test terminated successfully") |
||||
} |
||||
|
||||
func sendRunSignal(t *testing.T) { |
||||
//We need to first wait for the sim HTTP server to start running...
|
||||
time.Sleep(2 * time.Second) |
||||
//then we can send the signal
|
||||
|
||||
log.Debug("Sending run signal to simulation: POST /runsim...") |
||||
resp, err := http.Post(fmt.Sprintf("http://localhost%s/runsim", DefaultHTTPSimAddr), "application/json", nil) |
||||
if err != nil { |
||||
t.Fatalf("Request failed: %v", err) |
||||
} |
||||
defer resp.Body.Close() |
||||
log.Debug("Signal sent") |
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Fatalf("err %s", resp.Status) |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/hex" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/swarm/network" |
||||
) |
||||
|
||||
// BucketKeyKademlia is the key to be used for storing the kademlia
|
||||
// instance for particuar node, usually inside the ServiceFunc function.
|
||||
var BucketKeyKademlia BucketKey = "kademlia" |
||||
|
||||
// WaitTillHealthy is blocking until the health of all kademlias is true.
|
||||
// If error is not nil, a map of kademlia that was found not healthy is returned.
|
||||
func (s *Simulation) WaitTillHealthy(ctx context.Context, kadMinProxSize int) (ill map[discover.NodeID]*network.Kademlia, err error) { |
||||
// Prepare PeerPot map for checking Kademlia health
|
||||
var ppmap map[string]*network.PeerPot |
||||
kademlias := s.kademlias() |
||||
addrs := make([][]byte, 0, len(kademlias)) |
||||
for _, k := range kademlias { |
||||
addrs = append(addrs, k.BaseAddr()) |
||||
} |
||||
ppmap = network.NewPeerPotMap(kadMinProxSize, addrs) |
||||
|
||||
// Wait for healthy Kademlia on every node before checking files
|
||||
ticker := time.NewTicker(200 * time.Millisecond) |
||||
defer ticker.Stop() |
||||
|
||||
ill = make(map[discover.NodeID]*network.Kademlia) |
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
return ill, ctx.Err() |
||||
case <-ticker.C: |
||||
for k := range ill { |
||||
delete(ill, k) |
||||
} |
||||
log.Debug("kademlia health check", "addr count", len(addrs)) |
||||
for id, k := range kademlias { |
||||
//PeerPot for this node
|
||||
addr := common.Bytes2Hex(k.BaseAddr()) |
||||
pp := ppmap[addr] |
||||
//call Healthy RPC
|
||||
h := k.Healthy(pp) |
||||
//print info
|
||||
log.Debug(k.String()) |
||||
log.Debug("kademlia", "empty bins", pp.EmptyBins, "gotNN", h.GotNN, "knowNN", h.KnowNN, "full", h.Full) |
||||
log.Debug("kademlia", "health", h.GotNN && h.KnowNN && h.Full, "addr", hex.EncodeToString(k.BaseAddr()), "node", id) |
||||
log.Debug("kademlia", "ill condition", !h.GotNN || !h.Full, "addr", hex.EncodeToString(k.BaseAddr()), "node", id) |
||||
if !h.GotNN || !h.Full { |
||||
ill[id] = k |
||||
} |
||||
} |
||||
if len(ill) == 0 { |
||||
return nil, nil |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// kademlias returns all Kademlia instances that are set
|
||||
// in simulation bucket.
|
||||
func (s *Simulation) kademlias() (ks map[discover.NodeID]*network.Kademlia) { |
||||
items := s.UpNodesItems(BucketKeyKademlia) |
||||
ks = make(map[discover.NodeID]*network.Kademlia, len(items)) |
||||
for id, v := range items { |
||||
k, ok := v.(*network.Kademlia) |
||||
if !ok { |
||||
continue |
||||
} |
||||
ks[id] = k |
||||
} |
||||
return ks |
||||
} |
@ -0,0 +1,67 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
"github.com/ethereum/go-ethereum/swarm/network" |
||||
) |
||||
|
||||
func TestWaitTillHealthy(t *testing.T) { |
||||
sim := New(map[string]ServiceFunc{ |
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
addr := network.NewAddrFromNodeID(ctx.Config.ID) |
||||
hp := network.NewHiveParams() |
||||
hp.Discovery = false |
||||
config := &network.BzzConfig{ |
||||
OverlayAddr: addr.Over(), |
||||
UnderlayAddr: addr.Under(), |
||||
HiveParams: hp, |
||||
} |
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams()) |
||||
// store kademlia in node's bucket under BucketKeyKademlia
|
||||
// so that it can be found by WaitTillHealthy method.
|
||||
b.Store(BucketKeyKademlia, kad) |
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil |
||||
}, |
||||
}) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodesAndConnectRing(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) |
||||
defer cancel() |
||||
ill, err := sim.WaitTillHealthy(ctx, 2) |
||||
if err != nil { |
||||
for id, kad := range ill { |
||||
t.Log("Node", id) |
||||
t.Log(kad.String()) |
||||
} |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,357 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"io/ioutil" |
||||
"math/rand" |
||||
"os" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
) |
||||
|
||||
// NodeIDs returns NodeIDs for all nodes in the network.
|
||||
func (s *Simulation) NodeIDs() (ids []discover.NodeID) { |
||||
nodes := s.Net.GetNodes() |
||||
ids = make([]discover.NodeID, len(nodes)) |
||||
for i, node := range nodes { |
||||
ids[i] = node.ID() |
||||
} |
||||
return ids |
||||
} |
||||
|
||||
// UpNodeIDs returns NodeIDs for nodes that are up in the network.
|
||||
func (s *Simulation) UpNodeIDs() (ids []discover.NodeID) { |
||||
nodes := s.Net.GetNodes() |
||||
for _, node := range nodes { |
||||
if node.Up { |
||||
ids = append(ids, node.ID()) |
||||
} |
||||
} |
||||
return ids |
||||
} |
||||
|
||||
// DownNodeIDs returns NodeIDs for nodes that are stopped in the network.
|
||||
func (s *Simulation) DownNodeIDs() (ids []discover.NodeID) { |
||||
nodes := s.Net.GetNodes() |
||||
for _, node := range nodes { |
||||
if !node.Up { |
||||
ids = append(ids, node.ID()) |
||||
} |
||||
} |
||||
return ids |
||||
} |
||||
|
||||
// AddNodeOption defines the option that can be passed
|
||||
// to Simulation.AddNode method.
|
||||
type AddNodeOption func(*adapters.NodeConfig) |
||||
|
||||
// AddNodeWithMsgEvents sets the EnableMsgEvents option
|
||||
// to NodeConfig.
|
||||
func AddNodeWithMsgEvents(enable bool) AddNodeOption { |
||||
return func(o *adapters.NodeConfig) { |
||||
o.EnableMsgEvents = enable |
||||
} |
||||
} |
||||
|
||||
// AddNodeWithService specifies a service that should be
|
||||
// started on a node. This option can be repeated as variadic
|
||||
// argument toe AddNode and other add node related methods.
|
||||
// If AddNodeWithService is not specified, all services will be started.
|
||||
func AddNodeWithService(serviceName string) AddNodeOption { |
||||
return func(o *adapters.NodeConfig) { |
||||
o.Services = append(o.Services, serviceName) |
||||
} |
||||
} |
||||
|
||||
// AddNode creates a new node with random configuration,
|
||||
// applies provided options to the config and adds the node to network.
|
||||
// By default all services will be started on a node. If one or more
|
||||
// AddNodeWithService option are provided, only specified services will be started.
|
||||
func (s *Simulation) AddNode(opts ...AddNodeOption) (id discover.NodeID, err error) { |
||||
conf := adapters.RandomNodeConfig() |
||||
for _, o := range opts { |
||||
o(conf) |
||||
} |
||||
if len(conf.Services) == 0 { |
||||
conf.Services = s.serviceNames |
||||
} |
||||
node, err := s.Net.NewNodeWithConfig(conf) |
||||
if err != nil { |
||||
return id, err |
||||
} |
||||
return node.ID(), s.Net.Start(node.ID()) |
||||
} |
||||
|
||||
// AddNodes creates new nodes with random configurations,
|
||||
// applies provided options to the config and adds nodes to network.
|
||||
func (s *Simulation) AddNodes(count int, opts ...AddNodeOption) (ids []discover.NodeID, err error) { |
||||
ids = make([]discover.NodeID, 0, count) |
||||
for i := 0; i < count; i++ { |
||||
id, err := s.AddNode(opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ids = append(ids, id) |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// AddNodesAndConnectFull is a helpper method that combines
|
||||
// AddNodes and ConnectNodesFull. Only new nodes will be connected.
|
||||
func (s *Simulation) AddNodesAndConnectFull(count int, opts ...AddNodeOption) (ids []discover.NodeID, err error) { |
||||
if count < 2 { |
||||
return nil, errors.New("count of nodes must be at least 2") |
||||
} |
||||
ids, err = s.AddNodes(count, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = s.ConnectNodesFull(ids) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// AddNodesAndConnectChain is a helpper method that combines
|
||||
// AddNodes and ConnectNodesChain. The chain will be continued from the last
|
||||
// added node, if there is one in simulation using ConnectToLastNode method.
|
||||
func (s *Simulation) AddNodesAndConnectChain(count int, opts ...AddNodeOption) (ids []discover.NodeID, err error) { |
||||
if count < 2 { |
||||
return nil, errors.New("count of nodes must be at least 2") |
||||
} |
||||
id, err := s.AddNode(opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = s.ConnectToLastNode(id) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ids, err = s.AddNodes(count-1, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ids = append([]discover.NodeID{id}, ids...) |
||||
err = s.ConnectNodesChain(ids) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// AddNodesAndConnectRing is a helpper method that combines
|
||||
// AddNodes and ConnectNodesRing.
|
||||
func (s *Simulation) AddNodesAndConnectRing(count int, opts ...AddNodeOption) (ids []discover.NodeID, err error) { |
||||
if count < 2 { |
||||
return nil, errors.New("count of nodes must be at least 2") |
||||
} |
||||
ids, err = s.AddNodes(count, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = s.ConnectNodesRing(ids) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// AddNodesAndConnectStar is a helpper method that combines
|
||||
// AddNodes and ConnectNodesStar.
|
||||
func (s *Simulation) AddNodesAndConnectStar(count int, opts ...AddNodeOption) (ids []discover.NodeID, err error) { |
||||
if count < 2 { |
||||
return nil, errors.New("count of nodes must be at least 2") |
||||
} |
||||
ids, err = s.AddNodes(count, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = s.ConnectNodesStar(ids[0], ids[1:]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
//Upload a snapshot
|
||||
//This method tries to open the json file provided, applies the config to all nodes
|
||||
//and then loads the snapshot into the Simulation network
|
||||
func (s *Simulation) UploadSnapshot(snapshotFile string, opts ...AddNodeOption) error { |
||||
f, err := os.Open(snapshotFile) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer f.Close() |
||||
jsonbyte, err := ioutil.ReadAll(f) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var snap simulations.Snapshot |
||||
err = json.Unmarshal(jsonbyte, &snap) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
//the snapshot probably has the property EnableMsgEvents not set
|
||||
//just in case, set it to true!
|
||||
//(we need this to wait for messages before uploading)
|
||||
for _, n := range snap.Nodes { |
||||
n.Node.Config.EnableMsgEvents = true |
||||
n.Node.Config.Services = s.serviceNames |
||||
for _, o := range opts { |
||||
o(n.Node.Config) |
||||
} |
||||
} |
||||
|
||||
log.Info("Waiting for p2p connections to be established...") |
||||
|
||||
//now we can load the snapshot
|
||||
err = s.Net.Load(&snap) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
log.Info("Snapshot loaded") |
||||
return nil |
||||
} |
||||
|
||||
// SetPivotNode sets the NodeID of the network's pivot node.
|
||||
// Pivot node is just a specific node that should be treated
|
||||
// differently then other nodes in test. SetPivotNode and
|
||||
// PivotNodeID are just a convenient functions to set and
|
||||
// retrieve it.
|
||||
func (s *Simulation) SetPivotNode(id discover.NodeID) { |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
s.pivotNodeID = &id |
||||
} |
||||
|
||||
// PivotNodeID returns NodeID of the pivot node set by
|
||||
// Simulation.SetPivotNode method.
|
||||
func (s *Simulation) PivotNodeID() (id *discover.NodeID) { |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
return s.pivotNodeID |
||||
} |
||||
|
||||
// StartNode starts a node by NodeID.
|
||||
func (s *Simulation) StartNode(id discover.NodeID) (err error) { |
||||
return s.Net.Start(id) |
||||
} |
||||
|
||||
// StartRandomNode starts a random node.
|
||||
func (s *Simulation) StartRandomNode() (id discover.NodeID, err error) { |
||||
n := s.randomDownNode() |
||||
if n == nil { |
||||
return id, ErrNodeNotFound |
||||
} |
||||
return n.ID, s.Net.Start(n.ID) |
||||
} |
||||
|
||||
// StartRandomNodes starts random nodes.
|
||||
func (s *Simulation) StartRandomNodes(count int) (ids []discover.NodeID, err error) { |
||||
ids = make([]discover.NodeID, 0, count) |
||||
downIDs := s.DownNodeIDs() |
||||
for i := 0; i < count; i++ { |
||||
n := s.randomNode(downIDs, ids...) |
||||
if n == nil { |
||||
return nil, ErrNodeNotFound |
||||
} |
||||
err = s.Net.Start(n.ID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ids = append(ids, n.ID) |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// StopNode stops a node by NodeID.
|
||||
func (s *Simulation) StopNode(id discover.NodeID) (err error) { |
||||
return s.Net.Stop(id) |
||||
} |
||||
|
||||
// StopRandomNode stops a random node.
|
||||
func (s *Simulation) StopRandomNode() (id discover.NodeID, err error) { |
||||
n := s.randomUpNode() |
||||
if n == nil { |
||||
return id, ErrNodeNotFound |
||||
} |
||||
return n.ID, s.Net.Stop(n.ID) |
||||
} |
||||
|
||||
// StopRandomNodes stops random nodes.
|
||||
func (s *Simulation) StopRandomNodes(count int) (ids []discover.NodeID, err error) { |
||||
ids = make([]discover.NodeID, 0, count) |
||||
upIDs := s.UpNodeIDs() |
||||
for i := 0; i < count; i++ { |
||||
n := s.randomNode(upIDs, ids...) |
||||
if n == nil { |
||||
return nil, ErrNodeNotFound |
||||
} |
||||
err = s.Net.Stop(n.ID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ids = append(ids, n.ID) |
||||
} |
||||
return ids, nil |
||||
} |
||||
|
||||
// seed the random generator for Simulation.randomNode.
|
||||
func init() { |
||||
rand.Seed(time.Now().UnixNano()) |
||||
} |
||||
|
||||
// randomUpNode returns a random SimNode that is up.
|
||||
// Arguments are NodeIDs for nodes that should not be returned.
|
||||
func (s *Simulation) randomUpNode(exclude ...discover.NodeID) *adapters.SimNode { |
||||
return s.randomNode(s.UpNodeIDs(), exclude...) |
||||
} |
||||
|
||||
// randomUpNode returns a random SimNode that is not up.
|
||||
func (s *Simulation) randomDownNode(exclude ...discover.NodeID) *adapters.SimNode { |
||||
return s.randomNode(s.DownNodeIDs(), exclude...) |
||||
} |
||||
|
||||
// randomUpNode returns a random SimNode from the slice of NodeIDs.
|
||||
func (s *Simulation) randomNode(ids []discover.NodeID, exclude ...discover.NodeID) *adapters.SimNode { |
||||
for _, e := range exclude { |
||||
var i int |
||||
for _, id := range ids { |
||||
if id == e { |
||||
ids = append(ids[:i], ids[i+1:]...) |
||||
} else { |
||||
i++ |
||||
} |
||||
} |
||||
} |
||||
l := len(ids) |
||||
if l == 0 { |
||||
return nil |
||||
} |
||||
n := s.Net.GetNode(ids[rand.Intn(l)]) |
||||
node, _ := n.Node.(*adapters.SimNode) |
||||
return node |
||||
} |
@ -0,0 +1,462 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
"github.com/ethereum/go-ethereum/swarm/network" |
||||
) |
||||
|
||||
func TestUpDownNodeIDs(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
gotIDs := sim.NodeIDs() |
||||
|
||||
if !equalNodeIDs(ids, gotIDs) { |
||||
t.Error("returned nodes are not equal to added ones") |
||||
} |
||||
|
||||
stoppedIDs, err := sim.StopRandomNodes(3) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
gotIDs = sim.UpNodeIDs() |
||||
|
||||
for _, id := range gotIDs { |
||||
if !sim.Net.GetNode(id).Up { |
||||
t.Errorf("node %s should not be down", id) |
||||
} |
||||
} |
||||
|
||||
if !equalNodeIDs(ids, append(gotIDs, stoppedIDs...)) { |
||||
t.Error("returned nodes are not equal to added ones") |
||||
} |
||||
|
||||
gotIDs = sim.DownNodeIDs() |
||||
|
||||
for _, id := range gotIDs { |
||||
if sim.Net.GetNode(id).Up { |
||||
t.Errorf("node %s should not be up", id) |
||||
} |
||||
} |
||||
|
||||
if !equalNodeIDs(stoppedIDs, gotIDs) { |
||||
t.Error("returned nodes are not equal to the stopped ones") |
||||
} |
||||
} |
||||
|
||||
func equalNodeIDs(one, other []discover.NodeID) bool { |
||||
if len(one) != len(other) { |
||||
return false |
||||
} |
||||
var count int |
||||
for _, a := range one { |
||||
var found bool |
||||
for _, b := range other { |
||||
if a == b { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if found { |
||||
count++ |
||||
} else { |
||||
return false |
||||
} |
||||
} |
||||
return count == len(one) |
||||
} |
||||
|
||||
func TestAddNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
n := sim.Net.GetNode(id) |
||||
if n == nil { |
||||
t.Fatal("node not found") |
||||
} |
||||
|
||||
if !n.Up { |
||||
t.Error("node not started") |
||||
} |
||||
} |
||||
|
||||
func TestAddNodeWithMsgEvents(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode(AddNodeWithMsgEvents(true)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if !sim.Net.GetNode(id).Config.EnableMsgEvents { |
||||
t.Error("EnableMsgEvents is false") |
||||
} |
||||
|
||||
id, err = sim.AddNode(AddNodeWithMsgEvents(false)) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if sim.Net.GetNode(id).Config.EnableMsgEvents { |
||||
t.Error("EnableMsgEvents is true") |
||||
} |
||||
} |
||||
|
||||
func TestAddNodeWithService(t *testing.T) { |
||||
sim := New(map[string]ServiceFunc{ |
||||
"noop1": noopServiceFunc, |
||||
"noop2": noopServiceFunc, |
||||
}) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode(AddNodeWithService("noop1")) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
n := sim.Net.GetNode(id).Node.(*adapters.SimNode) |
||||
if n.Service("noop1") == nil { |
||||
t.Error("service noop1 not found on node") |
||||
} |
||||
if n.Service("noop2") != nil { |
||||
t.Error("service noop2 should not be found on node") |
||||
} |
||||
} |
||||
|
||||
func TestAddNodes(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
nodesCount := 12 |
||||
|
||||
ids, err := sim.AddNodes(nodesCount) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
count := len(ids) |
||||
if count != nodesCount { |
||||
t.Errorf("expected %v nodes, got %v", nodesCount, count) |
||||
} |
||||
|
||||
count = len(sim.Net.GetNodes()) |
||||
if count != nodesCount { |
||||
t.Errorf("expected %v nodes, got %v", nodesCount, count) |
||||
} |
||||
} |
||||
|
||||
func TestAddNodesAndConnectFull(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
n := 12 |
||||
|
||||
ids, err := sim.AddNodesAndConnectFull(n) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testFull(t, sim, ids) |
||||
} |
||||
|
||||
func TestAddNodesAndConnectChain(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodesAndConnectChain(12) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// add another set of nodes to test
|
||||
// if two chains are connected
|
||||
_, err = sim.AddNodesAndConnectChain(7) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testChain(t, sim, sim.UpNodeIDs()) |
||||
} |
||||
|
||||
func TestAddNodesAndConnectRing(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodesAndConnectRing(12) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testRing(t, sim, ids) |
||||
} |
||||
|
||||
func TestAddNodesAndConnectStar(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
ids, err := sim.AddNodesAndConnectStar(12) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
testStar(t, sim, ids, 0) |
||||
} |
||||
|
||||
//To test that uploading a snapshot works
|
||||
func TestUploadSnapshot(t *testing.T) { |
||||
log.Debug("Creating simulation") |
||||
s := New(map[string]ServiceFunc{ |
||||
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
addr := network.NewAddrFromNodeID(ctx.Config.ID) |
||||
hp := network.NewHiveParams() |
||||
hp.Discovery = false |
||||
config := &network.BzzConfig{ |
||||
OverlayAddr: addr.Over(), |
||||
UnderlayAddr: addr.Under(), |
||||
HiveParams: hp, |
||||
} |
||||
kad := network.NewKademlia(addr.Over(), network.NewKadParams()) |
||||
return network.NewBzz(config, kad, nil, nil, nil), nil, nil |
||||
}, |
||||
}) |
||||
defer s.Close() |
||||
|
||||
nodeCount := 16 |
||||
log.Debug("Uploading snapshot") |
||||
err := s.UploadSnapshot(fmt.Sprintf("../stream/testing/snapshot_%d.json", nodeCount)) |
||||
if err != nil { |
||||
t.Fatalf("Error uploading snapshot to simulation network: %v", err) |
||||
} |
||||
|
||||
ctx := context.Background() |
||||
log.Debug("Starting simulation...") |
||||
s.Run(ctx, func(ctx context.Context, sim *Simulation) error { |
||||
log.Debug("Checking") |
||||
nodes := sim.UpNodeIDs() |
||||
if len(nodes) != nodeCount { |
||||
t.Fatal("Simulation network node number doesn't match snapshot node number") |
||||
} |
||||
return nil |
||||
}) |
||||
log.Debug("Done.") |
||||
} |
||||
|
||||
func TestPivotNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
id2, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if sim.PivotNodeID() != nil { |
||||
t.Error("expected no pivot node") |
||||
} |
||||
|
||||
sim.SetPivotNode(id) |
||||
|
||||
pid := sim.PivotNodeID() |
||||
|
||||
if pid == nil { |
||||
t.Error("pivot node not set") |
||||
} else if *pid != id { |
||||
t.Errorf("expected pivot node %s, got %s", id, *pid) |
||||
} |
||||
|
||||
sim.SetPivotNode(id2) |
||||
|
||||
pid = sim.PivotNodeID() |
||||
|
||||
if pid == nil { |
||||
t.Error("pivot node not set") |
||||
} else if *pid != id2 { |
||||
t.Errorf("expected pivot node %s, got %s", id2, *pid) |
||||
} |
||||
} |
||||
|
||||
func TestStartStopNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
n := sim.Net.GetNode(id) |
||||
if n == nil { |
||||
t.Fatal("node not found") |
||||
} |
||||
if !n.Up { |
||||
t.Error("node not started") |
||||
} |
||||
|
||||
err = sim.StopNode(id) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if n.Up { |
||||
t.Error("node not stopped") |
||||
} |
||||
|
||||
// Sleep here to ensure that Network.watchPeerEvents defer function
|
||||
// has set the `node.Up = false` before we start the node again.
|
||||
// p2p/simulations/network.go:215
|
||||
//
|
||||
// The same node is stopped and started again, and upon start
|
||||
// watchPeerEvents is started in a goroutine. If the node is stopped
|
||||
// and then very quickly started, that goroutine may be scheduled later
|
||||
// then start and force `node.Up = false` in its defer function.
|
||||
// This will make this test unreliable.
|
||||
time.Sleep(time.Second) |
||||
|
||||
err = sim.StartNode(id) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !n.Up { |
||||
t.Error("node not started") |
||||
} |
||||
} |
||||
|
||||
func TestStartStopRandomNode(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodes(3) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
id, err := sim.StopRandomNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
n := sim.Net.GetNode(id) |
||||
if n == nil { |
||||
t.Fatal("node not found") |
||||
} |
||||
if n.Up { |
||||
t.Error("node not stopped") |
||||
} |
||||
|
||||
id2, err := sim.StopRandomNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Sleep here to ensure that Network.watchPeerEvents defer function
|
||||
// has set the `node.Up = false` before we start the node again.
|
||||
// p2p/simulations/network.go:215
|
||||
//
|
||||
// The same node is stopped and started again, and upon start
|
||||
// watchPeerEvents is started in a goroutine. If the node is stopped
|
||||
// and then very quickly started, that goroutine may be scheduled later
|
||||
// then start and force `node.Up = false` in its defer function.
|
||||
// This will make this test unreliable.
|
||||
time.Sleep(time.Second) |
||||
|
||||
idStarted, err := sim.StartRandomNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if idStarted != id && idStarted != id2 { |
||||
t.Error("unexpected started node ID") |
||||
} |
||||
} |
||||
|
||||
func TestStartStopRandomNodes(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
_, err := sim.AddNodes(10) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
ids, err := sim.StopRandomNodes(3) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for _, id := range ids { |
||||
n := sim.Net.GetNode(id) |
||||
if n == nil { |
||||
t.Fatal("node not found") |
||||
} |
||||
if n.Up { |
||||
t.Error("node not stopped") |
||||
} |
||||
} |
||||
|
||||
// Sleep here to ensure that Network.watchPeerEvents defer function
|
||||
// has set the `node.Up = false` before we start the node again.
|
||||
// p2p/simulations/network.go:215
|
||||
//
|
||||
// The same node is stopped and started again, and upon start
|
||||
// watchPeerEvents is started in a goroutine. If the node is stopped
|
||||
// and then very quickly started, that goroutine may be scheduled later
|
||||
// then start and force `node.Up = false` in its defer function.
|
||||
// This will make this test unreliable.
|
||||
time.Sleep(time.Second) |
||||
|
||||
ids, err = sim.StartRandomNodes(2) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
for _, id := range ids { |
||||
n := sim.Net.GetNode(id) |
||||
if n == nil { |
||||
t.Fatal("node not found") |
||||
} |
||||
if !n.Up { |
||||
t.Error("node not started") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,65 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
) |
||||
|
||||
// Service returns a single Service by name on a particular node
|
||||
// with provided id.
|
||||
func (s *Simulation) Service(name string, id discover.NodeID) node.Service { |
||||
simNode, ok := s.Net.GetNode(id).Node.(*adapters.SimNode) |
||||
if !ok { |
||||
return nil |
||||
} |
||||
services := simNode.ServiceMap() |
||||
if len(services) == 0 { |
||||
return nil |
||||
} |
||||
return services[name] |
||||
} |
||||
|
||||
// RandomService returns a single Service by name on a
|
||||
// randomly chosen node that is up.
|
||||
func (s *Simulation) RandomService(name string) node.Service { |
||||
n := s.randomUpNode() |
||||
if n == nil { |
||||
return nil |
||||
} |
||||
return n.Service(name) |
||||
} |
||||
|
||||
// Services returns all services with a provided name
|
||||
// from nodes that are up.
|
||||
func (s *Simulation) Services(name string) (services map[discover.NodeID]node.Service) { |
||||
nodes := s.Net.GetNodes() |
||||
services = make(map[discover.NodeID]node.Service) |
||||
for _, node := range nodes { |
||||
if !node.Up { |
||||
continue |
||||
} |
||||
simNode, ok := node.Node.(*adapters.SimNode) |
||||
if !ok { |
||||
continue |
||||
} |
||||
services[node.ID()] = simNode.Service(name) |
||||
} |
||||
return services |
||||
} |
@ -0,0 +1,46 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestService(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
id, err := sim.AddNode() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
_, ok := sim.Service("noop", id).(*noopService) |
||||
if !ok { |
||||
t.Fatalf("service is not of %T type", &noopService{}) |
||||
} |
||||
|
||||
_, ok = sim.RandomService("noop").(*noopService) |
||||
if !ok { |
||||
t.Fatalf("service is not of %T type", &noopService{}) |
||||
} |
||||
|
||||
_, ok = sim.Services("noop")[id].(*noopService) |
||||
if !ok { |
||||
t.Fatalf("service is not of %T type", &noopService{}) |
||||
} |
||||
} |
@ -0,0 +1,201 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"net/http" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p/discover" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
) |
||||
|
||||
// Common errors that are returned by functions in this package.
|
||||
var ( |
||||
ErrNodeNotFound = errors.New("node not found") |
||||
ErrNoPivotNode = errors.New("no pivot node set") |
||||
) |
||||
|
||||
// Simulation provides methods on network, nodes and services
|
||||
// to manage them.
|
||||
type Simulation struct { |
||||
// Net is exposed as a way to access lower level functionalities
|
||||
// of p2p/simulations.Network.
|
||||
Net *simulations.Network |
||||
|
||||
serviceNames []string |
||||
cleanupFuncs []func() |
||||
buckets map[discover.NodeID]*sync.Map |
||||
pivotNodeID *discover.NodeID |
||||
shutdownWG sync.WaitGroup |
||||
done chan struct{} |
||||
mu sync.RWMutex |
||||
|
||||
httpSrv *http.Server //attach a HTTP server via SimulationOptions
|
||||
handler *simulations.Server //HTTP handler for the server
|
||||
runC chan struct{} //channel where frontend signals it is ready
|
||||
} |
||||
|
||||
// ServiceFunc is used in New to declare new service constructor.
|
||||
// The first argument provides ServiceContext from the adapters package
|
||||
// giving for example the access to NodeID. Second argument is the sync.Map
|
||||
// where all "global" state related to the service should be kept.
|
||||
// All cleanups needed for constructed service and any other constructed
|
||||
// objects should ne provided in a single returned cleanup function.
|
||||
type ServiceFunc func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) |
||||
|
||||
// New creates a new Simulation instance with new
|
||||
// simulations.Network initialized with provided services.
|
||||
func New(services map[string]ServiceFunc) (s *Simulation) { |
||||
s = &Simulation{ |
||||
buckets: make(map[discover.NodeID]*sync.Map), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
adapterServices := make(map[string]adapters.ServiceFunc, len(services)) |
||||
for name, serviceFunc := range services { |
||||
s.serviceNames = append(s.serviceNames, name) |
||||
adapterServices[name] = func(ctx *adapters.ServiceContext) (node.Service, error) { |
||||
b := new(sync.Map) |
||||
service, cleanup, err := serviceFunc(ctx, b) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
if cleanup != nil { |
||||
s.cleanupFuncs = append(s.cleanupFuncs, cleanup) |
||||
} |
||||
s.buckets[ctx.Config.ID] = b |
||||
return service, nil |
||||
} |
||||
} |
||||
|
||||
s.Net = simulations.NewNetwork( |
||||
adapters.NewSimAdapter(adapterServices), |
||||
&simulations.NetworkConfig{ID: "0"}, |
||||
) |
||||
|
||||
return s |
||||
} |
||||
|
||||
// RunFunc is the function that will be called
|
||||
// on Simulation.Run method call.
|
||||
type RunFunc func(context.Context, *Simulation) error |
||||
|
||||
// Result is the returned value of Simulation.Run method.
|
||||
type Result struct { |
||||
Duration time.Duration |
||||
Error error |
||||
} |
||||
|
||||
// Run calls the RunFunc function while taking care of
|
||||
// cancelation provided through the Context.
|
||||
func (s *Simulation) Run(ctx context.Context, f RunFunc) (r Result) { |
||||
//if the option is set to run a HTTP server with the simulation,
|
||||
//init the server and start it
|
||||
start := time.Now() |
||||
if s.httpSrv != nil { |
||||
log.Info("Waiting for frontend to be ready...(send POST /runsim to HTTP server)") |
||||
//wait for the frontend to connect
|
||||
select { |
||||
case <-s.runC: |
||||
case <-ctx.Done(): |
||||
return Result{ |
||||
Duration: time.Since(start), |
||||
Error: ctx.Err(), |
||||
} |
||||
} |
||||
log.Info("Received signal from frontend - starting simulation run.") |
||||
} |
||||
errc := make(chan error) |
||||
quit := make(chan struct{}) |
||||
defer close(quit) |
||||
go func() { |
||||
select { |
||||
case errc <- f(ctx, s): |
||||
case <-quit: |
||||
} |
||||
}() |
||||
var err error |
||||
select { |
||||
case <-ctx.Done(): |
||||
err = ctx.Err() |
||||
case err = <-errc: |
||||
} |
||||
return Result{ |
||||
Duration: time.Since(start), |
||||
Error: err, |
||||
} |
||||
} |
||||
|
||||
// Maximal number of parallel calls to cleanup functions on
|
||||
// Simulation.Close.
|
||||
var maxParallelCleanups = 10 |
||||
|
||||
// Close calls all cleanup functions that are returned by
|
||||
// ServiceFunc, waits for all of them to finish and other
|
||||
// functions that explicitly block shutdownWG
|
||||
// (like Simulation.PeerEvents) and shuts down the network
|
||||
// at the end. It is used to clean all resources from the
|
||||
// simulation.
|
||||
func (s *Simulation) Close() { |
||||
close(s.done) |
||||
sem := make(chan struct{}, maxParallelCleanups) |
||||
s.mu.RLock() |
||||
cleanupFuncs := make([]func(), len(s.cleanupFuncs)) |
||||
for i, f := range s.cleanupFuncs { |
||||
if f != nil { |
||||
cleanupFuncs[i] = f |
||||
} |
||||
} |
||||
s.mu.RUnlock() |
||||
for _, cleanup := range cleanupFuncs { |
||||
s.shutdownWG.Add(1) |
||||
sem <- struct{}{} |
||||
go func(cleanup func()) { |
||||
defer s.shutdownWG.Done() |
||||
defer func() { <-sem }() |
||||
|
||||
cleanup() |
||||
}(cleanup) |
||||
} |
||||
if s.httpSrv != nil { |
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
||||
defer cancel() |
||||
err := s.httpSrv.Shutdown(ctx) |
||||
if err != nil { |
||||
log.Error("Error shutting down HTTP server!", "err", err) |
||||
} |
||||
close(s.runC) |
||||
} |
||||
s.shutdownWG.Wait() |
||||
s.Net.Shutdown() |
||||
} |
||||
|
||||
// Done returns a channel that is closed when the simulation
|
||||
// is closed by Close method. It is useful for signaling termination
|
||||
// of all possible goroutines that are created within the test.
|
||||
func (s *Simulation) Done() <-chan struct{} { |
||||
return s.done |
||||
} |
@ -0,0 +1,207 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package simulation |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"flag" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/node" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters" |
||||
"github.com/ethereum/go-ethereum/rpc" |
||||
colorable "github.com/mattn/go-colorable" |
||||
) |
||||
|
||||
var ( |
||||
loglevel = flag.Int("loglevel", 2, "verbosity of logs") |
||||
) |
||||
|
||||
func init() { |
||||
flag.Parse() |
||||
log.PrintOrigins(true) |
||||
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) |
||||
} |
||||
|
||||
// TestRun tests if Run method calls RunFunc and if it handles context properly.
|
||||
func TestRun(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
defer sim.Close() |
||||
|
||||
t.Run("call", func(t *testing.T) { |
||||
expect := "something" |
||||
var got string |
||||
r := sim.Run(context.Background(), func(ctx context.Context, sim *Simulation) error { |
||||
got = expect |
||||
return nil |
||||
}) |
||||
|
||||
if r.Error != nil { |
||||
t.Errorf("unexpected error: %v", r.Error) |
||||
} |
||||
if got != expect { |
||||
t.Errorf("expected %q, got %q", expect, got) |
||||
} |
||||
}) |
||||
|
||||
t.Run("cancelation", func(t *testing.T) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) |
||||
defer cancel() |
||||
|
||||
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error { |
||||
time.Sleep(100 * time.Millisecond) |
||||
return nil |
||||
}) |
||||
|
||||
if r.Error != context.DeadlineExceeded { |
||||
t.Errorf("unexpected error: %v", r.Error) |
||||
} |
||||
}) |
||||
|
||||
t.Run("context value and duration", func(t *testing.T) { |
||||
ctx := context.WithValue(context.Background(), "hey", "there") |
||||
sleep := 50 * time.Millisecond |
||||
|
||||
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error { |
||||
if ctx.Value("hey") != "there" { |
||||
return errors.New("expected context value not passed") |
||||
} |
||||
time.Sleep(sleep) |
||||
return nil |
||||
}) |
||||
|
||||
if r.Error != nil { |
||||
t.Errorf("unexpected error: %v", r.Error) |
||||
} |
||||
if r.Duration < sleep { |
||||
t.Errorf("reported run duration less then expected: %s", r.Duration) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// TestClose tests are Close method triggers all close functions and are all nodes not up anymore.
|
||||
func TestClose(t *testing.T) { |
||||
var mu sync.Mutex |
||||
var cleanupCount int |
||||
|
||||
sleep := 50 * time.Millisecond |
||||
|
||||
sim := New(map[string]ServiceFunc{ |
||||
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
return newNoopService(), func() { |
||||
time.Sleep(sleep) |
||||
mu.Lock() |
||||
defer mu.Unlock() |
||||
cleanupCount++ |
||||
}, nil |
||||
}, |
||||
}) |
||||
|
||||
nodeCount := 30 |
||||
|
||||
_, err := sim.AddNodes(nodeCount) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
var upNodeCount int |
||||
for _, n := range sim.Net.GetNodes() { |
||||
if n.Up { |
||||
upNodeCount++ |
||||
} |
||||
} |
||||
if upNodeCount != nodeCount { |
||||
t.Errorf("all nodes should be up, insted only %v are up", upNodeCount) |
||||
} |
||||
|
||||
sim.Close() |
||||
|
||||
if cleanupCount != nodeCount { |
||||
t.Errorf("number of cleanups expected %v, got %v", nodeCount, cleanupCount) |
||||
} |
||||
|
||||
upNodeCount = 0 |
||||
for _, n := range sim.Net.GetNodes() { |
||||
if n.Up { |
||||
upNodeCount++ |
||||
} |
||||
} |
||||
if upNodeCount != 0 { |
||||
t.Errorf("all nodes should be down, insted %v are up", upNodeCount) |
||||
} |
||||
} |
||||
|
||||
// TestDone checks if Close method triggers the closing of done channel.
|
||||
func TestDone(t *testing.T) { |
||||
sim := New(noopServiceFuncMap) |
||||
sleep := 50 * time.Millisecond |
||||
timeout := 2 * time.Second |
||||
|
||||
start := time.Now() |
||||
go func() { |
||||
time.Sleep(sleep) |
||||
sim.Close() |
||||
}() |
||||
|
||||
select { |
||||
case <-time.After(timeout): |
||||
t.Error("done channel closing timmed out") |
||||
case <-sim.Done(): |
||||
if d := time.Since(start); d < sleep { |
||||
t.Errorf("done channel closed sooner then expected: %s", d) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// a helper map for usual services that do not do anyting
|
||||
var noopServiceFuncMap = map[string]ServiceFunc{ |
||||
"noop": noopServiceFunc, |
||||
} |
||||
|
||||
// a helper function for most basic noop service
|
||||
func noopServiceFunc(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) { |
||||
return newNoopService(), nil, nil |
||||
} |
||||
|
||||
// noopService is the service that does not do anything
|
||||
// but implements node.Service interface.
|
||||
type noopService struct{} |
||||
|
||||
func newNoopService() node.Service { |
||||
return &noopService{} |
||||
} |
||||
|
||||
func (t *noopService) Protocols() []p2p.Protocol { |
||||
return []p2p.Protocol{} |
||||
} |
||||
|
||||
func (t *noopService) APIs() []rpc.API { |
||||
return []rpc.API{} |
||||
} |
||||
|
||||
func (t *noopService) Start(server *p2p.Server) error { |
||||
return nil |
||||
} |
||||
|
||||
func (t *noopService) Stop() error { |
||||
return nil |
||||
} |
@ -0,0 +1,61 @@ |
||||
// Package mru defines Mutable resource updates.
|
||||
// A Mutable Resource is an entity which allows updates to a resource
|
||||
// without resorting to ENS on each update.
|
||||
// The update scheme is built on swarm chunks with chunk keys following
|
||||
// a predictable, versionable pattern.
|
||||
//
|
||||
// Updates are defined to be periodic in nature, where the update frequency
|
||||
// is expressed in seconds.
|
||||
//
|
||||
// The root entry of a mutable resource is tied to a unique identifier that
|
||||
// is deterministically generated out of the metadata content that describes
|
||||
// the resource. This metadata includes a user-defined resource name, a resource
|
||||
// start time that indicates when the resource becomes valid,
|
||||
// the frequency in seconds with which the resource is expected to be updated, both of
|
||||
// which are stored as little-endian uint64 values in the database (for a
|
||||
// total of 16 bytes). It also contains the owner's address (ownerAddr)
|
||||
// This MRU info is stored in a separate content-addressed chunk
|
||||
// (call it the metadata chunk), with the following layout:
|
||||
//
|
||||
// (00|length|startTime|frequency|name|ownerAddr)
|
||||
//
|
||||
// (The two first zero-value bytes are used for disambiguation by the chunk validator,
|
||||
// and update chunk will always have a value > 0 there.)
|
||||
//
|
||||
// Each metadata chunk is identified by its rootAddr, calculated as follows:
|
||||
// metaHash=H(len(metadata), startTime, frequency,name)
|
||||
// rootAddr = H(metaHash, ownerAddr).
|
||||
// where H is the SHA3 hash function
|
||||
// This scheme effectively locks the root chunk so that only the owner of the private key
|
||||
// that ownerAddr was derived from can sign updates.
|
||||
//
|
||||
// The root entry tells the requester from when the mutable resource was
|
||||
// first added (Unix time in seconds) and in which moments to look for the
|
||||
// actual updates. Thus, a resource update for identifier "føø.bar"
|
||||
// starting at unix time 1528800000 with frequency 300 (every 5 mins) will have updates on 1528800300,
|
||||
// 1528800600, 1528800900 and so on.
|
||||
//
|
||||
// Actual data updates are also made in the form of swarm chunks. The keys
|
||||
// of the updates are the hash of a concatenation of properties as follows:
|
||||
//
|
||||
// updateAddr = H(period, version, rootAddr)
|
||||
// where H is the SHA3 hash function
|
||||
// The period is (currentTime - startTime) / frequency
|
||||
//
|
||||
// Using our previous example, this means that a period 3 will happen when the
|
||||
// clock hits 1528800900
|
||||
//
|
||||
// If more than one update is made in the same period, incremental
|
||||
// version numbers are used successively.
|
||||
//
|
||||
// A user looking up a resource would only need to know the rootAddr in order to get the versions
|
||||
//
|
||||
// the resource update data is:
|
||||
// resourcedata = headerlength|period|version|rootAddr|flags|metaHash
|
||||
// where flags is a 1-byte flags field. Flag 0 is set to 1 to indicate multihash
|
||||
//
|
||||
// the full update data that goes in the chunk payload is:
|
||||
// resourcedata|sign(resourcedata)
|
||||
//
|
||||
// headerlength is a 16 bit value containing the byte length of period|version|rootAddr|flags|metaHash
|
||||
package mru |
@ -0,0 +1,514 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Handler is the API for Mutable Resources
|
||||
// It enables creating, updating, syncing and retrieving resources and their update data
|
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"fmt" |
||||
"sync" |
||||
"time" |
||||
"unsafe" |
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/log" |
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
const chunkSize = 4096 // temporary until we implement FileStore in the resourcehandler
|
||||
|
||||
type Handler struct { |
||||
chunkStore *storage.NetStore |
||||
HashSize int |
||||
resources map[uint64]*resource |
||||
resourceLock sync.RWMutex |
||||
storeTimeout time.Duration |
||||
queryMaxPeriods uint32 |
||||
} |
||||
|
||||
// HandlerParams pass parameters to the Handler constructor NewHandler
|
||||
// Signer and TimestampProvider are mandatory parameters
|
||||
type HandlerParams struct { |
||||
QueryMaxPeriods uint32 |
||||
} |
||||
|
||||
// hashPool contains a pool of ready hashers
|
||||
var hashPool sync.Pool |
||||
var minimumChunkLength int |
||||
|
||||
// init initializes the package and hashPool
|
||||
func init() { |
||||
hashPool = sync.Pool{ |
||||
New: func() interface{} { |
||||
return storage.MakeHashFunc(resourceHashAlgorithm)() |
||||
}, |
||||
} |
||||
if minimumMetadataLength < minimumUpdateDataLength { |
||||
minimumChunkLength = minimumMetadataLength |
||||
} else { |
||||
minimumChunkLength = minimumUpdateDataLength |
||||
} |
||||
} |
||||
|
||||
// NewHandler creates a new Mutable Resource API
|
||||
func NewHandler(params *HandlerParams) (*Handler, error) { |
||||
|
||||
rh := &Handler{ |
||||
resources: make(map[uint64]*resource), |
||||
storeTimeout: defaultStoreTimeout, |
||||
queryMaxPeriods: params.QueryMaxPeriods, |
||||
} |
||||
|
||||
for i := 0; i < hasherCount; i++ { |
||||
hashfunc := storage.MakeHashFunc(resourceHashAlgorithm)() |
||||
if rh.HashSize == 0 { |
||||
rh.HashSize = hashfunc.Size() |
||||
} |
||||
hashPool.Put(hashfunc) |
||||
} |
||||
|
||||
return rh, nil |
||||
} |
||||
|
||||
// SetStore sets the store backend for the Mutable Resource API
|
||||
func (h *Handler) SetStore(store *storage.NetStore) { |
||||
h.chunkStore = store |
||||
} |
||||
|
||||
// Validate is a chunk validation method
|
||||
// If it looks like a resource update, the chunk address is checked against the ownerAddr of the update's signature
|
||||
// It implements the storage.ChunkValidator interface
|
||||
func (h *Handler) Validate(chunkAddr storage.Address, data []byte) bool { |
||||
|
||||
dataLength := len(data) |
||||
if dataLength < minimumChunkLength { |
||||
return false |
||||
} |
||||
|
||||
//metadata chunks have the first two bytes set to zero
|
||||
if data[0] == 0 && data[1] == 0 && dataLength >= minimumMetadataLength { |
||||
//metadata chunk
|
||||
rootAddr, _ := metadataHash(data) |
||||
valid := bytes.Equal(chunkAddr, rootAddr) |
||||
if !valid { |
||||
log.Debug(fmt.Sprintf("Invalid root metadata chunk with address: %s", chunkAddr.Hex())) |
||||
} |
||||
return valid |
||||
} |
||||
|
||||
// if it is not a metadata chunk, check if it is a properly formatted update chunk with
|
||||
// valid signature and proof of ownership of the resource it is trying
|
||||
// to update
|
||||
|
||||
// First, deserialize the chunk
|
||||
var r SignedResourceUpdate |
||||
if err := r.fromChunk(chunkAddr, data); err != nil { |
||||
log.Debug("Invalid resource chunk with address %s: %s ", chunkAddr.Hex(), err.Error()) |
||||
return false |
||||
} |
||||
|
||||
// check that the lookup information contained in the chunk matches the updateAddr (chunk search key)
|
||||
// that was used to retrieve this chunk
|
||||
// if this validation fails, someone forged a chunk.
|
||||
if !bytes.Equal(chunkAddr, r.updateHeader.UpdateAddr()) { |
||||
log.Debug("period,version,rootAddr contained in update chunk do not match updateAddr %s", chunkAddr.Hex()) |
||||
return false |
||||
} |
||||
|
||||
// Verify signatures and that the signer actually owns the resource
|
||||
// If it fails, it means either the signature is not valid, data is corrupted
|
||||
// or someone is trying to update someone else's resource.
|
||||
if err := r.Verify(); err != nil { |
||||
log.Debug("Invalid signature: %v", err) |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
// GetContent retrieves the data payload of the last synced update of the Mutable Resource
|
||||
func (h *Handler) GetContent(rootAddr storage.Address) (storage.Address, []byte, error) { |
||||
rsrc := h.get(rootAddr) |
||||
if rsrc == nil || !rsrc.isSynced() { |
||||
return nil, nil, NewError(ErrNotFound, " does not exist or is not synced") |
||||
} |
||||
return rsrc.lastKey, rsrc.data, nil |
||||
} |
||||
|
||||
// GetLastPeriod retrieves the period of the last synced update of the Mutable Resource
|
||||
func (h *Handler) GetLastPeriod(rootAddr storage.Address) (uint32, error) { |
||||
rsrc := h.get(rootAddr) |
||||
if rsrc == nil { |
||||
return 0, NewError(ErrNotFound, " does not exist") |
||||
} else if !rsrc.isSynced() { |
||||
return 0, NewError(ErrNotSynced, " is not synced") |
||||
} |
||||
return rsrc.period, nil |
||||
} |
||||
|
||||
// GetVersion retrieves the period of the last synced update of the Mutable Resource
|
||||
func (h *Handler) GetVersion(rootAddr storage.Address) (uint32, error) { |
||||
rsrc := h.get(rootAddr) |
||||
if rsrc == nil { |
||||
return 0, NewError(ErrNotFound, " does not exist") |
||||
} else if !rsrc.isSynced() { |
||||
return 0, NewError(ErrNotSynced, " is not synced") |
||||
} |
||||
return rsrc.version, nil |
||||
} |
||||
|
||||
// \TODO should be hashsize * branches from the chosen chunker, implement with FileStore
|
||||
func (h *Handler) chunkSize() int64 { |
||||
return chunkSize |
||||
} |
||||
|
||||
// New creates a new metadata chunk out of the request passed in.
|
||||
func (h *Handler) New(ctx context.Context, request *Request) error { |
||||
|
||||
// frequency 0 is invalid
|
||||
if request.metadata.Frequency == 0 { |
||||
return NewError(ErrInvalidValue, "frequency cannot be 0 when creating a resource") |
||||
} |
||||
|
||||
// make sure owner is set to something
|
||||
if request.metadata.Owner == zeroAddr { |
||||
return NewError(ErrInvalidValue, "ownerAddr must be set to create a new metadata chunk") |
||||
} |
||||
|
||||
// create the meta chunk and store it in swarm
|
||||
chunk, metaHash, err := request.metadata.newChunk() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if request.metaHash != nil && !bytes.Equal(request.metaHash, metaHash) || |
||||
request.rootAddr != nil && !bytes.Equal(request.rootAddr, chunk.Addr) { |
||||
return NewError(ErrInvalidValue, "metaHash in UpdateRequest does not match actual metadata") |
||||
} |
||||
|
||||
request.metaHash = metaHash |
||||
request.rootAddr = chunk.Addr |
||||
|
||||
h.chunkStore.Put(ctx, chunk) |
||||
log.Debug("new resource", "name", request.metadata.Name, "startTime", request.metadata.StartTime, "frequency", request.metadata.Frequency, "owner", request.metadata.Owner) |
||||
|
||||
// create the internal index for the resource and populate it with its metadata
|
||||
rsrc := &resource{ |
||||
resourceUpdate: resourceUpdate{ |
||||
updateHeader: updateHeader{ |
||||
UpdateLookup: UpdateLookup{ |
||||
rootAddr: chunk.Addr, |
||||
}, |
||||
}, |
||||
}, |
||||
ResourceMetadata: request.metadata, |
||||
updated: time.Now(), |
||||
} |
||||
h.set(chunk.Addr, rsrc) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// NewUpdateRequest prepares an UpdateRequest structure with all the necessary information to
|
||||
// just add the desired data and sign it.
|
||||
// The resulting structure can then be signed and passed to Handler.Update to be verified and sent
|
||||
func (h *Handler) NewUpdateRequest(ctx context.Context, rootAddr storage.Address) (updateRequest *Request, err error) { |
||||
|
||||
if rootAddr == nil { |
||||
return nil, NewError(ErrInvalidValue, "rootAddr cannot be nil") |
||||
} |
||||
|
||||
// Make sure we have a cache of the metadata chunk
|
||||
rsrc, err := h.Load(ctx, rootAddr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
now := TimestampProvider.Now() |
||||
|
||||
updateRequest = new(Request) |
||||
updateRequest.period, err = getNextPeriod(rsrc.StartTime.Time, now.Time, rsrc.Frequency) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if _, err = h.lookup(rsrc, LookupLatestVersionInPeriod(rsrc.rootAddr, updateRequest.period)); err != nil { |
||||
if err.(*Error).code != ErrNotFound { |
||||
return nil, err |
||||
} |
||||
// not finding updates means that there is a network error
|
||||
// or that the resource really does not have updates in this period.
|
||||
} |
||||
|
||||
updateRequest.multihash = rsrc.multihash |
||||
updateRequest.rootAddr = rsrc.rootAddr |
||||
updateRequest.metaHash = rsrc.metaHash |
||||
updateRequest.metadata = rsrc.ResourceMetadata |
||||
|
||||
// if we already have an update for this period then increment version
|
||||
// resource object MUST be in sync for version to be correct, but we checked this earlier in the method already
|
||||
if h.hasUpdate(rootAddr, updateRequest.period) { |
||||
updateRequest.version = rsrc.version + 1 |
||||
} else { |
||||
updateRequest.version = 1 |
||||
} |
||||
|
||||
return updateRequest, nil |
||||
} |
||||
|
||||
// Lookup retrieves a specific or latest version of the resource update with metadata chunk at params.Root
|
||||
// Lookup works differently depending on the configuration of `LookupParams`
|
||||
// See the `LookupParams` documentation and helper functions:
|
||||
// `LookupLatest`, `LookupLatestVersionInPeriod` and `LookupVersion`
|
||||
// When looking for the latest update, it starts at the next period after the current time.
|
||||
// upon failure tries the corresponding keys of each previous period until one is found
|
||||
// (or startTime is reached, in which case there are no updates).
|
||||
func (h *Handler) Lookup(ctx context.Context, params *LookupParams) (*resource, error) { |
||||
|
||||
rsrc := h.get(params.rootAddr) |
||||
if rsrc == nil { |
||||
return nil, NewError(ErrNothingToReturn, "resource not loaded") |
||||
} |
||||
return h.lookup(rsrc, params) |
||||
} |
||||
|
||||
// LookupPrevious returns the resource before the one currently loaded in the resource cache
|
||||
// This is useful where resource updates are used incrementally in contrast to
|
||||
// merely replacing content.
|
||||
// Requires a cached resource object to determine the current state of the resource.
|
||||
func (h *Handler) LookupPrevious(ctx context.Context, params *LookupParams) (*resource, error) { |
||||
rsrc := h.get(params.rootAddr) |
||||
if rsrc == nil { |
||||
return nil, NewError(ErrNothingToReturn, "resource not loaded") |
||||
} |
||||
if !rsrc.isSynced() { |
||||
return nil, NewError(ErrNotSynced, "LookupPrevious requires synced resource.") |
||||
} else if rsrc.period == 0 { |
||||
return nil, NewError(ErrNothingToReturn, " not found") |
||||
} |
||||
var version, period uint32 |
||||
if rsrc.version > 1 { |
||||
version = rsrc.version - 1 |
||||
period = rsrc.period |
||||
} else if rsrc.period == 1 { |
||||
return nil, NewError(ErrNothingToReturn, "Current update is the oldest") |
||||
} else { |
||||
version = 0 |
||||
period = rsrc.period - 1 |
||||
} |
||||
return h.lookup(rsrc, NewLookupParams(rsrc.rootAddr, period, version, params.Limit)) |
||||
} |
||||
|
||||
// base code for public lookup methods
|
||||
func (h *Handler) lookup(rsrc *resource, params *LookupParams) (*resource, error) { |
||||
|
||||
lp := *params |
||||
// we can't look for anything without a store
|
||||
if h.chunkStore == nil { |
||||
return nil, NewError(ErrInit, "Call Handler.SetStore() before performing lookups") |
||||
} |
||||
|
||||
var specificperiod bool |
||||
if lp.period > 0 { |
||||
specificperiod = true |
||||
} else { |
||||
// get the current time and the next period
|
||||
now := TimestampProvider.Now() |
||||
|
||||
var period uint32 |
||||
period, err := getNextPeriod(rsrc.StartTime.Time, now.Time, rsrc.Frequency) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
lp.period = period |
||||
} |
||||
|
||||
// start from the last possible period, and iterate previous ones
|
||||
// (unless we want a specific period only) until we find a match.
|
||||
// If we hit startTime we're out of options
|
||||
var specificversion bool |
||||
if lp.version > 0 { |
||||
specificversion = true |
||||
} else { |
||||
lp.version = 1 |
||||
} |
||||
|
||||
var hops uint32 |
||||
if lp.Limit == 0 { |
||||
lp.Limit = h.queryMaxPeriods |
||||
} |
||||
log.Trace("resource lookup", "period", lp.period, "version", lp.version, "limit", lp.Limit) |
||||
for lp.period > 0 { |
||||
if lp.Limit != 0 && hops > lp.Limit { |
||||
return nil, NewErrorf(ErrPeriodDepth, "Lookup exceeded max period hops (%d)", lp.Limit) |
||||
} |
||||
updateAddr := lp.UpdateAddr() |
||||
chunk, err := h.chunkStore.GetWithTimeout(context.TODO(), updateAddr, defaultRetrieveTimeout) |
||||
if err == nil { |
||||
if specificversion { |
||||
return h.updateIndex(rsrc, chunk) |
||||
} |
||||
// check if we have versions > 1. If a version fails, the previous version is used and returned.
|
||||
log.Trace("rsrc update version 1 found, checking for version updates", "period", lp.period, "updateAddr", updateAddr) |
||||
for { |
||||
newversion := lp.version + 1 |
||||
updateAddr := lp.UpdateAddr() |
||||
newchunk, err := h.chunkStore.GetWithTimeout(context.TODO(), updateAddr, defaultRetrieveTimeout) |
||||
if err != nil { |
||||
return h.updateIndex(rsrc, chunk) |
||||
} |
||||
chunk = newchunk |
||||
lp.version = newversion |
||||
log.Trace("version update found, checking next", "version", lp.version, "period", lp.period, "updateAddr", updateAddr) |
||||
} |
||||
} |
||||
if specificperiod { |
||||
break |
||||
} |
||||
log.Trace("rsrc update not found, checking previous period", "period", lp.period, "updateAddr", updateAddr) |
||||
lp.period-- |
||||
hops++ |
||||
} |
||||
return nil, NewError(ErrNotFound, "no updates found") |
||||
} |
||||
|
||||
// Load retrieves the Mutable Resource metadata chunk stored at rootAddr
|
||||
// Upon retrieval it creates/updates the index entry for it with metadata corresponding to the chunk contents
|
||||
func (h *Handler) Load(ctx context.Context, rootAddr storage.Address) (*resource, error) { |
||||
chunk, err := h.chunkStore.GetWithTimeout(ctx, rootAddr, defaultRetrieveTimeout) |
||||
if err != nil { |
||||
return nil, NewError(ErrNotFound, err.Error()) |
||||
} |
||||
|
||||
// create the index entry
|
||||
rsrc := &resource{} |
||||
|
||||
if err := rsrc.ResourceMetadata.binaryGet(chunk.SData); err != nil { // Will fail if this is not really a metadata chunk
|
||||
return nil, err |
||||
} |
||||
|
||||
rsrc.rootAddr, rsrc.metaHash = metadataHash(chunk.SData) |
||||
if !bytes.Equal(rsrc.rootAddr, rootAddr) { |
||||
return nil, NewError(ErrCorruptData, "Corrupt metadata chunk") |
||||
} |
||||
h.set(rootAddr, rsrc) |
||||
log.Trace("resource index load", "rootkey", rootAddr, "name", rsrc.ResourceMetadata.Name, "starttime", rsrc.ResourceMetadata.StartTime, "frequency", rsrc.ResourceMetadata.Frequency) |
||||
return rsrc, nil |
||||
} |
||||
|
||||
// update mutable resource index map with specified content
|
||||
func (h *Handler) updateIndex(rsrc *resource, chunk *storage.Chunk) (*resource, error) { |
||||
|
||||
// retrieve metadata from chunk data and check that it matches this mutable resource
|
||||
var r SignedResourceUpdate |
||||
if err := r.fromChunk(chunk.Addr, chunk.SData); err != nil { |
||||
return nil, err |
||||
} |
||||
log.Trace("resource index update", "name", rsrc.ResourceMetadata.Name, "updatekey", chunk.Addr, "period", r.period, "version", r.version) |
||||
|
||||
// update our rsrcs entry map
|
||||
rsrc.lastKey = chunk.Addr |
||||
rsrc.period = r.period |
||||
rsrc.version = r.version |
||||
rsrc.updated = time.Now() |
||||
rsrc.data = make([]byte, len(r.data)) |
||||
rsrc.multihash = r.multihash |
||||
copy(rsrc.data, r.data) |
||||
rsrc.Reader = bytes.NewReader(rsrc.data) |
||||
log.Debug("resource synced", "name", rsrc.ResourceMetadata.Name, "updateAddr", chunk.Addr, "period", rsrc.period, "version", rsrc.version) |
||||
h.set(chunk.Addr, rsrc) |
||||
return rsrc, nil |
||||
} |
||||
|
||||
// Update adds an actual data update
|
||||
// Uses the Mutable Resource metadata currently loaded in the resources map entry.
|
||||
// It is the caller's responsibility to make sure that this data is not stale.
|
||||
// Note that a Mutable Resource update cannot span chunks, and thus has a MAX NET LENGTH 4096, INCLUDING update header data and signature. An error will be returned if the total length of the chunk payload will exceed this limit.
|
||||
// Update can only check if the caller is trying to overwrite the very last known version, otherwise it just puts the update
|
||||
// on the network.
|
||||
func (h *Handler) Update(ctx context.Context, r *SignedResourceUpdate) (storage.Address, error) { |
||||
return h.update(ctx, r) |
||||
} |
||||
|
||||
// create and commit an update
|
||||
func (h *Handler) update(ctx context.Context, r *SignedResourceUpdate) (updateAddr storage.Address, err error) { |
||||
|
||||
// we can't update anything without a store
|
||||
if h.chunkStore == nil { |
||||
return nil, NewError(ErrInit, "Call Handler.SetStore() before updating") |
||||
} |
||||
|
||||
rsrc := h.get(r.rootAddr) |
||||
if rsrc != nil && rsrc.period != 0 && rsrc.version != 0 && // This is the only cheap check we can do for sure
|
||||
rsrc.period == r.period && rsrc.version >= r.version { // without having to lookup update chunks
|
||||
|
||||
return nil, NewError(ErrInvalidValue, "A former update in this period is already known to exist") |
||||
} |
||||
|
||||
chunk, err := r.toChunk() // Serialize the update into a chunk. Fails if data is too big
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// send the chunk
|
||||
h.chunkStore.Put(ctx, chunk) |
||||
log.Trace("resource update", "updateAddr", r.updateAddr, "lastperiod", r.period, "version", r.version, "data", chunk.SData, "multihash", r.multihash) |
||||
|
||||
// update our resources map entry if the new update is older than the one we have, if we have it.
|
||||
if rsrc != nil && r.period > rsrc.period || (rsrc.period == r.period && r.version > rsrc.version) { |
||||
rsrc.period = r.period |
||||
rsrc.version = r.version |
||||
rsrc.data = make([]byte, len(r.data)) |
||||
rsrc.updated = time.Now() |
||||
rsrc.lastKey = r.updateAddr |
||||
rsrc.multihash = r.multihash |
||||
copy(rsrc.data, r.data) |
||||
rsrc.Reader = bytes.NewReader(rsrc.data) |
||||
} |
||||
return r.updateAddr, nil |
||||
} |
||||
|
||||
// Retrieves the resource index value for the given nameHash
|
||||
func (h *Handler) get(rootAddr storage.Address) *resource { |
||||
if len(rootAddr) < storage.KeyLength { |
||||
log.Warn("Handler.get with invalid rootAddr") |
||||
return nil |
||||
} |
||||
hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0])) |
||||
h.resourceLock.RLock() |
||||
defer h.resourceLock.RUnlock() |
||||
rsrc := h.resources[hashKey] |
||||
return rsrc |
||||
} |
||||
|
||||
// Sets the resource index value for the given nameHash
|
||||
func (h *Handler) set(rootAddr storage.Address, rsrc *resource) { |
||||
if len(rootAddr) < storage.KeyLength { |
||||
log.Warn("Handler.set with invalid rootAddr") |
||||
return |
||||
} |
||||
hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0])) |
||||
h.resourceLock.Lock() |
||||
defer h.resourceLock.Unlock() |
||||
h.resources[hashKey] = rsrc |
||||
} |
||||
|
||||
// Checks if we already have an update on this resource, according to the value in the current state of the resource index
|
||||
func (h *Handler) hasUpdate(rootAddr storage.Address, period uint32) bool { |
||||
rsrc := h.get(rootAddr) |
||||
return rsrc != nil && rsrc.period == period |
||||
} |
@ -0,0 +1,117 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"hash" |
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
// LookupParams is used to specify constraints when performing an update lookup
|
||||
// Limit defines whether or not the lookup should be limited
|
||||
// If Limit is set to true then Max defines the amount of hops that can be performed
|
||||
type LookupParams struct { |
||||
UpdateLookup |
||||
Limit uint32 |
||||
} |
||||
|
||||
// RootAddr returns the metadata chunk address
|
||||
func (r *LookupParams) RootAddr() storage.Address { |
||||
return r.rootAddr |
||||
} |
||||
|
||||
func NewLookupParams(rootAddr storage.Address, period, version uint32, limit uint32) *LookupParams { |
||||
return &LookupParams{ |
||||
UpdateLookup: UpdateLookup{ |
||||
period: period, |
||||
version: version, |
||||
rootAddr: rootAddr, |
||||
}, |
||||
Limit: limit, |
||||
} |
||||
} |
||||
|
||||
// LookupLatest generates lookup parameters that look for the latest version of a resource
|
||||
func LookupLatest(rootAddr storage.Address) *LookupParams { |
||||
return NewLookupParams(rootAddr, 0, 0, 0) |
||||
} |
||||
|
||||
// LookupLatestVersionInPeriod generates lookup parameters that look for the latest version of a resource in a given period
|
||||
func LookupLatestVersionInPeriod(rootAddr storage.Address, period uint32) *LookupParams { |
||||
return NewLookupParams(rootAddr, period, 0, 0) |
||||
} |
||||
|
||||
// LookupVersion generates lookup parameters that look for a specific version of a resource
|
||||
func LookupVersion(rootAddr storage.Address, period, version uint32) *LookupParams { |
||||
return NewLookupParams(rootAddr, period, version, 0) |
||||
} |
||||
|
||||
// UpdateLookup represents the components of a resource update search key
|
||||
type UpdateLookup struct { |
||||
period uint32 |
||||
version uint32 |
||||
rootAddr storage.Address |
||||
} |
||||
|
||||
// 4 bytes period
|
||||
// 4 bytes version
|
||||
// storage.Keylength for rootAddr
|
||||
const updateLookupLength = 4 + 4 + storage.KeyLength |
||||
|
||||
// UpdateAddr calculates the resource update chunk address corresponding to this lookup key
|
||||
func (u *UpdateLookup) UpdateAddr() (updateAddr storage.Address) { |
||||
serializedData := make([]byte, updateLookupLength) |
||||
u.binaryPut(serializedData) |
||||
hasher := hashPool.Get().(hash.Hash) |
||||
defer hashPool.Put(hasher) |
||||
hasher.Reset() |
||||
hasher.Write(serializedData) |
||||
return hasher.Sum(nil) |
||||
} |
||||
|
||||
// binaryPut serializes this UpdateLookup instance into the provided slice
|
||||
func (u *UpdateLookup) binaryPut(serializedData []byte) error { |
||||
if len(serializedData) != updateLookupLength { |
||||
return NewErrorf(ErrInvalidValue, "Incorrect slice size to serialize UpdateLookup. Expected %d, got %d", updateLookupLength, len(serializedData)) |
||||
} |
||||
if len(u.rootAddr) != storage.KeyLength { |
||||
return NewError(ErrInvalidValue, "UpdateLookup.binaryPut called without rootAddr set") |
||||
} |
||||
binary.LittleEndian.PutUint32(serializedData[:4], u.period) |
||||
binary.LittleEndian.PutUint32(serializedData[4:8], u.version) |
||||
copy(serializedData[8:], u.rootAddr[:]) |
||||
return nil |
||||
} |
||||
|
||||
// binaryLength returns the expected size of this structure when serialized
|
||||
func (u *UpdateLookup) binaryLength() int { |
||||
return updateLookupLength |
||||
} |
||||
|
||||
// binaryGet restores the current instance from the information contained in the passed slice
|
||||
func (u *UpdateLookup) binaryGet(serializedData []byte) error { |
||||
if len(serializedData) != updateLookupLength { |
||||
return NewErrorf(ErrInvalidValue, "Incorrect slice size to read UpdateLookup. Expected %d, got %d", updateLookupLength, len(serializedData)) |
||||
} |
||||
u.period = binary.LittleEndian.Uint32(serializedData[:4]) |
||||
u.version = binary.LittleEndian.Uint32(serializedData[4:8]) |
||||
u.rootAddr = storage.Address(make([]byte, storage.KeyLength)) |
||||
copy(u.rootAddr[:], serializedData[8:]) |
||||
return nil |
||||
} |
@ -0,0 +1,85 @@ |
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
) |
||||
|
||||
func getTestUpdateLookup() *UpdateLookup { |
||||
metadata := *getTestMetadata() |
||||
rootAddr, _, _, _ := metadata.serializeAndHash() |
||||
return &UpdateLookup{ |
||||
period: 79, |
||||
version: 2010, |
||||
rootAddr: rootAddr, |
||||
} |
||||
} |
||||
|
||||
func compareUpdateLookup(a, b *UpdateLookup) bool { |
||||
return a.version == b.version && |
||||
a.period == b.period && |
||||
bytes.Equal(a.rootAddr, b.rootAddr) |
||||
} |
||||
|
||||
func TestUpdateLookupUpdateAddr(t *testing.T) { |
||||
ul := getTestUpdateLookup() |
||||
updateAddr := ul.UpdateAddr() |
||||
compareByteSliceToExpectedHex(t, "updateAddr", updateAddr, "0x8fbc8d4777ef6da790257eda80ab4321fabd08cbdbe67e4e3da6caca386d64e0") |
||||
} |
||||
|
||||
func TestUpdateLookupSerializer(t *testing.T) { |
||||
serializedUpdateLookup := make([]byte, updateLookupLength) |
||||
ul := getTestUpdateLookup() |
||||
if err := ul.binaryPut(serializedUpdateLookup); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
compareByteSliceToExpectedHex(t, "serializedUpdateLookup", serializedUpdateLookup, "0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb") |
||||
|
||||
// set receiving slice to the wrong size
|
||||
serializedUpdateLookup = make([]byte, updateLookupLength+7) |
||||
if err := ul.binaryPut(serializedUpdateLookup); err == nil { |
||||
t.Fatalf("Expected UpdateLookup.binaryPut to fail when receiving slice has a length != %d", updateLookupLength) |
||||
} |
||||
|
||||
// set rootAddr to an invalid length
|
||||
ul.rootAddr = []byte{1, 2, 3, 4} |
||||
serializedUpdateLookup = make([]byte, updateLookupLength) |
||||
if err := ul.binaryPut(serializedUpdateLookup); err == nil { |
||||
t.Fatal("Expected UpdateLookup.binaryPut to fail when rootAddr is not of the correct size") |
||||
} |
||||
} |
||||
|
||||
func TestUpdateLookupDeserializer(t *testing.T) { |
||||
serializedUpdateLookup, _ := hexutil.Decode("0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb") |
||||
var recoveredUpdateLookup UpdateLookup |
||||
if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
originalUpdateLookup := *getTestUpdateLookup() |
||||
if !compareUpdateLookup(&originalUpdateLookup, &recoveredUpdateLookup) { |
||||
t.Fatalf("Expected recovered UpdateLookup to match") |
||||
} |
||||
|
||||
// set source slice to the wrong size
|
||||
serializedUpdateLookup = make([]byte, updateLookupLength+4) |
||||
if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err == nil { |
||||
t.Fatalf("Expected UpdateLookup.binaryGet to fail when source slice has a length != %d", updateLookupLength) |
||||
} |
||||
} |
||||
|
||||
func TestUpdateLookupSerializeDeserialize(t *testing.T) { |
||||
serializedUpdateLookup := make([]byte, updateLookupLength) |
||||
originalUpdateLookup := getTestUpdateLookup() |
||||
if err := originalUpdateLookup.binaryPut(serializedUpdateLookup); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
var recoveredUpdateLookup UpdateLookup |
||||
if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !compareUpdateLookup(originalUpdateLookup, &recoveredUpdateLookup) { |
||||
t.Fatalf("Expected recovered UpdateLookup to match") |
||||
} |
||||
} |
@ -0,0 +1,189 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"hash" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
// ResourceMetadata encapsulates the immutable information about a mutable resource :)
|
||||
// once serialized into a chunk, the resource can be retrieved by knowing its content-addressed rootAddr
|
||||
type ResourceMetadata struct { |
||||
StartTime Timestamp // time at which the resource starts to be valid
|
||||
Frequency uint64 // expected update frequency for the resource
|
||||
Name string // name of the resource, for the reference of the user or to disambiguate resources with same starttime, frequency, owneraddr
|
||||
Owner common.Address // public address of the resource owner
|
||||
} |
||||
|
||||
const frequencyLength = 8 // sizeof(uint64)
|
||||
const nameLengthLength = 1 |
||||
|
||||
// Resource metadata chunk layout:
|
||||
// 4 prefix bytes (chunkPrefixLength). The first two set to zero. The second two indicate the length
|
||||
// Timestamp: timestampLength bytes
|
||||
// frequency: frequencyLength bytes
|
||||
// name length: nameLengthLength bytes
|
||||
// name (variable length, can be empty, up to 255 bytes)
|
||||
// ownerAddr: common.AddressLength
|
||||
const minimumMetadataLength = chunkPrefixLength + timestampLength + frequencyLength + nameLengthLength + common.AddressLength |
||||
|
||||
// binaryGet populates the resource metadata from a byte array
|
||||
func (r *ResourceMetadata) binaryGet(serializedData []byte) error { |
||||
if len(serializedData) < minimumMetadataLength { |
||||
return NewErrorf(ErrInvalidValue, "Metadata chunk to deserialize is too short. Expected at least %d. Got %d.", minimumMetadataLength, len(serializedData)) |
||||
} |
||||
|
||||
// first two bytes must be set to zero to indicate metadata chunks, so enforce this.
|
||||
if serializedData[0] != 0 || serializedData[1] != 0 { |
||||
return NewError(ErrCorruptData, "Invalid metadata chunk") |
||||
} |
||||
|
||||
cursor := 2 |
||||
metadataLength := int(binary.LittleEndian.Uint16(serializedData[cursor : cursor+2])) // metadataLength does not include the 4 prefix bytes
|
||||
if metadataLength+chunkPrefixLength != len(serializedData) { |
||||
return NewErrorf(ErrCorruptData, "Incorrect declared metadata length. Expected %d, got %d.", metadataLength+chunkPrefixLength, len(serializedData)) |
||||
} |
||||
|
||||
cursor += 2 |
||||
|
||||
if err := r.StartTime.binaryGet(serializedData[cursor : cursor+timestampLength]); err != nil { |
||||
return err |
||||
} |
||||
cursor += timestampLength |
||||
|
||||
r.Frequency = binary.LittleEndian.Uint64(serializedData[cursor : cursor+frequencyLength]) |
||||
cursor += frequencyLength |
||||
|
||||
nameLength := int(serializedData[cursor]) |
||||
if nameLength+minimumMetadataLength > len(serializedData) { |
||||
return NewErrorf(ErrInvalidValue, "Metadata chunk to deserialize is too short when decoding resource name. Expected at least %d. Got %d.", nameLength+minimumMetadataLength, len(serializedData)) |
||||
} |
||||
cursor++ |
||||
r.Name = string(serializedData[cursor : cursor+nameLength]) |
||||
cursor += nameLength |
||||
|
||||
copy(r.Owner[:], serializedData[cursor:]) |
||||
cursor += common.AddressLength |
||||
if cursor != len(serializedData) { |
||||
return NewErrorf(ErrInvalidValue, "Metadata chunk has leftover data after deserialization. %d left to read", len(serializedData)-cursor) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// binaryPut encodes the metadata into a byte array
|
||||
func (r *ResourceMetadata) binaryPut(serializedData []byte) error { |
||||
metadataChunkLength := r.binaryLength() |
||||
if len(serializedData) != metadataChunkLength { |
||||
return NewErrorf(ErrInvalidValue, "Need a slice of exactly %d bytes to serialize this metadata, but got a slice of size %d.", metadataChunkLength, len(serializedData)) |
||||
} |
||||
|
||||
// root chunk has first two bytes both set to 0, which distinguishes from update bytes
|
||||
// therefore, skip the first two bytes of a zero-initialized array.
|
||||
cursor := 2 |
||||
binary.LittleEndian.PutUint16(serializedData[cursor:cursor+2], uint16(metadataChunkLength-chunkPrefixLength)) // metadataLength does not include the 4 prefix bytes
|
||||
cursor += 2 |
||||
|
||||
r.StartTime.binaryPut(serializedData[cursor : cursor+timestampLength]) |
||||
cursor += timestampLength |
||||
|
||||
binary.LittleEndian.PutUint64(serializedData[cursor:cursor+frequencyLength], r.Frequency) |
||||
cursor += frequencyLength |
||||
|
||||
// Encode the name string as a 1 byte length followed by the encoded string.
|
||||
// Longer strings will be truncated.
|
||||
nameLength := len(r.Name) |
||||
if nameLength > 255 { |
||||
nameLength = 255 |
||||
} |
||||
serializedData[cursor] = uint8(nameLength) |
||||
cursor++ |
||||
copy(serializedData[cursor:cursor+nameLength], []byte(r.Name[:nameLength])) |
||||
cursor += nameLength |
||||
|
||||
copy(serializedData[cursor:cursor+common.AddressLength], r.Owner[:]) |
||||
cursor += common.AddressLength |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (r *ResourceMetadata) binaryLength() int { |
||||
return minimumMetadataLength + len(r.Name) |
||||
} |
||||
|
||||
// serializeAndHash returns the root chunk addr and metadata hash that help identify and ascertain ownership of this resource
|
||||
// returns the serialized metadata as a byproduct of having to hash it.
|
||||
func (r *ResourceMetadata) serializeAndHash() (rootAddr, metaHash []byte, chunkData []byte, err error) { |
||||
|
||||
chunkData = make([]byte, r.binaryLength()) |
||||
if err := r.binaryPut(chunkData); err != nil { |
||||
return nil, nil, nil, err |
||||
} |
||||
rootAddr, metaHash = metadataHash(chunkData) |
||||
return rootAddr, metaHash, chunkData, nil |
||||
|
||||
} |
||||
|
||||
// creates a metadata chunk out of a resourceMetadata structure
|
||||
func (metadata *ResourceMetadata) newChunk() (chunk *storage.Chunk, metaHash []byte, err error) { |
||||
// the metadata chunk contains a timestamp of when the resource starts to be valid
|
||||
// and also how frequently it is expected to be updated
|
||||
// from this we know at what time we should look for updates, and how often
|
||||
// it also contains the name of the resource, so we know what resource we are working with
|
||||
|
||||
// the key (rootAddr) of the metadata chunk is content-addressed
|
||||
// if it wasn't we couldn't replace it later
|
||||
// resolving this relationship is left up to external agents (for example ENS)
|
||||
rootAddr, metaHash, chunkData, err := metadata.serializeAndHash() |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
// make the chunk and send it to swarm
|
||||
chunk = storage.NewChunk(rootAddr, nil) |
||||
chunk.SData = chunkData |
||||
chunk.Size = int64(len(chunkData)) |
||||
|
||||
return chunk, metaHash, nil |
||||
} |
||||
|
||||
// metadataHash returns the metadata chunk root address and metadata hash
|
||||
// that help identify and ascertain ownership of this resource
|
||||
// We compute it as rootAddr = H(ownerAddr, H(metadata))
|
||||
// Where H() is SHA3
|
||||
// metadata are all the metadata fields, except ownerAddr
|
||||
// ownerAddr is the public address of the resource owner
|
||||
// Update chunks must carry a rootAddr reference and metaHash in order to be verified
|
||||
// This way, a node that receives an update can check the signature, recover the public address
|
||||
// and check the ownership by computing H(ownerAddr, metaHash) and comparing it to the rootAddr
|
||||
// the resource is claiming to update without having to lookup the metadata chunk.
|
||||
// see verifyResourceOwnerhsip in signedupdate.go
|
||||
func metadataHash(chunkData []byte) (rootAddr, metaHash []byte) { |
||||
hasher := hashPool.Get().(hash.Hash) |
||||
defer hashPool.Put(hasher) |
||||
hasher.Reset() |
||||
hasher.Write(chunkData[:len(chunkData)-common.AddressLength]) |
||||
metaHash = hasher.Sum(nil) |
||||
hasher.Reset() |
||||
hasher.Write(metaHash) |
||||
hasher.Write(chunkData[len(chunkData)-common.AddressLength:]) |
||||
rootAddr = hasher.Sum(nil) |
||||
return |
||||
} |
@ -0,0 +1,126 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
package mru |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
) |
||||
|
||||
func compareByteSliceToExpectedHex(t *testing.T, variableName string, actualValue []byte, expectedHex string) { |
||||
if hexutil.Encode(actualValue) != expectedHex { |
||||
t.Fatalf("%s: Expected %s to be %s, got %s", t.Name(), variableName, expectedHex, hexutil.Encode(actualValue)) |
||||
} |
||||
} |
||||
|
||||
func getTestMetadata() *ResourceMetadata { |
||||
return &ResourceMetadata{ |
||||
Name: "world news report, every hour, on the hour", |
||||
StartTime: Timestamp{ |
||||
Time: 1528880400, |
||||
}, |
||||
Frequency: 3600, |
||||
Owner: newCharlieSigner().Address(), |
||||
} |
||||
} |
||||
|
||||
func TestMetadataSerializerDeserializer(t *testing.T) { |
||||
metadata := *getTestMetadata() |
||||
|
||||
rootAddr, metaHash, chunkData, err := metadata.serializeAndHash() // creates hashes and marshals, in one go
|
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
const expectedRootAddr = "0xfb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb" |
||||
const expectedMetaHash = "0xf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf0" |
||||
const expectedChunkData = "0x00004f0010dd205b00000000100e0000000000002a776f726c64206e657773207265706f72742c20657665727920686f75722c206f6e2074686520686f7572876a8936a7cd0b79ef0735ad0896c1afe278781c" |
||||
|
||||
compareByteSliceToExpectedHex(t, "rootAddr", rootAddr, expectedRootAddr) |
||||
compareByteSliceToExpectedHex(t, "metaHash", metaHash, expectedMetaHash) |
||||
compareByteSliceToExpectedHex(t, "chunkData", chunkData, expectedChunkData) |
||||
|
||||
recoveredMetadata := ResourceMetadata{} |
||||
recoveredMetadata.binaryGet(chunkData) |
||||
|
||||
if recoveredMetadata != metadata { |
||||
t.Fatalf("Expected that the recovered metadata equals the marshalled metadata") |
||||
} |
||||
|
||||
// we are going to mess with the data, so create a backup to go back to it for the next test
|
||||
backup := make([]byte, len(chunkData)) |
||||
copy(backup, chunkData) |
||||
|
||||
chunkData = []byte{1, 2, 3} |
||||
if err := recoveredMetadata.binaryGet(chunkData); err == nil { |
||||
t.Fatal("Expected binaryGet to fail since chunk is too small") |
||||
} |
||||
|
||||
// restore backup
|
||||
chunkData = make([]byte, len(backup)) |
||||
copy(chunkData, backup) |
||||
|
||||
// mess with the prefix so it is not zero
|
||||
chunkData[0] = 7 |
||||
chunkData[1] = 9 |
||||
|
||||
if err := recoveredMetadata.binaryGet(chunkData); err == nil { |
||||
t.Fatal("Expected binaryGet to fail since prefix bytes are not zero") |
||||
} |
||||
|
||||
// restore backup
|
||||
chunkData = make([]byte, len(backup)) |
||||
copy(chunkData, backup) |
||||
|
||||
// mess with the length header to trigger an error
|
||||
chunkData[2] = 255 |
||||
chunkData[3] = 44 |
||||
if err := recoveredMetadata.binaryGet(chunkData); err == nil { |
||||
t.Fatal("Expected binaryGet to fail since header length does not match") |
||||
} |
||||
|
||||
// restore backup
|
||||
chunkData = make([]byte, len(backup)) |
||||
copy(chunkData, backup) |
||||
|
||||
// mess with name length header to trigger a chunk too short error
|
||||
chunkData[20] = 255 |
||||
if err := recoveredMetadata.binaryGet(chunkData); err == nil { |
||||
t.Fatal("Expected binaryGet to fail since name length is incorrect") |
||||
} |
||||
|
||||
// restore backup
|
||||
chunkData = make([]byte, len(backup)) |
||||
copy(chunkData, backup) |
||||
|
||||
// mess with name length header to trigger an leftover bytes to read error
|
||||
chunkData[20] = 3 |
||||
if err := recoveredMetadata.binaryGet(chunkData); err == nil { |
||||
t.Fatal("Expected binaryGet to fail since name length is too small") |
||||
} |
||||
} |
||||
|
||||
func TestMetadataSerializerLengthCheck(t *testing.T) { |
||||
metadata := *getTestMetadata() |
||||
|
||||
// make a slice that is too small to contain the metadata
|
||||
serializedMetadata := make([]byte, 4) |
||||
|
||||
if err := metadata.binaryPut(serializedMetadata); err == nil { |
||||
t.Fatal("Expected metadata.binaryPut to fail, since target slice is too small") |
||||
} |
||||
|
||||
} |
@ -0,0 +1,297 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
// updateRequestJSON represents a JSON-serialized UpdateRequest
|
||||
type updateRequestJSON struct { |
||||
Name string `json:"name,omitempty"` |
||||
Frequency uint64 `json:"frequency,omitempty"` |
||||
StartTime uint64 `json:"startTime,omitempty"` |
||||
Owner string `json:"ownerAddr,omitempty"` |
||||
RootAddr string `json:"rootAddr,omitempty"` |
||||
MetaHash string `json:"metaHash,omitempty"` |
||||
Version uint32 `json:"version,omitempty"` |
||||
Period uint32 `json:"period,omitempty"` |
||||
Data string `json:"data,omitempty"` |
||||
Multihash bool `json:"multiHash"` |
||||
Signature string `json:"signature,omitempty"` |
||||
} |
||||
|
||||
// Request represents an update and/or resource create message
|
||||
type Request struct { |
||||
SignedResourceUpdate |
||||
metadata ResourceMetadata |
||||
isNew bool |
||||
} |
||||
|
||||
var zeroAddr = common.Address{} |
||||
|
||||
// NewCreateUpdateRequest returns a ready to sign request to create and initialize a resource with data
|
||||
func NewCreateUpdateRequest(metadata *ResourceMetadata) (*Request, error) { |
||||
|
||||
request, err := NewCreateRequest(metadata) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// get the current time
|
||||
now := TimestampProvider.Now().Time |
||||
|
||||
request.version = 1 |
||||
request.period, err = getNextPeriod(metadata.StartTime.Time, now, metadata.Frequency) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return request, nil |
||||
} |
||||
|
||||
// NewCreateRequest returns a request to create a new resource
|
||||
func NewCreateRequest(metadata *ResourceMetadata) (request *Request, err error) { |
||||
if metadata.StartTime.Time == 0 { // get the current time
|
||||
metadata.StartTime = TimestampProvider.Now() |
||||
} |
||||
|
||||
if metadata.Owner == zeroAddr { |
||||
return nil, NewError(ErrInvalidValue, "OwnerAddr is not set") |
||||
} |
||||
|
||||
request = &Request{ |
||||
metadata: *metadata, |
||||
} |
||||
request.rootAddr, request.metaHash, _, err = request.metadata.serializeAndHash() |
||||
request.isNew = true |
||||
return request, nil |
||||
} |
||||
|
||||
// Frequency returns the resource's expected update frequency
|
||||
func (r *Request) Frequency() uint64 { |
||||
return r.metadata.Frequency |
||||
} |
||||
|
||||
// Name returns the resource human-readable name
|
||||
func (r *Request) Name() string { |
||||
return r.metadata.Name |
||||
} |
||||
|
||||
// Multihash returns true if the resource data should be interpreted as a multihash
|
||||
func (r *Request) Multihash() bool { |
||||
return r.multihash |
||||
} |
||||
|
||||
// Period returns in which period the resource will be published
|
||||
func (r *Request) Period() uint32 { |
||||
return r.period |
||||
} |
||||
|
||||
// Version returns the resource version to publish
|
||||
func (r *Request) Version() uint32 { |
||||
return r.version |
||||
} |
||||
|
||||
// RootAddr returns the metadata chunk address
|
||||
func (r *Request) RootAddr() storage.Address { |
||||
return r.rootAddr |
||||
} |
||||
|
||||
// StartTime returns the time that the resource was/will be created at
|
||||
func (r *Request) StartTime() Timestamp { |
||||
return r.metadata.StartTime |
||||
} |
||||
|
||||
// Owner returns the resource owner's address
|
||||
func (r *Request) Owner() common.Address { |
||||
return r.metadata.Owner |
||||
} |
||||
|
||||
// Sign executes the signature to validate the resource and sets the owner address field
|
||||
func (r *Request) Sign(signer Signer) error { |
||||
if r.metadata.Owner != zeroAddr && r.metadata.Owner != signer.Address() { |
||||
return NewError(ErrInvalidSignature, "Signer does not match current owner of the resource") |
||||
} |
||||
|
||||
if err := r.SignedResourceUpdate.Sign(signer); err != nil { |
||||
return err |
||||
} |
||||
r.metadata.Owner = signer.Address() |
||||
return nil |
||||
} |
||||
|
||||
// SetData stores the payload data the resource will be updated with
|
||||
func (r *Request) SetData(data []byte, multihash bool) { |
||||
r.data = data |
||||
r.multihash = multihash |
||||
r.signature = nil |
||||
if !r.isNew { |
||||
r.metadata.Frequency = 0 // mark as update
|
||||
} |
||||
} |
||||
|
||||
func (r *Request) IsNew() bool { |
||||
return r.metadata.Frequency > 0 && (r.period <= 1 || r.version <= 1) |
||||
} |
||||
|
||||
func (r *Request) IsUpdate() bool { |
||||
return r.signature != nil |
||||
} |
||||
|
||||
// fromJSON takes an update request JSON and populates an UpdateRequest
|
||||
func (r *Request) fromJSON(j *updateRequestJSON) error { |
||||
|
||||
r.version = j.Version |
||||
r.period = j.Period |
||||
r.multihash = j.Multihash |
||||
r.metadata.Name = j.Name |
||||
r.metadata.Frequency = j.Frequency |
||||
r.metadata.StartTime.Time = j.StartTime |
||||
|
||||
if err := decodeHexArray(r.metadata.Owner[:], j.Owner, "ownerAddr"); err != nil { |
||||
return err |
||||
} |
||||
|
||||
var err error |
||||
if j.Data != "" { |
||||
r.data, err = hexutil.Decode(j.Data) |
||||
if err != nil { |
||||
return NewError(ErrInvalidValue, "Cannot decode data") |
||||
} |
||||
} |
||||
|
||||
var declaredRootAddr storage.Address |
||||
var declaredMetaHash []byte |
||||
|
||||
declaredRootAddr, err = decodeHexSlice(j.RootAddr, storage.KeyLength, "rootAddr") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
declaredMetaHash, err = decodeHexSlice(j.MetaHash, 32, "metaHash") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if r.IsNew() { |
||||
// for new resource creation, rootAddr and metaHash are optional because
|
||||
// we can derive them from the content itself.
|
||||
// however, if the user sent them, we check them for consistency.
|
||||
|
||||
r.rootAddr, r.metaHash, _, err = r.metadata.serializeAndHash() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if j.RootAddr != "" && !bytes.Equal(declaredRootAddr, r.rootAddr) { |
||||
return NewError(ErrInvalidValue, "rootAddr does not match resource metadata") |
||||
} |
||||
if j.MetaHash != "" && !bytes.Equal(declaredMetaHash, r.metaHash) { |
||||
return NewError(ErrInvalidValue, "metaHash does not match resource metadata") |
||||
} |
||||
|
||||
} else { |
||||
//Update message
|
||||
r.rootAddr = declaredRootAddr |
||||
r.metaHash = declaredMetaHash |
||||
} |
||||
|
||||
if j.Signature != "" { |
||||
sigBytes, err := hexutil.Decode(j.Signature) |
||||
if err != nil || len(sigBytes) != signatureLength { |
||||
return NewError(ErrInvalidSignature, "Cannot decode signature") |
||||
} |
||||
r.signature = new(Signature) |
||||
r.updateAddr = r.UpdateAddr() |
||||
copy(r.signature[:], sigBytes) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func decodeHexArray(dst []byte, src, name string) error { |
||||
bytes, err := decodeHexSlice(src, len(dst), name) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if bytes != nil { |
||||
copy(dst, bytes) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func decodeHexSlice(src string, expectedLength int, name string) (bytes []byte, err error) { |
||||
if src != "" { |
||||
bytes, err = hexutil.Decode(src) |
||||
if err != nil || len(bytes) != expectedLength { |
||||
return nil, NewErrorf(ErrInvalidValue, "Cannot decode %s", name) |
||||
} |
||||
} |
||||
return bytes, nil |
||||
} |
||||
|
||||
// UnmarshalJSON takes a JSON structure stored in a byte array and populates the Request object
|
||||
// Implements json.Unmarshaler interface
|
||||
func (r *Request) UnmarshalJSON(rawData []byte) error { |
||||
var requestJSON updateRequestJSON |
||||
if err := json.Unmarshal(rawData, &requestJSON); err != nil { |
||||
return err |
||||
} |
||||
return r.fromJSON(&requestJSON) |
||||
} |
||||
|
||||
// MarshalJSON takes an update request and encodes it as a JSON structure into a byte array
|
||||
// Implements json.Marshaler interface
|
||||
func (r *Request) MarshalJSON() (rawData []byte, err error) { |
||||
var signatureString, dataHashString, rootAddrString, metaHashString string |
||||
if r.signature != nil { |
||||
signatureString = hexutil.Encode(r.signature[:]) |
||||
} |
||||
if r.data != nil { |
||||
dataHashString = hexutil.Encode(r.data) |
||||
} |
||||
if r.rootAddr != nil { |
||||
rootAddrString = hexutil.Encode(r.rootAddr) |
||||
} |
||||
if r.metaHash != nil { |
||||
metaHashString = hexutil.Encode(r.metaHash) |
||||
} |
||||
var ownerAddrString string |
||||
if r.metadata.Frequency == 0 { |
||||
ownerAddrString = "" |
||||
} else { |
||||
ownerAddrString = hexutil.Encode(r.metadata.Owner[:]) |
||||
} |
||||
|
||||
requestJSON := &updateRequestJSON{ |
||||
Name: r.metadata.Name, |
||||
Frequency: r.metadata.Frequency, |
||||
StartTime: r.metadata.StartTime.Time, |
||||
Version: r.version, |
||||
Period: r.period, |
||||
Owner: ownerAddrString, |
||||
Data: dataHashString, |
||||
Multihash: r.multihash, |
||||
Signature: signatureString, |
||||
RootAddr: rootAddrString, |
||||
MetaHash: metaHashString, |
||||
} |
||||
|
||||
return json.Marshal(requestJSON) |
||||
} |
@ -0,0 +1,175 @@ |
||||
package mru |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"encoding/json" |
||||
"fmt" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func areEqualJSON(s1, s2 string) (bool, error) { |
||||
//credit for the trick: turtlemonvh https://gist.github.com/turtlemonvh/e4f7404e28387fadb8ad275a99596f67
|
||||
var o1 interface{} |
||||
var o2 interface{} |
||||
|
||||
err := json.Unmarshal([]byte(s1), &o1) |
||||
if err != nil { |
||||
return false, fmt.Errorf("Error mashalling string 1 :: %s", err.Error()) |
||||
} |
||||
err = json.Unmarshal([]byte(s2), &o2) |
||||
if err != nil { |
||||
return false, fmt.Errorf("Error mashalling string 2 :: %s", err.Error()) |
||||
} |
||||
|
||||
return reflect.DeepEqual(o1, o2), nil |
||||
} |
||||
|
||||
// TestEncodingDecodingUpdateRequests ensures that requests are serialized properly
|
||||
// while also checking cryptographically that only the owner of a resource can update it.
|
||||
func TestEncodingDecodingUpdateRequests(t *testing.T) { |
||||
|
||||
signer := newCharlieSigner() //Charlie, our good guy
|
||||
falseSigner := newBobSigner() //Bob will play the bad guy again
|
||||
|
||||
// Create a resource to our good guy Charlie's name
|
||||
createRequest, err := NewCreateRequest(&ResourceMetadata{ |
||||
Name: "a good resource name", |
||||
Frequency: 300, |
||||
StartTime: Timestamp{Time: 1528900000}, |
||||
Owner: signer.Address()}) |
||||
|
||||
if err != nil { |
||||
t.Fatalf("Error creating resource name: %s", err) |
||||
} |
||||
|
||||
// We now encode the create message to simulate we send it over the wire
|
||||
messageRawData, err := createRequest.MarshalJSON() |
||||
if err != nil { |
||||
t.Fatalf("Error encoding create resource request: %s", err) |
||||
} |
||||
|
||||
// ... the message arrives and is decoded...
|
||||
var recoveredCreateRequest Request |
||||
if err := recoveredCreateRequest.UnmarshalJSON(messageRawData); err != nil { |
||||
t.Fatalf("Error decoding create resource request: %s", err) |
||||
} |
||||
|
||||
// ... but verification should fail because it is not signed!
|
||||
if err := recoveredCreateRequest.Verify(); err == nil { |
||||
t.Fatal("Expected Verify to fail since the message is not signed") |
||||
} |
||||
|
||||
// We now assume that the resource was created and propagated. With rootAddr we can retrieve the resource metadata
|
||||
// and recover the information above. To sign an update, we need the rootAddr and the metaHash to construct
|
||||
// proof of ownership
|
||||
|
||||
metaHash := createRequest.metaHash |
||||
rootAddr := createRequest.rootAddr |
||||
const expectedSignature = "0x1c2bab66dc4ed63783d62934e3a628e517888d6949aef0349f3bd677121db9aa09bbfb865904e6c50360e209e0fe6fe757f8a2474cf1b34169c99b95e3fd5a5101" |
||||
const expectedJSON = `{"rootAddr":"0x6e744a730f7ea0881528576f0354b6268b98e35a6981ef703153ff1b8d32bbef","metaHash":"0x0c0d5c18b89da503af92302a1a64fab6acb60f78e288eb9c3d541655cd359b60","version":1,"period":7,"data":"0x5468697320686f75722773207570646174653a20537761726d2039392e3020686173206265656e2072656c656173656421","multiHash":false}` |
||||
|
||||
//Put together an unsigned update request that we will serialize to send it to the signer.
|
||||
data := []byte("This hour's update: Swarm 99.0 has been released!") |
||||
request := &Request{ |
||||
SignedResourceUpdate: SignedResourceUpdate{ |
||||
resourceUpdate: resourceUpdate{ |
||||
updateHeader: updateHeader{ |
||||
UpdateLookup: UpdateLookup{ |
||||
period: 7, |
||||
version: 1, |
||||
rootAddr: rootAddr, |
||||
}, |
||||
multihash: false, |
||||
metaHash: metaHash, |
||||
}, |
||||
data: data, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
messageRawData, err = request.MarshalJSON() |
||||
if err != nil { |
||||
t.Fatalf("Error encoding update request: %s", err) |
||||
} |
||||
|
||||
equalJSON, err := areEqualJSON(string(messageRawData), expectedJSON) |
||||
if err != nil { |
||||
t.Fatalf("Error decoding update request JSON: %s", err) |
||||
} |
||||
if !equalJSON { |
||||
t.Fatalf("Received a different JSON message. Expected %s, got %s", expectedJSON, string(messageRawData)) |
||||
} |
||||
|
||||
// now the encoded message messageRawData is sent over the wire and arrives to the signer
|
||||
|
||||
//Attempt to extract an UpdateRequest out of the encoded message
|
||||
var recoveredRequest Request |
||||
if err := recoveredRequest.UnmarshalJSON(messageRawData); err != nil { |
||||
t.Fatalf("Error decoding update request: %s", err) |
||||
} |
||||
|
||||
//sign the request and see if it matches our predefined signature above.
|
||||
if err := recoveredRequest.Sign(signer); err != nil { |
||||
t.Fatalf("Error signing request: %s", err) |
||||
} |
||||
|
||||
compareByteSliceToExpectedHex(t, "signature", recoveredRequest.signature[:], expectedSignature) |
||||
|
||||
// mess with the signature and see what happens. To alter the signature, we briefly decode it as JSON
|
||||
// to alter the signature field.
|
||||
var j updateRequestJSON |
||||
if err := json.Unmarshal([]byte(expectedJSON), &j); err != nil { |
||||
t.Fatal("Error unmarshalling test json, check expectedJSON constant") |
||||
} |
||||
j.Signature = "Certainly not a signature" |
||||
corruptMessage, _ := json.Marshal(j) // encode the message with the bad signature
|
||||
var corruptRequest Request |
||||
if err = corruptRequest.UnmarshalJSON(corruptMessage); err == nil { |
||||
t.Fatal("Expected DecodeUpdateRequest to fail when trying to interpret a corrupt message with an invalid signature") |
||||
} |
||||
|
||||
// Now imagine Evil Bob (why always Bob, poor Bob) attempts to update Charlie's resource,
|
||||
// signing a message with his private key
|
||||
if err := request.Sign(falseSigner); err != nil { |
||||
t.Fatalf("Error signing: %s", err) |
||||
} |
||||
|
||||
// Now Bob encodes the message to send it over the wire...
|
||||
messageRawData, err = request.MarshalJSON() |
||||
if err != nil { |
||||
t.Fatalf("Error encoding message:%s", err) |
||||
} |
||||
|
||||
// ... the message arrives to our Swarm node and it is decoded.
|
||||
recoveredRequest = Request{} |
||||
if err := recoveredRequest.UnmarshalJSON(messageRawData); err != nil { |
||||
t.Fatalf("Error decoding message:%s", err) |
||||
} |
||||
|
||||
// Before discovering Bob's misdemeanor, let's see what would happen if we mess
|
||||
// with the signature big time to see if Verify catches it
|
||||
savedSignature := *recoveredRequest.signature // save the signature for later
|
||||
binary.LittleEndian.PutUint64(recoveredRequest.signature[5:], 556845463424) // write some random data to break the signature
|
||||
if err = recoveredRequest.Verify(); err == nil { |
||||
t.Fatal("Expected Verify to fail on corrupt signature") |
||||
} |
||||
|
||||
// restore the Evil Bob's signature from corruption
|
||||
*recoveredRequest.signature = savedSignature |
||||
|
||||
// Now the signature is not corrupt, however Verify should now fail because Bob doesn't own the resource
|
||||
if err = recoveredRequest.Verify(); err == nil { |
||||
t.Fatalf("Expected Verify to fail because this resource belongs to Charlie, not Bob the attacker:%s", err) |
||||
} |
||||
|
||||
// Sign with our friend Charlie's private key
|
||||
if err := recoveredRequest.Sign(signer); err != nil { |
||||
t.Fatalf("Error signing with the correct private key: %s", err) |
||||
} |
||||
|
||||
// And now, Verify should work since this resource belongs to Charlie
|
||||
if err = recoveredRequest.Verify(); err != nil { |
||||
t.Fatalf("Error verifying that Charlie, the good guy, can sign his resource:%s", err) |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,184 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"hash" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
// SignedResourceUpdate represents a resource update with all the necessary information to prove ownership of the resource
|
||||
type SignedResourceUpdate struct { |
||||
resourceUpdate // actual content that will be put on the chunk, less signature
|
||||
signature *Signature |
||||
updateAddr storage.Address // resulting chunk address for the update (not serialized, for internal use)
|
||||
binaryData []byte // resulting serialized data (not serialized, for efficiency/internal use)
|
||||
} |
||||
|
||||
// Verify checks that signatures are valid and that the signer owns the resource to be updated
|
||||
func (r *SignedResourceUpdate) Verify() (err error) { |
||||
if len(r.data) == 0 { |
||||
return NewError(ErrInvalidValue, "Update does not contain data") |
||||
} |
||||
if r.signature == nil { |
||||
return NewError(ErrInvalidSignature, "Missing signature field") |
||||
} |
||||
|
||||
digest, err := r.GetDigest() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// get the address of the signer (which also checks that it's a valid signature)
|
||||
ownerAddr, err := getOwner(digest, *r.signature) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !bytes.Equal(r.updateAddr, r.UpdateAddr()) { |
||||
return NewError(ErrInvalidSignature, "Signature address does not match with ownerAddr") |
||||
} |
||||
|
||||
// Check if who signed the resource update really owns the resource
|
||||
if !verifyOwner(ownerAddr, r.metaHash, r.rootAddr) { |
||||
return NewErrorf(ErrUnauthorized, "signature is valid but signer does not own the resource: %v", err) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Sign executes the signature to validate the resource
|
||||
func (r *SignedResourceUpdate) Sign(signer Signer) error { |
||||
|
||||
r.binaryData = nil //invalidate serialized data
|
||||
digest, err := r.GetDigest() // computes digest and serializes into .binaryData
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
signature, err := signer.Sign(digest) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Although the Signer interface returns the public address of the signer,
|
||||
// recover it from the signature to see if they match
|
||||
ownerAddress, err := getOwner(digest, signature) |
||||
if err != nil { |
||||
return NewError(ErrInvalidSignature, "Error verifying signature") |
||||
} |
||||
|
||||
if ownerAddress != signer.Address() { // sanity check to make sure the Signer is declaring the same address used to sign!
|
||||
return NewError(ErrInvalidSignature, "Signer address does not match ownerAddr") |
||||
} |
||||
|
||||
r.signature = &signature |
||||
r.updateAddr = r.UpdateAddr() |
||||
return nil |
||||
} |
||||
|
||||
// create an update chunk.
|
||||
func (r *SignedResourceUpdate) toChunk() (*storage.Chunk, error) { |
||||
|
||||
// Check that the update is signed and serialized
|
||||
// For efficiency, data is serialized during signature and cached in
|
||||
// the binaryData field when computing the signature digest in .getDigest()
|
||||
if r.signature == nil || r.binaryData == nil { |
||||
return nil, NewError(ErrInvalidSignature, "newUpdateChunk called without a valid signature or payload data. Call .Sign() first.") |
||||
} |
||||
|
||||
chunk := storage.NewChunk(r.updateAddr, nil) |
||||
resourceUpdateLength := r.resourceUpdate.binaryLength() |
||||
chunk.SData = r.binaryData |
||||
|
||||
// signature is the last item in the chunk data
|
||||
copy(chunk.SData[resourceUpdateLength:], r.signature[:]) |
||||
|
||||
chunk.Size = int64(len(chunk.SData)) |
||||
return chunk, nil |
||||
} |
||||
|
||||
// fromChunk populates this structure from chunk data. It does not verify the signature is valid.
|
||||
func (r *SignedResourceUpdate) fromChunk(updateAddr storage.Address, chunkdata []byte) error { |
||||
// for update chunk layout see SignedResourceUpdate definition
|
||||
|
||||
//deserialize the resource update portion
|
||||
if err := r.resourceUpdate.binaryGet(chunkdata); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Extract the signature
|
||||
var signature *Signature |
||||
cursor := r.resourceUpdate.binaryLength() |
||||
sigdata := chunkdata[cursor : cursor+signatureLength] |
||||
if len(sigdata) > 0 { |
||||
signature = &Signature{} |
||||
copy(signature[:], sigdata) |
||||
} |
||||
|
||||
r.signature = signature |
||||
r.updateAddr = updateAddr |
||||
r.binaryData = chunkdata |
||||
|
||||
return nil |
||||
|
||||
} |
||||
|
||||
// GetDigest creates the resource update digest used in signatures (formerly known as keyDataHash)
|
||||
// the serialized payload is cached in .binaryData
|
||||
func (r *SignedResourceUpdate) GetDigest() (result common.Hash, err error) { |
||||
hasher := hashPool.Get().(hash.Hash) |
||||
defer hashPool.Put(hasher) |
||||
hasher.Reset() |
||||
dataLength := r.resourceUpdate.binaryLength() |
||||
if r.binaryData == nil { |
||||
r.binaryData = make([]byte, dataLength+signatureLength) |
||||
if err := r.resourceUpdate.binaryPut(r.binaryData[:dataLength]); err != nil { |
||||
return result, err |
||||
} |
||||
} |
||||
hasher.Write(r.binaryData[:dataLength]) //everything except the signature.
|
||||
|
||||
return common.BytesToHash(hasher.Sum(nil)), nil |
||||
} |
||||
|
||||
// getOwner extracts the address of the resource update signer
|
||||
func getOwner(digest common.Hash, signature Signature) (common.Address, error) { |
||||
pub, err := crypto.SigToPub(digest.Bytes(), signature[:]) |
||||
if err != nil { |
||||
return common.Address{}, err |
||||
} |
||||
return crypto.PubkeyToAddress(*pub), nil |
||||
} |
||||
|
||||
// verifyResourceOwnerhsip checks that the signer of the update actually owns the resource
|
||||
// H(ownerAddr, metaHash) is computed. If it matches the rootAddr the update chunk is claiming
|
||||
// to update, it is proven that signer of the resource update owns the resource.
|
||||
// See metadataHash in metadata.go for a more detailed explanation
|
||||
func verifyOwner(ownerAddr common.Address, metaHash []byte, rootAddr storage.Address) bool { |
||||
hasher := hashPool.Get().(hash.Hash) |
||||
defer hashPool.Put(hasher) |
||||
hasher.Reset() |
||||
hasher.Write(metaHash) |
||||
hasher.Write(ownerAddr.Bytes()) |
||||
rootAddr2 := hasher.Sum(nil) |
||||
return bytes.Equal(rootAddr2, rootAddr) |
||||
} |
@ -0,0 +1,56 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"fmt" |
||||
"path/filepath" |
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
const ( |
||||
testDbDirName = "mru" |
||||
) |
||||
|
||||
type TestHandler struct { |
||||
*Handler |
||||
} |
||||
|
||||
func (t *TestHandler) Close() { |
||||
t.chunkStore.Close() |
||||
} |
||||
|
||||
// NewTestHandler creates Handler object to be used for testing purposes.
|
||||
func NewTestHandler(datadir string, params *HandlerParams) (*TestHandler, error) { |
||||
path := filepath.Join(datadir, testDbDirName) |
||||
rh, err := NewHandler(params) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("resource handler create fail: %v", err) |
||||
} |
||||
localstoreparams := storage.NewDefaultLocalStoreParams() |
||||
localstoreparams.Init(path) |
||||
localStore, err := storage.NewLocalStore(localstoreparams, nil) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("localstore create fail, path %s: %v", path, err) |
||||
} |
||||
localStore.Validators = append(localStore.Validators, storage.NewContentAddressValidator(storage.MakeHashFunc(resourceHashAlgorithm))) |
||||
localStore.Validators = append(localStore.Validators, rh) |
||||
netStore := storage.NewNetStore(localStore, nil) |
||||
rh.SetStore(netStore) |
||||
return &TestHandler{rh}, nil |
||||
} |
@ -0,0 +1,71 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"time" |
||||
) |
||||
|
||||
// TimestampProvider sets the time source of the mru package
|
||||
var TimestampProvider timestampProvider = NewDefaultTimestampProvider() |
||||
|
||||
// Encodes a point in time as a Unix epoch
|
||||
type Timestamp struct { |
||||
Time uint64 // Unix epoch timestamp, in seconds
|
||||
} |
||||
|
||||
// 8 bytes uint64 Time
|
||||
const timestampLength = 8 |
||||
|
||||
// timestampProvider interface describes a source of timestamp information
|
||||
type timestampProvider interface { |
||||
Now() Timestamp // returns the current timestamp information
|
||||
} |
||||
|
||||
// binaryGet populates the timestamp structure from the given byte slice
|
||||
func (t *Timestamp) binaryGet(data []byte) error { |
||||
if len(data) != timestampLength { |
||||
return NewError(ErrCorruptData, "timestamp data has the wrong size") |
||||
} |
||||
t.Time = binary.LittleEndian.Uint64(data[:8]) |
||||
return nil |
||||
} |
||||
|
||||
// binaryPut Serializes a Timestamp to a byte slice
|
||||
func (t *Timestamp) binaryPut(data []byte) error { |
||||
if len(data) != timestampLength { |
||||
return NewError(ErrCorruptData, "timestamp data has the wrong size") |
||||
} |
||||
binary.LittleEndian.PutUint64(data, t.Time) |
||||
return nil |
||||
} |
||||
|
||||
type DefaultTimestampProvider struct { |
||||
} |
||||
|
||||
// NewDefaultTimestampProvider creates a system clock based timestamp provider
|
||||
func NewDefaultTimestampProvider() *DefaultTimestampProvider { |
||||
return &DefaultTimestampProvider{} |
||||
} |
||||
|
||||
// Now returns the current time according to this provider
|
||||
func (dtp *DefaultTimestampProvider) Now() Timestamp { |
||||
return Timestamp{ |
||||
Time: uint64(time.Now().Unix()), |
||||
} |
||||
} |
@ -0,0 +1,147 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"errors" |
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/log" |
||||
"github.com/ethereum/go-ethereum/swarm/multihash" |
||||
) |
||||
|
||||
// resourceUpdate encapsulates the information sent as part of a resource update
|
||||
type resourceUpdate struct { |
||||
updateHeader // metainformationa about this resource update
|
||||
data []byte // actual data payload
|
||||
} |
||||
|
||||
// Update chunk layout
|
||||
// Prefix:
|
||||
// 2 bytes updateHeaderLength
|
||||
// 2 bytes data length
|
||||
const chunkPrefixLength = 2 + 2 |
||||
|
||||
// Header: (see updateHeader)
|
||||
// Data:
|
||||
// data (datalength bytes)
|
||||
//
|
||||
// Minimum size is Header + 1 (minimum data length, enforced)
|
||||
const minimumUpdateDataLength = updateHeaderLength + 1 |
||||
const maxUpdateDataLength = chunkSize - signatureLength - updateHeaderLength - chunkPrefixLength |
||||
|
||||
// binaryPut serializes the resource update information into the given slice
|
||||
func (r *resourceUpdate) binaryPut(serializedData []byte) error { |
||||
datalength := len(r.data) |
||||
if datalength == 0 { |
||||
return NewError(ErrInvalidValue, "cannot update a resource with no data") |
||||
} |
||||
|
||||
if datalength > maxUpdateDataLength { |
||||
return NewErrorf(ErrInvalidValue, "data is too big (length=%d). Max length=%d", datalength, maxUpdateDataLength) |
||||
} |
||||
|
||||
if len(serializedData) != r.binaryLength() { |
||||
return NewErrorf(ErrInvalidValue, "slice passed to putBinary must be of exact size. Expected %d bytes", r.binaryLength()) |
||||
} |
||||
|
||||
if r.multihash { |
||||
if _, _, err := multihash.GetMultihashLength(r.data); err != nil { |
||||
return NewError(ErrInvalidValue, "Invalid multihash") |
||||
} |
||||
} |
||||
|
||||
// Add prefix: updateHeaderLength and actual data length
|
||||
cursor := 0 |
||||
binary.LittleEndian.PutUint16(serializedData[cursor:], uint16(updateHeaderLength)) |
||||
cursor += 2 |
||||
|
||||
// data length
|
||||
binary.LittleEndian.PutUint16(serializedData[cursor:], uint16(datalength)) |
||||
cursor += 2 |
||||
|
||||
// serialize header (see updateHeader)
|
||||
if err := r.updateHeader.binaryPut(serializedData[cursor : cursor+updateHeaderLength]); err != nil { |
||||
return err |
||||
} |
||||
cursor += updateHeaderLength |
||||
|
||||
// add the data
|
||||
copy(serializedData[cursor:], r.data) |
||||
cursor += datalength |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// binaryLength returns the expected number of bytes this structure will take to encode
|
||||
func (r *resourceUpdate) binaryLength() int { |
||||
return chunkPrefixLength + updateHeaderLength + len(r.data) |
||||
} |
||||
|
||||
// binaryGet populates this instance from the information contained in the passed byte slice
|
||||
func (r *resourceUpdate) binaryGet(serializedData []byte) error { |
||||
if len(serializedData) < minimumUpdateDataLength { |
||||
return NewErrorf(ErrNothingToReturn, "chunk less than %d bytes cannot be a resource update chunk", minimumUpdateDataLength) |
||||
} |
||||
cursor := 0 |
||||
declaredHeaderlength := binary.LittleEndian.Uint16(serializedData[cursor : cursor+2]) |
||||
if declaredHeaderlength != updateHeaderLength { |
||||
return NewErrorf(ErrCorruptData, "Invalid header length. Expected %d, got %d", updateHeaderLength, declaredHeaderlength) |
||||
} |
||||
|
||||
cursor += 2 |
||||
datalength := int(binary.LittleEndian.Uint16(serializedData[cursor : cursor+2])) |
||||
cursor += 2 |
||||
|
||||
if chunkPrefixLength+updateHeaderLength+datalength+signatureLength != len(serializedData) { |
||||
return NewError(ErrNothingToReturn, "length specified in header is different than actual chunk size") |
||||
} |
||||
|
||||
// at this point we can be satisfied that we have the correct data length to read
|
||||
if err := r.updateHeader.binaryGet(serializedData[cursor : cursor+updateHeaderLength]); err != nil { |
||||
return err |
||||
} |
||||
cursor += updateHeaderLength |
||||
|
||||
data := serializedData[cursor : cursor+datalength] |
||||
cursor += datalength |
||||
|
||||
// if multihash content is indicated we check the validity of the multihash
|
||||
if r.updateHeader.multihash { |
||||
mhLength, mhHeaderLength, err := multihash.GetMultihashLength(data) |
||||
if err != nil { |
||||
log.Error("multihash parse error", "err", err) |
||||
return err |
||||
} |
||||
if datalength != mhLength+mhHeaderLength { |
||||
log.Debug("multihash error", "datalength", datalength, "mhLength", mhLength, "mhHeaderLength", mhHeaderLength) |
||||
return errors.New("Corrupt multihash data") |
||||
} |
||||
} |
||||
|
||||
// now that all checks have passed, copy data into structure
|
||||
r.data = make([]byte, datalength) |
||||
copy(r.data, data) |
||||
|
||||
return nil |
||||
|
||||
} |
||||
|
||||
// Multihash specifies whether the resource data should be interpreted as multihash
|
||||
func (r *resourceUpdate) Multihash() bool { |
||||
return r.multihash |
||||
} |
@ -0,0 +1,72 @@ |
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
) |
||||
|
||||
const serializedUpdateHex = "0x490034004f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf000456c20717565206c6565206d7563686f207920616e6461206d7563686f2c207665206d7563686f20792073616265206d7563686f" |
||||
const serializedUpdateMultihashHex = "0x490022004f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf0011b200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1c1e1f20" |
||||
|
||||
func getTestResourceUpdate() *resourceUpdate { |
||||
return &resourceUpdate{ |
||||
updateHeader: *getTestUpdateHeader(false), |
||||
data: []byte("El que lee mucho y anda mucho, ve mucho y sabe mucho"), |
||||
} |
||||
} |
||||
|
||||
func getTestResourceUpdateMultihash() *resourceUpdate { |
||||
return &resourceUpdate{ |
||||
updateHeader: *getTestUpdateHeader(true), |
||||
data: []byte{0x1b, 0x20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 28, 30, 31, 32}, |
||||
} |
||||
} |
||||
|
||||
func compareResourceUpdate(a, b *resourceUpdate) bool { |
||||
return compareUpdateHeader(&a.updateHeader, &b.updateHeader) && |
||||
bytes.Equal(a.data, b.data) |
||||
} |
||||
|
||||
func TestResourceUpdateSerializer(t *testing.T) { |
||||
var serializedUpdateLength = len(serializedUpdateHex)/2 - 1 // hack to calculate the byte length out of the hex representation
|
||||
update := getTestResourceUpdate() |
||||
serializedUpdate := make([]byte, serializedUpdateLength) |
||||
if err := update.binaryPut(serializedUpdate); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
compareByteSliceToExpectedHex(t, "serializedUpdate", serializedUpdate, serializedUpdateHex) |
||||
|
||||
// Test fail if update does not contain data
|
||||
update.data = nil |
||||
if err := update.binaryPut(serializedUpdate); err == nil { |
||||
t.Fatal("Expected resourceUpdate.binaryPut to fail since update does not contain data") |
||||
} |
||||
|
||||
// Test fail if update is too big
|
||||
update.data = make([]byte, 10000) |
||||
if err := update.binaryPut(serializedUpdate); err == nil { |
||||
t.Fatal("Expected resourceUpdate.binaryPut to fail since update is too big") |
||||
} |
||||
|
||||
// Test fail if passed slice is not of the exact size required for this update
|
||||
update.data = make([]byte, 1) |
||||
if err := update.binaryPut(serializedUpdate); err == nil { |
||||
t.Fatal("Expected resourceUpdate.binaryPut to fail since passed slice is not of the appropriate size") |
||||
} |
||||
|
||||
// Test serializing a multihash update
|
||||
var serializedUpdateMultihashLength = len(serializedUpdateMultihashHex)/2 - 1 // hack to calculate the byte length out of the hex representation
|
||||
update = getTestResourceUpdateMultihash() |
||||
serializedUpdate = make([]byte, serializedUpdateMultihashLength) |
||||
if err := update.binaryPut(serializedUpdate); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
compareByteSliceToExpectedHex(t, "serializedUpdate", serializedUpdate, serializedUpdateMultihashHex) |
||||
|
||||
// mess with the multihash to test it fails with a wrong multihash error
|
||||
update.data[1] = 79 |
||||
if err := update.binaryPut(serializedUpdate); err == nil { |
||||
t.Fatal("Expected resourceUpdate.binaryPut to fail since data contains an invalid multihash") |
||||
} |
||||
|
||||
} |
@ -0,0 +1,88 @@ |
||||
// Copyright 2018 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package mru |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/swarm/storage" |
||||
) |
||||
|
||||
// updateHeader models the non-payload components of a Resource Update
|
||||
type updateHeader struct { |
||||
UpdateLookup // UpdateLookup contains the information required to locate this resource (components of the search key used to find it)
|
||||
multihash bool // Whether the data in this Resource Update should be interpreted as multihash
|
||||
metaHash []byte // SHA3 hash of the metadata chunk (less ownerAddr). Used to prove ownerhsip of the resource.
|
||||
} |
||||
|
||||
const metaHashLength = storage.KeyLength |
||||
|
||||
// updateLookupLength bytes
|
||||
// 1 byte flags (multihash bool for now)
|
||||
// 32 bytes metaHash
|
||||
const updateHeaderLength = updateLookupLength + 1 + metaHashLength |
||||
|
||||
// binaryPut serializes the resource header information into the given slice
|
||||
func (h *updateHeader) binaryPut(serializedData []byte) error { |
||||
if len(serializedData) != updateHeaderLength { |
||||
return NewErrorf(ErrInvalidValue, "Incorrect slice size to serialize updateHeaderLength. Expected %d, got %d", updateHeaderLength, len(serializedData)) |
||||
} |
||||
if len(h.metaHash) != metaHashLength { |
||||
return NewError(ErrInvalidValue, "updateHeader.binaryPut called without metaHash set") |
||||
} |
||||
if err := h.UpdateLookup.binaryPut(serializedData[:updateLookupLength]); err != nil { |
||||
return err |
||||
} |
||||
cursor := updateLookupLength |
||||
copy(serializedData[cursor:], h.metaHash[:metaHashLength]) |
||||
cursor += metaHashLength |
||||
|
||||
var flags byte |
||||
if h.multihash { |
||||
flags |= 0x01 |
||||
} |
||||
|
||||
serializedData[cursor] = flags |
||||
cursor++ |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// binaryLength returns the expected size of this structure when serialized
|
||||
func (h *updateHeader) binaryLength() int { |
||||
return updateHeaderLength |
||||
} |
||||
|
||||
// binaryGet restores the current updateHeader instance from the information contained in the passed slice
|
||||
func (h *updateHeader) binaryGet(serializedData []byte) error { |
||||
if len(serializedData) != updateHeaderLength { |
||||
return NewErrorf(ErrInvalidValue, "Incorrect slice size to read updateHeaderLength. Expected %d, got %d", updateHeaderLength, len(serializedData)) |
||||
} |
||||
|
||||
if err := h.UpdateLookup.binaryGet(serializedData[:updateLookupLength]); err != nil { |
||||
return err |
||||
} |
||||
cursor := updateLookupLength |
||||
h.metaHash = make([]byte, metaHashLength) |
||||
copy(h.metaHash[:storage.KeyLength], serializedData[cursor:cursor+storage.KeyLength]) |
||||
cursor += metaHashLength |
||||
|
||||
flags := serializedData[cursor] |
||||
cursor++ |
||||
|
||||
h.multihash = flags&0x01 != 0 |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,64 @@ |
||||
package mru |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
) |
||||
|
||||
const serializedUpdateHeaderMultihashHex = "0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf001" |
||||
|
||||
func getTestUpdateHeader(multihash bool) (header *updateHeader) { |
||||
_, metaHash, _, _ := getTestMetadata().serializeAndHash() |
||||
return &updateHeader{ |
||||
UpdateLookup: *getTestUpdateLookup(), |
||||
multihash: multihash, |
||||
metaHash: metaHash, |
||||
} |
||||
} |
||||
|
||||
func compareUpdateHeader(a, b *updateHeader) bool { |
||||
return compareUpdateLookup(&a.UpdateLookup, &b.UpdateLookup) && |
||||
a.multihash == b.multihash && |
||||
bytes.Equal(a.metaHash, b.metaHash) |
||||
} |
||||
|
||||
func TestUpdateHeaderSerializer(t *testing.T) { |
||||
header := getTestUpdateHeader(true) |
||||
serializedHeader := make([]byte, updateHeaderLength) |
||||
if err := header.binaryPut(serializedHeader); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
compareByteSliceToExpectedHex(t, "serializedHeader", serializedHeader, serializedUpdateHeaderMultihashHex) |
||||
|
||||
// trigger incorrect slice length error passing a slice that is 1 byte too big
|
||||
if err := header.binaryPut(make([]byte, updateHeaderLength+1)); err == nil { |
||||
t.Fatal("Expected updateHeader.binaryPut to fail since supplied slice is of incorrect length") |
||||
} |
||||
|
||||
// trigger invalid metaHash error
|
||||
header.metaHash = nil |
||||
if err := header.binaryPut(serializedHeader); err == nil { |
||||
t.Fatal("Expected updateHeader.binaryPut to fail metaHash is of incorrect length") |
||||
} |
||||
} |
||||
|
||||
func TestUpdateHeaderDeserializer(t *testing.T) { |
||||
originalUpdate := getTestUpdateHeader(true) |
||||
serializedData, _ := hexutil.Decode(serializedUpdateHeaderMultihashHex) |
||||
var retrievedUpdate updateHeader |
||||
if err := retrievedUpdate.binaryGet(serializedData); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !compareUpdateHeader(originalUpdate, &retrievedUpdate) { |
||||
t.Fatalf("Expected deserialized structure to equal the original") |
||||
} |
||||
|
||||
// mess with source slice to test length checks
|
||||
serializedData = []byte{1, 2, 3} |
||||
if err := retrievedUpdate.binaryGet(serializedData); err == nil { |
||||
t.Fatal("Expected retrievedUpdate.binaryGet, since passed slice is too small") |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue