Merge pull request #1 from ethereum/master

Updating files
This commit is contained in:
SphinxCoin 2018-10-02 01:02:40 +02:00 committed by GitHub
commit 29d2d9f7c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 7890 additions and 3607 deletions

View file

@ -57,6 +57,9 @@ devtools:
@type "solc" 2> /dev/null || echo 'Please install solc' @type "solc" 2> /dev/null || echo 'Please install solc'
@type "protoc" 2> /dev/null || echo 'Please install protoc' @type "protoc" 2> /dev/null || echo 'Please install protoc'
swarm-devtools:
env GOBIN= go install ./cmd/swarm/mimegen
# Cross Compilation Targets (xgo) # Cross Compilation Targets (xgo)
geth-cross: geth-linux geth-darwin geth-windows geth-android geth-ios geth-cross: geth-linux geth-darwin geth-windows geth-android geth-ios

View file

@ -137,6 +137,9 @@ func (abi *ABI) UnmarshalJSON(data []byte) error {
// MethodById looks up a method by the 4-byte id // MethodById looks up a method by the 4-byte id
// returns nil if none found // returns nil if none found
func (abi *ABI) MethodById(sigdata []byte) (*Method, error) { func (abi *ABI) MethodById(sigdata []byte) (*Method, error) {
if len(sigdata) < 4 {
return nil, fmt.Errorf("data too short (% bytes) for abi method lookup", len(sigdata))
}
for _, method := range abi.Methods { for _, method := range abi.Methods {
if bytes.Equal(method.Id(), sigdata[:4]) { if bytes.Equal(method.Id(), sigdata[:4]) {
return &method, nil return &method, nil

View file

@ -711,5 +711,14 @@ func TestABI_MethodById(t *testing.T) {
t.Errorf("Method %v (id %v) not 'findable' by id in ABI", name, common.ToHex(m.Id())) t.Errorf("Method %v (id %v) not 'findable' by id in ABI", name, common.ToHex(m.Id()))
} }
} }
// Also test empty
if _, err := abi.MethodById([]byte{0x00}); err == nil {
t.Errorf("Expected error, too short to decode data")
}
if _, err := abi.MethodById([]byte{}); err == nil {
t.Errorf("Expected error, too short to decode data")
}
if _, err := abi.MethodById(nil); err == nil {
t.Errorf("Expected error, nil is short to decode data")
}
} }

View file

@ -208,7 +208,7 @@ func (b *SimulatedBackend) PendingNonceAt(ctx context.Context, account common.Ad
} }
// SuggestGasPrice implements ContractTransactor.SuggestGasPrice. Since the simulated // SuggestGasPrice implements ContractTransactor.SuggestGasPrice. Since the simulated
// chain doens't have miners, we just return a gas price of 1 for any call. // chain doesn't have miners, we just return a gas price of 1 for any call.
func (b *SimulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) { func (b *SimulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
return big.NewInt(1), nil return big.NewInt(1), nil
} }

View file

@ -1040,7 +1040,7 @@ func xgoTool(args []string) *exec.Cmd {
func doPurge(cmdline []string) { func doPurge(cmdline []string) {
var ( var (
store = flag.String("store", "", `Destination from where to purge archives (usually "gethstore/builds")`) store = flag.String("store", "", `Destination from where to purge archives (usually "gethstore/builds")`)
limit = flag.Int("days", 30, `Age threshold above which to delete unstalbe archives`) limit = flag.Int("days", 30, `Age threshold above which to delete unstable archives`)
) )
flag.CommandLine.Parse(cmdline) flag.CommandLine.Parse(cmdline)

View file

@ -1,5 +1,21 @@
### Changelog for internal API (ui-api) ### Changelog for internal API (ui-api)
### 2.1.0
* Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords.
The following structures are used:
```golang
UserInputRequest struct {
Prompt string `json:"prompt"`
Title string `json:"title"`
IsPassword bool `json:"isPassword"`
}
UserInputResponse struct {
Text string `json:"text"`
}
```
### 2.0.0 ### 2.0.0
* Modify how `call_info` on a transaction is conveyed. New format: * Modify how `call_info` on a transaction is conveyed. New format:

View file

@ -130,7 +130,7 @@ func accessNewACT(ctx *cli.Context) {
if err != nil { if err != nil {
utils.Fatalf("had an error reading the grantee public key list") utils.Fatalf("had an error reading the grantee public key list")
} }
pkGrantees = strings.Split(string(bytes), "\n") pkGrantees = strings.Split(strings.Trim(string(bytes), "\n"), "\n")
} }
if passGranteesFilename != "" { if passGranteesFilename != "" {
@ -138,7 +138,7 @@ func accessNewACT(ctx *cli.Context) {
if err != nil { if err != nil {
utils.Fatalf("could not read password filename: %v", err) utils.Fatalf("could not read password filename: %v", err)
} }
passGrantees = strings.Split(string(bytes), "\n") passGrantees = strings.Split(strings.Trim(string(bytes), "\n"), "\n")
} }
accessKey, ae, actManifest, err = api.DoACT(ctx, privateKey, salt, pkGrantees, passGrantees) accessKey, ae, actManifest, err = api.DoACT(ctx, privateKey, salt, pkGrantees, passGrantees)
if err != nil { if err != nil {

View file

@ -59,27 +59,28 @@ var (
//constants for environment variables //constants for environment variables
const ( const (
SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR" SWARM_ENV_CHEQUEBOOK_ADDR = "SWARM_CHEQUEBOOK_ADDR"
SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT" SWARM_ENV_ACCOUNT = "SWARM_ACCOUNT"
SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR" SWARM_ENV_LISTEN_ADDR = "SWARM_LISTEN_ADDR"
SWARM_ENV_PORT = "SWARM_PORT" SWARM_ENV_PORT = "SWARM_PORT"
SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID" SWARM_ENV_NETWORK_ID = "SWARM_NETWORK_ID"
SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE" SWARM_ENV_SWAP_ENABLE = "SWARM_SWAP_ENABLE"
SWARM_ENV_SWAP_API = "SWARM_SWAP_API" SWARM_ENV_SWAP_API = "SWARM_SWAP_API"
SWARM_ENV_SYNC_DISABLE = "SWARM_SYNC_DISABLE" SWARM_ENV_SYNC_DISABLE = "SWARM_SYNC_DISABLE"
SWARM_ENV_SYNC_UPDATE_DELAY = "SWARM_ENV_SYNC_UPDATE_DELAY" SWARM_ENV_SYNC_UPDATE_DELAY = "SWARM_ENV_SYNC_UPDATE_DELAY"
SWARM_ENV_LIGHT_NODE_ENABLE = "SWARM_LIGHT_NODE_ENABLE" SWARM_ENV_MAX_STREAM_PEER_SERVERS = "SWARM_ENV_MAX_STREAM_PEER_SERVERS"
SWARM_ENV_DELIVERY_SKIP_CHECK = "SWARM_DELIVERY_SKIP_CHECK" SWARM_ENV_LIGHT_NODE_ENABLE = "SWARM_LIGHT_NODE_ENABLE"
SWARM_ENV_ENS_API = "SWARM_ENS_API" SWARM_ENV_DELIVERY_SKIP_CHECK = "SWARM_DELIVERY_SKIP_CHECK"
SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR" SWARM_ENV_ENS_API = "SWARM_ENS_API"
SWARM_ENV_CORS = "SWARM_CORS" SWARM_ENV_ENS_ADDR = "SWARM_ENS_ADDR"
SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES" SWARM_ENV_CORS = "SWARM_CORS"
SWARM_ENV_PSS_ENABLE = "SWARM_PSS_ENABLE" SWARM_ENV_BOOTNODES = "SWARM_BOOTNODES"
SWARM_ENV_STORE_PATH = "SWARM_STORE_PATH" SWARM_ENV_PSS_ENABLE = "SWARM_PSS_ENABLE"
SWARM_ENV_STORE_CAPACITY = "SWARM_STORE_CAPACITY" SWARM_ENV_STORE_PATH = "SWARM_STORE_PATH"
SWARM_ENV_STORE_CACHE_CAPACITY = "SWARM_STORE_CACHE_CAPACITY" SWARM_ENV_STORE_CAPACITY = "SWARM_STORE_CAPACITY"
SWARM_ACCESS_PASSWORD = "SWARM_ACCESS_PASSWORD" SWARM_ENV_STORE_CACHE_CAPACITY = "SWARM_STORE_CACHE_CAPACITY"
GETH_ENV_DATADIR = "GETH_DATADIR" SWARM_ACCESS_PASSWORD = "SWARM_ACCESS_PASSWORD"
GETH_ENV_DATADIR = "GETH_DATADIR"
) )
// These settings ensure that TOML keys use the same names as Go struct fields. // These settings ensure that TOML keys use the same names as Go struct fields.
@ -124,7 +125,7 @@ func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context) {
//get the account for the provided swarm account //get the account for the provided swarm account
prvkey := getAccount(config.BzzAccount, ctx, stack) prvkey := getAccount(config.BzzAccount, ctx, stack)
//set the resolved config path (geth --datadir) //set the resolved config path (geth --datadir)
config.Path = stack.InstanceDir() config.Path = expandPath(stack.InstanceDir())
//finally, initialize the configuration //finally, initialize the configuration
config.Init(prvkey) config.Init(prvkey)
//configuration phase completed here //configuration phase completed here
@ -182,7 +183,7 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
if ctx.GlobalIsSet(utils.DataDirFlag.Name) { if ctx.GlobalIsSet(utils.DataDirFlag.Name) {
if datadir := ctx.GlobalString(utils.DataDirFlag.Name); datadir != "" { if datadir := ctx.GlobalString(utils.DataDirFlag.Name); datadir != "" {
currentConfig.Path = datadir currentConfig.Path = expandPath(datadir)
} }
} }
@ -207,6 +208,9 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
currentConfig.SyncUpdateDelay = d currentConfig.SyncUpdateDelay = d
} }
// any value including 0 is acceptable
currentConfig.MaxStreamPeerServers = ctx.GlobalInt(SwarmMaxStreamPeerServersFlag.Name)
if ctx.GlobalIsSet(SwarmLightNodeEnabled.Name) { if ctx.GlobalIsSet(SwarmLightNodeEnabled.Name) {
currentConfig.LightNodeEnabled = true currentConfig.LightNodeEnabled = true
} }
@ -226,6 +230,10 @@ func cmdLineOverride(currentConfig *bzzapi.Config, ctx *cli.Context) *bzzapi.Con
if len(ensAPIs) == 1 && ensAPIs[0] == "" { if len(ensAPIs) == 1 && ensAPIs[0] == "" {
ensAPIs = nil ensAPIs = nil
} }
for i := range ensAPIs {
ensAPIs[i] = expandPath(ensAPIs[i])
}
currentConfig.EnsAPIs = ensAPIs currentConfig.EnsAPIs = ensAPIs
} }
@ -268,7 +276,7 @@ func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
} }
if datadir := os.Getenv(GETH_ENV_DATADIR); datadir != "" { if datadir := os.Getenv(GETH_ENV_DATADIR); datadir != "" {
currentConfig.Path = datadir currentConfig.Path = expandPath(datadir)
} }
bzzport := os.Getenv(SWARM_ENV_PORT) bzzport := os.Getenv(SWARM_ENV_PORT)
@ -304,6 +312,14 @@ func envVarsOverride(currentConfig *bzzapi.Config) (config *bzzapi.Config) {
} }
} }
if max := os.Getenv(SWARM_ENV_MAX_STREAM_PEER_SERVERS); max != "" {
m, err := strconv.Atoi(max)
if err != nil {
utils.Fatalf("invalid environment variable %s: %v", SWARM_ENV_MAX_STREAM_PEER_SERVERS, err)
}
currentConfig.MaxStreamPeerServers = m
}
if lne := os.Getenv(SWARM_ENV_LIGHT_NODE_ENABLE); lne != "" { if lne := os.Getenv(SWARM_ENV_LIGHT_NODE_ENABLE); lne != "" {
if lightnode, err := strconv.ParseBool(lne); err != nil { if lightnode, err := strconv.ParseBool(lne); err != nil {
currentConfig.LightNodeEnabled = lightnode currentConfig.LightNodeEnabled = lightnode

View file

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
// +build !windows
package main package main
import ( import (

View file

@ -116,6 +116,12 @@ var (
Usage: "Duration for sync subscriptions update after no new peers are added (default 15s)", Usage: "Duration for sync subscriptions update after no new peers are added (default 15s)",
EnvVar: SWARM_ENV_SYNC_UPDATE_DELAY, EnvVar: SWARM_ENV_SYNC_UPDATE_DELAY,
} }
SwarmMaxStreamPeerServersFlag = cli.IntFlag{
Name: "max-stream-peer-servers",
Usage: "Limit of Stream peer servers, 0 denotes unlimited",
EnvVar: SWARM_ENV_MAX_STREAM_PEER_SERVERS,
Value: 10000, // A very large default value is possible as stream servers have very small memory footprint
}
SwarmLightNodeEnabled = cli.BoolFlag{ SwarmLightNodeEnabled = cli.BoolFlag{
Name: "lightnode", Name: "lightnode",
Usage: "Enable Swarm LightNode (default false)", Usage: "Enable Swarm LightNode (default false)",
@ -197,21 +203,29 @@ var (
Usage: "Number of recent chunks cached in memory (default 5000)", Usage: "Number of recent chunks cached in memory (default 5000)",
EnvVar: SWARM_ENV_STORE_CACHE_CAPACITY, EnvVar: SWARM_ENV_STORE_CACHE_CAPACITY,
} }
SwarmResourceMultihashFlag = cli.BoolFlag{ SwarmCompressedFlag = cli.BoolFlag{
Name: "multihash", Name: "compressed",
Usage: "Determines how to interpret data for a resource update. If not present, data will be interpreted as raw, literal data that will be included in the resource", Usage: "Prints encryption keys in compressed form",
} }
SwarmResourceNameFlag = cli.StringFlag{ SwarmResourceNameFlag = cli.StringFlag{
Name: "name", Name: "name",
Usage: "User-defined name for the new resource", Usage: "User-defined name for the new resource, limited to 32 characters. If combined with topic, the resource will be a subtopic with this name",
}
SwarmResourceTopicFlag = cli.StringFlag{
Name: "topic",
Usage: "User-defined topic this resource is tracking, hex encoded. Limited to 64 hexadecimal characters",
} }
SwarmResourceDataOnCreateFlag = cli.StringFlag{ SwarmResourceDataOnCreateFlag = cli.StringFlag{
Name: "data", Name: "data",
Usage: "Initializes the resource with the given hex-encoded data. Data must be prefixed by 0x", Usage: "Initializes the resource with the given hex-encoded data. Data must be prefixed by 0x",
} }
SwarmCompressedFlag = cli.BoolFlag{ SwarmResourceManifestFlag = cli.StringFlag{
Name: "compressed", Name: "manifest",
Usage: "Prints encryption keys in compressed form", Usage: "Refers to the resource through a manifest",
}
SwarmResourceUserFlag = cli.StringFlag{
Name: "user",
Usage: "Indicates the user who updates the resource",
} }
) )
@ -341,27 +355,53 @@ func init() {
Action: resourceCreate, Action: resourceCreate,
CustomHelpTemplate: helpTemplate, CustomHelpTemplate: helpTemplate,
Name: "create", Name: "create",
Usage: "creates a new Mutable Resource", Usage: "creates and publishes a new Mutable Resource manifest",
ArgsUsage: "<frequency>", Description: `creates and publishes a new Mutable Resource manifest pointing to a specified user's updates about a particular topic.
Description: "creates a new Mutable Resource", The resource topic can be built in the following ways:
Flags: []cli.Flag{SwarmResourceNameFlag, SwarmResourceDataOnCreateFlag, SwarmResourceMultihashFlag}, * use --topic to set the topic to an arbitrary binary hex string.
* use --name to set the topic to a human-readable name.
For example --name could be set to "profile-picture", meaning this Mutable Resource allows to get this user's current profile picture.
* use both --topic and --name to create named subtopics.
For example, --topic could be set to an Ethereum contract address and --name could be set to "comments", meaning
the Mutable Resource tracks a discussion about that contract.
The --user flag allows to have this manifest refer to a user other than yourself. If not specified,
it will then default to your local account (--bzzaccount)`,
Flags: []cli.Flag{SwarmResourceNameFlag, SwarmResourceTopicFlag, SwarmResourceUserFlag},
}, },
{ {
Action: resourceUpdate, Action: resourceUpdate,
CustomHelpTemplate: helpTemplate, CustomHelpTemplate: helpTemplate,
Name: "update", Name: "update",
Usage: "updates the content of an existing Mutable Resource", Usage: "updates the content of an existing Mutable Resource",
ArgsUsage: "<Manifest Address or ENS domain> <0x Hex data>", ArgsUsage: "<0x Hex data>",
Description: "updates the content of an existing Mutable Resource", Description: `publishes a new update on the specified topic
Flags: []cli.Flag{SwarmResourceMultihashFlag}, The resource topic can be built in the following ways:
* use --topic to set the topic to an arbitrary binary hex string.
* use --name to set the topic to a human-readable name.
For example --name could be set to "profile-picture", meaning this Mutable Resource allows to get this user's current profile picture.
* use both --topic and --name to create named subtopics.
For example, --topic could be set to an Ethereum contract address and --name could be set to "comments", meaning
the Mutable Resource tracks a discussion about that contract.
If you have a manifest, you can specify it with --manifest to refer to the resource,
instead of using --topic / --name
`,
Flags: []cli.Flag{SwarmResourceManifestFlag, SwarmResourceNameFlag, SwarmResourceTopicFlag},
}, },
{ {
Action: resourceInfo, Action: resourceInfo,
CustomHelpTemplate: helpTemplate, CustomHelpTemplate: helpTemplate,
Name: "info", Name: "info",
Usage: "obtains information about an existing Mutable Resource", Usage: "obtains information about an existing Mutable Resource",
ArgsUsage: "<Manifest Address or ENS domain>", Description: `obtains information about an existing Mutable Resource
Description: "obtains information about an existing Mutable Resource", The topic can be specified directly with the --topic flag as an hex string
If no topic is specified, the default topic (zero) will be used
The --name flag can be used to specify subtopics with a specific name.
The --user flag allows to refer to a user other than yourself. If not specified,
it will then default to your local account (--bzzaccount)
If you have a manifest, you can specify it with --manifest instead of --topic / --name / ---user
to refer to the resource`,
Flags: []cli.Flag{SwarmResourceManifestFlag, SwarmResourceNameFlag, SwarmResourceTopicFlag, SwarmResourceUserFlag},
}, },
}, },
}, },
@ -542,6 +582,7 @@ pv(1) tool to get a progress bar:
SwarmSwapAPIFlag, SwarmSwapAPIFlag,
SwarmSyncDisabledFlag, SwarmSyncDisabledFlag,
SwarmSyncUpdateDelay, SwarmSyncUpdateDelay,
SwarmMaxStreamPeerServersFlag,
SwarmLightNodeEnabled, SwarmLightNodeEnabled,
SwarmDeliverySkipCheckFlag, SwarmDeliverySkipCheckFlag,
SwarmListenAddrFlag, SwarmListenAddrFlag,

View file

@ -0,0 +1,124 @@
// Copyright 2018 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/>.
package main
// Standard "mime" package rely on system-settings, see mime.osInitMime
// Swarm will run on many OS/Platform/Docker and must behave similar
// This command generates code to add common mime types based on mime.types file
//
// mime.types file provided by mailcap, which follow https://www.iana.org/assignments/media-types/media-types.xhtml
//
// Get last version of mime.types file by:
// docker run --rm -v $(pwd):/tmp alpine:edge /bin/sh -c "apk add -U mailcap; mv /etc/mime.types /tmp"
import (
"bufio"
"bytes"
"flag"
"html/template"
"io/ioutil"
"strings"
"log"
)
var (
typesFlag = flag.String("types", "", "Input mime.types file")
packageFlag = flag.String("package", "", "Golang package in output file")
outFlag = flag.String("out", "", "Output file name for the generated mime types")
)
type mime struct {
Name string
Exts []string
}
type templateParams struct {
PackageName string
Mimes []mime
}
func main() {
// Parse and ensure all needed inputs are specified
flag.Parse()
if *typesFlag == "" {
log.Fatalf("--types is required")
}
if *packageFlag == "" {
log.Fatalf("--types is required")
}
if *outFlag == "" {
log.Fatalf("--out is required")
}
params := templateParams{
PackageName: *packageFlag,
}
types, err := ioutil.ReadFile(*typesFlag)
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(bytes.NewReader(types))
for scanner.Scan() {
txt := scanner.Text()
if strings.HasPrefix(txt, "#") || len(txt) == 0 {
continue
}
parts := strings.Fields(txt)
if len(parts) == 1 {
continue
}
params.Mimes = append(params.Mimes, mime{parts[0], parts[1:]})
}
if err = scanner.Err(); err != nil {
log.Fatal(err)
}
result := bytes.NewBuffer([]byte{})
if err := template.Must(template.New("_").Parse(tpl)).Execute(result, params); err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile(*outFlag, result.Bytes(), 0600); err != nil {
log.Fatal(err)
}
}
var tpl = `// Code generated by github.com/ethereum/go-ethereum/cmd/swarm/mimegen. DO NOT EDIT.
package {{ .PackageName }}
import "mime"
func init() {
var mimeTypes = map[string]string{
{{- range .Mimes -}}
{{ $name := .Name -}}
{{- range .Exts }}
".{{ . }}": "{{ $name | html }}",
{{- end }}
{{- end }}
}
for ext, name := range mimeTypes {
if err := mime.AddExtensionType(ext, name); err != nil {
panic(err)
}
}
}
`

1828
cmd/swarm/mimegen/mime.types Normal file

File diff suppressed because it is too large Load diff

View file

@ -19,10 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
swarm "github.com/ethereum/go-ethereum/swarm/api/client" swarm "github.com/ethereum/go-ethereum/swarm/api/client"
@ -34,62 +35,38 @@ func NewGenericSigner(ctx *cli.Context) mru.Signer {
return mru.NewGenericSigner(getPrivKey(ctx)) return mru.NewGenericSigner(getPrivKey(ctx))
} }
func getTopic(ctx *cli.Context) (topic mru.Topic) {
var name = ctx.String(SwarmResourceNameFlag.Name)
var relatedTopic = ctx.String(SwarmResourceTopicFlag.Name)
var relatedTopicBytes []byte
var err error
if relatedTopic != "" {
relatedTopicBytes, err = hexutil.Decode(relatedTopic)
if err != nil {
utils.Fatalf("Error parsing topic: %s", err)
}
}
topic, err = mru.NewTopic(name, relatedTopicBytes)
if err != nil {
utils.Fatalf("Error parsing topic: %s", err)
}
return topic
}
// swarm resource create <frequency> [--name <name>] [--data <0x Hexdata> [--multihash=false]] // 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 update <Manifest Address or ENS domain> <0x Hexdata> [--multihash=false]
// swarm resource info <Manifest Address or ENS domain> // swarm resource info <Manifest Address or ENS domain>
func resourceCreate(ctx *cli.Context) { func resourceCreate(ctx *cli.Context) {
args := ctx.Args()
var ( var (
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
client = swarm.NewClient(bzzapi) client = swarm.NewClient(bzzapi)
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name)
initialData = ctx.String(SwarmResourceDataOnCreateFlag.Name)
name = ctx.String(SwarmResourceNameFlag.Name)
) )
if len(args) < 1 { newResourceRequest := mru.NewFirstRequest(getTopic(ctx))
fmt.Println("Incorrect number of arguments") newResourceRequest.View.User = resourceGetUser(ctx)
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) manifestAddress, err := client.CreateResource(newResourceRequest)
if err != nil { if err != nil {
@ -104,32 +81,43 @@ func resourceUpdate(ctx *cli.Context) {
args := ctx.Args() args := ctx.Args()
var ( var (
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
client = swarm.NewClient(bzzapi) client = swarm.NewClient(bzzapi)
multihash = ctx.Bool(SwarmResourceMultihashFlag.Name) manifestAddressOrDomain = ctx.String(SwarmResourceManifestFlag.Name)
) )
if len(args) < 2 { if len(args) < 1 {
fmt.Println("Incorrect number of arguments") fmt.Println("Incorrect number of arguments")
cli.ShowCommandHelpAndExit(ctx, "update", 1) cli.ShowCommandHelpAndExit(ctx, "update", 1)
return return
} }
signer := NewGenericSigner(ctx) signer := NewGenericSigner(ctx)
manifestAddressOrDomain := args[0]
data, err := hexutil.Decode(args[1]) data, err := hexutil.Decode(args[0])
if err != nil { if err != nil {
utils.Fatalf("Error parsing data: %s", err.Error()) utils.Fatalf("Error parsing data: %s", err.Error())
return return
} }
var updateRequest *mru.Request
var query *mru.Query
if manifestAddressOrDomain == "" {
query = new(mru.Query)
query.User = signer.Address()
query.Topic = getTopic(ctx)
}
// Retrieve resource status and metadata out of the manifest // Retrieve resource status and metadata out of the manifest
updateRequest, err := client.GetResourceMetadata(manifestAddressOrDomain) updateRequest, err = client.GetResourceMetadata(query, manifestAddressOrDomain)
if err != nil { if err != nil {
utils.Fatalf("Error retrieving resource status: %s", err.Error()) utils.Fatalf("Error retrieving resource status: %s", err.Error())
} }
// set the new data // set the new data
updateRequest.SetData(data, multihash) updateRequest.SetData(data)
// sign update // sign update
if err = updateRequest.Sign(signer); err != nil { if err = updateRequest.Sign(signer); err != nil {
@ -146,17 +134,19 @@ func resourceUpdate(ctx *cli.Context) {
func resourceInfo(ctx *cli.Context) { func resourceInfo(ctx *cli.Context) {
var ( var (
bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/")
client = swarm.NewClient(bzzapi) client = swarm.NewClient(bzzapi)
manifestAddressOrDomain = ctx.String(SwarmResourceManifestFlag.Name)
) )
args := ctx.Args()
if len(args) < 1 { var query *mru.Query
fmt.Println("Incorrect number of arguments.") if manifestAddressOrDomain == "" {
cli.ShowCommandHelpAndExit(ctx, "info", 1) query = new(mru.Query)
return query.Topic = getTopic(ctx)
query.User = resourceGetUser(ctx)
} }
manifestAddressOrDomain := args[0]
metadata, err := client.GetResourceMetadata(manifestAddressOrDomain) metadata, err := client.GetResourceMetadata(query, manifestAddressOrDomain)
if err != nil { if err != nil {
utils.Fatalf("Error retrieving resource metadata: %s", err.Error()) utils.Fatalf("Error retrieving resource metadata: %s", err.Error())
return return
@ -167,3 +157,16 @@ func resourceInfo(ctx *cli.Context) {
} }
fmt.Println(string(encodedMetadata)) fmt.Println(string(encodedMetadata))
} }
func resourceGetUser(ctx *cli.Context) common.Address {
var user = ctx.String(SwarmResourceUserFlag.Name)
if user != "" {
return common.HexToAddress(user)
}
pk := getPrivKey(ctx)
if pk == nil {
utils.Fatalf("Cannot read private key. Must specify --user or --bzzaccount")
}
return crypto.PubkeyToAddress(pk.PublicKey)
}

182
cmd/swarm/mru_test.go Normal file
View file

@ -0,0 +1,182 @@
// Copyright 2017 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/>.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/ethereum/go-ethereum/swarm/testutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/swarm/storage/mru"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http"
)
func TestCLIResourceUpdate(t *testing.T) {
srv := testutil.NewTestSwarmServer(t, func(api *api.API) testutil.TestServer {
return swarmhttp.NewServer(api, "")
}, nil)
log.Info("starting 1 node cluster")
defer srv.Close()
// create a private key file for signing
pkfile, err := ioutil.TempFile("", "swarm-test")
if err != nil {
t.Fatal(err)
}
defer pkfile.Close()
defer os.Remove(pkfile.Name())
privkeyHex := "0000000000000000000000000000000000000000000000000000000000001979"
privKey, _ := crypto.HexToECDSA(privkeyHex)
address := crypto.PubkeyToAddress(privKey.PublicKey)
// save the private key to a file
_, err = io.WriteString(pkfile, privkeyHex)
if err != nil {
t.Fatal(err)
}
// compose a topic. We'll be doing quotes about Miguel de Cervantes
var topic mru.Topic
subject := []byte("Miguel de Cervantes")
copy(topic[:], subject[:])
name := "quotes"
// prepare some data for the update
data := []byte("En boca cerrada no entran moscas")
hexData := hexutil.Encode(data)
flags := []string{
"--bzzapi", srv.URL,
"--bzzaccount", pkfile.Name(),
"resource", "update",
"--topic", topic.Hex(),
"--name", name,
hexData}
// create an update and expect an exit without errors
log.Info(fmt.Sprintf("updating a resource with 'swarm resource update'"))
cmd := runSwarm(t, flags...)
cmd.ExpectExit()
// now try to get the update using the client
client := swarm.NewClient(srv.URL)
if err != nil {
t.Fatal(err)
}
// build the same topic as before, this time
// we use NewTopic to create a topic automatically.
topic, err = mru.NewTopic(name, subject)
if err != nil {
t.Fatal(err)
}
// View configures whose updates we will be looking up.
view := mru.View{
Topic: topic,
User: address,
}
// Build a query to get the latest update
query := mru.NewQueryLatest(&view, lookup.NoClue)
// retrieve content!
reader, err := client.GetResource(query, "")
if err != nil {
t.Fatal(err)
}
retrieved, err := ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
// check we retrieved the sent information
if !bytes.Equal(data, retrieved) {
t.Fatalf("Received %s, expected %s", retrieved, data)
}
// Now retrieve info for the next update
flags = []string{
"--bzzapi", srv.URL,
"resource", "info",
"--topic", topic.Hex(),
"--user", address.Hex(),
}
log.Info(fmt.Sprintf("getting resource info with 'swarm resource info'"))
cmd = runSwarm(t, flags...)
_, matches := cmd.ExpectRegexp(`.*`) // regex hack to extract stdout
cmd.ExpectExit()
// verify we can deserialize the result as a valid JSON
var request mru.Request
err = json.Unmarshal([]byte(matches[0]), &request)
if err != nil {
t.Fatal(err)
}
// make sure the retrieved view is the same
if request.View != view {
t.Fatalf("Expected view to be: %s, got %s", view, request.View)
}
// test publishing a manifest
flags = []string{
"--bzzapi", srv.URL,
"--bzzaccount", pkfile.Name(),
"resource", "create",
"--topic", topic.Hex(),
}
log.Info(fmt.Sprintf("Publishing manifest with 'swarm resource create'"))
cmd = runSwarm(t, flags...)
_, matches = cmd.ExpectRegexp(`[a-f\d]{64}`) // regex hack to extract stdout
cmd.ExpectExit()
manifestAddress := matches[0] // read the received resource manifest
// now attempt to lookup the latest update using a manifest instead
reader, err = client.GetResource(nil, manifestAddress)
if err != nil {
t.Fatal(err)
}
retrieved, err = ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, retrieved) {
t.Fatalf("Received %s, expected %s", retrieved, data)
}
}

View file

@ -242,7 +242,7 @@ func existingTestNode(t *testing.T, dir string, bzzaccount string) *testNode {
"--bzzaccount", bzzaccount, "--bzzaccount", bzzaccount,
"--bzznetworkid", "321", "--bzznetworkid", "321",
"--bzzport", httpPort, "--bzzport", httpPort,
"--verbosity", "3", "--verbosity", fmt.Sprint(*loglevel),
) )
node.Cmd.InputLine(testPassphrase) node.Cmd.InputLine(testPassphrase)
defer func() { defer func() {
@ -318,7 +318,7 @@ func newTestNode(t *testing.T, dir string) *testNode {
"--bzzaccount", account.Address.String(), "--bzzaccount", account.Address.String(),
"--bzznetworkid", "321", "--bzznetworkid", "321",
"--bzzport", httpPort, "--bzzport", httpPort,
"--verbosity", "3", "--verbosity", fmt.Sprint(*loglevel),
) )
node.Cmd.InputLine(testPassphrase) node.Cmd.InputLine(testPassphrase)
defer func() { defer func() {

View file

@ -22,16 +22,15 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime"
"net/http"
"os" "os"
"os/user" "os/user"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/ethereum/go-ethereum/cmd/utils"
swarm "github.com/ethereum/go-ethereum/swarm/api/client" swarm "github.com/ethereum/go-ethereum/swarm/api/client"
"github.com/ethereum/go-ethereum/cmd/utils"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
@ -118,10 +117,9 @@ func upload(ctx *cli.Context) {
return "", fmt.Errorf("error opening file: %s", err) return "", fmt.Errorf("error opening file: %s", err)
} }
defer f.Close() defer f.Close()
if mimeType == "" { if mimeType != "" {
mimeType = detectMimeType(file) f.ContentType = mimeType
} }
f.ContentType = mimeType
return client.Upload(f, "", toEncrypt) return client.Upload(f, "", toEncrypt)
} }
} }
@ -138,6 +136,12 @@ func upload(ctx *cli.Context) {
// 3. cleans the path, e.g. /a/b/../c -> /a/c // 3. cleans the path, e.g. /a/b/../c -> /a/c
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded // Note, it has limitations, e.g. ~someuser/tmp will not be expanded
func expandPath(p string) string { func expandPath(p string) string {
if i := strings.Index(p, ":"); i > 0 {
return p
}
if i := strings.Index(p, "@"); i > 0 {
return p
}
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
if home := homeDir(); home != "" { if home := homeDir(); home != "" {
p = home + p[1:] p = home + p[1:]
@ -155,19 +159,3 @@ func homeDir() string {
} }
return "" return ""
} }
func detectMimeType(file string) string {
if ext := filepath.Ext(file); ext != "" {
return mime.TypeByExtension(ext)
}
f, err := os.Open(file)
if err != nil {
return ""
}
defer f.Close()
buf := make([]byte, 512)
if n, _ := f.Read(buf); n > 0 {
return http.DetectContentType(buf)
}
return ""
}

View file

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
// +build !windows
package main package main
import ( import (
@ -32,7 +34,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
swarm "github.com/ethereum/go-ethereum/swarm/api/client" swarm "github.com/ethereum/go-ethereum/swarm/api/client"
colorable "github.com/mattn/go-colorable" "github.com/mattn/go-colorable"
) )
var loglevel = flag.Int("loglevel", 3, "verbosity of logs") var loglevel = flag.Int("loglevel", 3, "verbosity of logs")

View file

@ -151,6 +151,38 @@ func (self *ENS) Resolve(name string) (common.Hash, error) {
return common.BytesToHash(ret[:]), nil return common.BytesToHash(ret[:]), nil
} }
// Addr is a non-transactional call that returns the address associated with a name.
func (self *ENS) Addr(name string) (common.Address, error) {
node := EnsNode(name)
resolver, err := self.getResolver(node)
if err != nil {
return common.Address{}, err
}
ret, err := resolver.Addr(node)
if err != nil {
return common.Address{}, err
}
return common.BytesToAddress(ret[:]), nil
}
// SetAddress sets the address associated with a name. Only works if the caller
// owns the name, and the associated resolver implements a `setAddress` function.
func (self *ENS) SetAddr(name string, addr common.Address) (*types.Transaction, error) {
node := EnsNode(name)
resolver, err := self.getResolver(node)
if err != nil {
return nil, err
}
opts := self.TransactOpts
opts.GasLimit = 200000
return resolver.Contract.SetAddr(&opts, node, addr)
}
// Register registers a new domain name for the caller, making them the owner of the new name. // Register registers a new domain name for the caller, making them the owner of the new name.
// Only works if the registrar for the parent domain implements the FIFS registrar protocol. // Only works if the registrar for the parent domain implements the FIFS registrar protocol.
func (self *ENS) Register(name string) (*types.Transaction, error) { func (self *ENS) Register(name string) (*types.Transaction, error) {

View file

@ -22,16 +22,18 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/contracts/ens/contract" "github.com/ethereum/go-ethereum/contracts/ens/contract"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
) )
var ( var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
name = "my name on ENS" name = "my name on ENS"
hash = crypto.Keccak256Hash([]byte("my content")) hash = crypto.Keccak256Hash([]byte("my content"))
addr = crypto.PubkeyToAddress(key.PublicKey) addr = crypto.PubkeyToAddress(key.PublicKey)
testAddr = common.HexToAddress("0x1234123412341234123412341234123412341234")
) )
func TestENS(t *testing.T) { func TestENS(t *testing.T) {
@ -74,4 +76,19 @@ func TestENS(t *testing.T) {
if vhost != hash { if vhost != hash {
t.Fatalf("resolve error, expected %v, got %v", hash.Hex(), vhost.Hex()) t.Fatalf("resolve error, expected %v, got %v", hash.Hex(), vhost.Hex())
} }
// set the address for the name
if _, err = ens.SetAddr(name, testAddr); err != nil {
t.Fatalf("can't set address: %v", err)
}
contractBackend.Commit()
// Try to resolve the name to an address
recoveredAddr, err := ens.Addr(name)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if vhost != hash {
t.Fatalf("resolve error, expected %v, got %v", testAddr.Hex(), recoveredAddr.Hex())
}
} }

View file

@ -55,6 +55,7 @@ var (
const ( const (
bodyCacheLimit = 256 bodyCacheLimit = 256
blockCacheLimit = 256 blockCacheLimit = 256
receiptsCacheLimit = 32
maxFutureBlocks = 256 maxFutureBlocks = 256
maxTimeFutureBlocks = 30 maxTimeFutureBlocks = 30
badBlockLimit = 10 badBlockLimit = 10
@ -111,11 +112,12 @@ type BlockChain struct {
currentBlock atomic.Value // Current head of the block chain currentBlock atomic.Value // Current head of the block chain
currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!) currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!)
stateCache state.Database // State database to reuse between imports (contains state cache) stateCache state.Database // State database to reuse between imports (contains state cache)
bodyCache *lru.Cache // Cache for the most recent block bodies bodyCache *lru.Cache // Cache for the most recent block bodies
bodyRLPCache *lru.Cache // Cache for the most recent block bodies in RLP encoded format bodyRLPCache *lru.Cache // Cache for the most recent block bodies in RLP encoded format
blockCache *lru.Cache // Cache for the most recent entire blocks receiptsCache *lru.Cache // Cache for the most recent receipts per block
futureBlocks *lru.Cache // future blocks are blocks added for later processing blockCache *lru.Cache // Cache for the most recent entire blocks
futureBlocks *lru.Cache // future blocks are blocks added for later processing
quit chan struct{} // blockchain quit channel quit chan struct{} // blockchain quit channel
running int32 // running must be called atomically running int32 // running must be called atomically
@ -144,6 +146,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
} }
bodyCache, _ := lru.New(bodyCacheLimit) bodyCache, _ := lru.New(bodyCacheLimit)
bodyRLPCache, _ := lru.New(bodyCacheLimit) bodyRLPCache, _ := lru.New(bodyCacheLimit)
receiptsCache, _ := lru.New(receiptsCacheLimit)
blockCache, _ := lru.New(blockCacheLimit) blockCache, _ := lru.New(blockCacheLimit)
futureBlocks, _ := lru.New(maxFutureBlocks) futureBlocks, _ := lru.New(maxFutureBlocks)
badBlocks, _ := lru.New(badBlockLimit) badBlocks, _ := lru.New(badBlockLimit)
@ -158,6 +161,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
shouldPreserve: shouldPreserve, shouldPreserve: shouldPreserve,
bodyCache: bodyCache, bodyCache: bodyCache,
bodyRLPCache: bodyRLPCache, bodyRLPCache: bodyRLPCache,
receiptsCache: receiptsCache,
blockCache: blockCache, blockCache: blockCache,
futureBlocks: futureBlocks, futureBlocks: futureBlocks,
engine: engine, engine: engine,
@ -280,6 +284,7 @@ func (bc *BlockChain) SetHead(head uint64) error {
// Clear out any stale content from the caches // Clear out any stale content from the caches
bc.bodyCache.Purge() bc.bodyCache.Purge()
bc.bodyRLPCache.Purge() bc.bodyRLPCache.Purge()
bc.receiptsCache.Purge()
bc.blockCache.Purge() bc.blockCache.Purge()
bc.futureBlocks.Purge() bc.futureBlocks.Purge()
@ -603,11 +608,18 @@ func (bc *BlockChain) GetBlockByNumber(number uint64) *types.Block {
// GetReceiptsByHash retrieves the receipts for all transactions in a given block. // GetReceiptsByHash retrieves the receipts for all transactions in a given block.
func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts {
if receipts, ok := bc.receiptsCache.Get(hash); ok {
return receipts.(types.Receipts)
}
number := rawdb.ReadHeaderNumber(bc.db, hash) number := rawdb.ReadHeaderNumber(bc.db, hash)
if number == nil { if number == nil {
return nil return nil
} }
return rawdb.ReadReceipts(bc.db, hash, *number)
receipts := rawdb.ReadReceipts(bc.db, hash, *number)
bc.receiptsCache.Add(hash, receipts)
return receipts
} }
// GetBlocksFromHash returns the block corresponding to hash and up to n-1 ancestors. // GetBlocksFromHash returns the block corresponding to hash and up to n-1 ancestors.

View file

@ -153,16 +153,21 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
if err := dec.UnmarshalJSON(input); err != nil { if err := dec.UnmarshalJSON(input); err != nil {
return err return err
} }
var V byte
if isProtectedV(dec.V) { withSignature := dec.V.Sign() != 0 || dec.R.Sign() != 0 || dec.S.Sign() != 0
chainID := deriveChainId(dec.V).Uint64() if withSignature {
V = byte(dec.V.Uint64() - 35 - 2*chainID) var V byte
} else { if isProtectedV(dec.V) {
V = byte(dec.V.Uint64() - 27) chainID := deriveChainId(dec.V).Uint64()
} V = byte(dec.V.Uint64() - 35 - 2*chainID)
if !crypto.ValidateSignatureValues(V, dec.R, dec.S, false) { } else {
return ErrInvalidSig V = byte(dec.V.Uint64() - 27)
}
if !crypto.ValidateSignatureValues(V, dec.R, dec.S, false) {
return ErrInvalidSig
}
} }
*tx = Transaction{data: dec} *tx = Transaction{data: dec}
return nil return nil
} }

View file

@ -227,13 +227,13 @@ func recoverPlain(sighash common.Hash, R, S, Vb *big.Int, homestead bool) (commo
if !crypto.ValidateSignatureValues(V, R, S, homestead) { if !crypto.ValidateSignatureValues(V, R, S, homestead) {
return common.Address{}, ErrInvalidSig return common.Address{}, ErrInvalidSig
} }
// encode the snature in uncompressed format // encode the signature in uncompressed format
r, s := R.Bytes(), S.Bytes() r, s := R.Bytes(), S.Bytes()
sig := make([]byte, 65) sig := make([]byte, 65)
copy(sig[32-len(r):32], r) copy(sig[32-len(r):32], r)
copy(sig[64-len(s):64], s) copy(sig[64-len(s):64], s)
sig[64] = V sig[64] = V
// recover the public key from the snature // recover the public key from the signature
pub, err := crypto.Ecrecover(sighash[:], sig) pub, err := crypto.Ecrecover(sighash[:], sig)
if err != nil { if err != nil {
return common.Address{}, err return common.Address{}, err

View file

@ -185,6 +185,7 @@ func TestTransactionJSON(t *testing.T) {
} }
signer := NewEIP155Signer(common.Big1) signer := NewEIP155Signer(common.Big1)
transactions := make([]*Transaction, 0, 50)
for i := uint64(0); i < 25; i++ { for i := uint64(0); i < 25; i++ {
var tx *Transaction var tx *Transaction
switch i % 2 { switch i % 2 {
@ -193,20 +194,25 @@ func TestTransactionJSON(t *testing.T) {
case 1: case 1:
tx = NewContractCreation(i, common.Big0, 1, common.Big2, []byte("abcdef")) tx = NewContractCreation(i, common.Big0, 1, common.Big2, []byte("abcdef"))
} }
transactions = append(transactions, tx)
tx, err := SignTx(tx, signer, key) signedTx, err := SignTx(tx, signer, key)
if err != nil { if err != nil {
t.Fatalf("could not sign transaction: %v", err) t.Fatalf("could not sign transaction: %v", err)
} }
transactions = append(transactions, signedTx)
}
for _, tx := range transactions {
data, err := json.Marshal(tx) data, err := json.Marshal(tx)
if err != nil { if err != nil {
t.Errorf("json.Marshal failed: %v", err) t.Fatalf("json.Marshal failed: %v", err)
} }
var parsedTx *Transaction var parsedTx *Transaction
if err := json.Unmarshal(data, &parsedTx); err != nil { if err := json.Unmarshal(data, &parsedTx); err != nil {
t.Errorf("json.Unmarshal failed: %v", err) t.Fatalf("json.Unmarshal failed: %v", err)
} }
// compare nonce, price, gaslimit, recipient, amount, payload, V, R, S // compare nonce, price, gaslimit, recipient, amount, payload, V, R, S

View file

@ -67,6 +67,15 @@ func (api *PublicEthereumAPI) Hashrate() hexutil.Uint64 {
return hexutil.Uint64(api.e.Miner().HashRate()) return hexutil.Uint64(api.e.Miner().HashRate())
} }
// ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config.
func (api *PublicEthereumAPI) ChainId() hexutil.Uint64 {
chainID := new(big.Int)
if config := api.e.chainConfig; config.IsEIP155(api.e.blockchain.CurrentBlock().Number()) {
chainID = config.ChainID
}
return (hexutil.Uint64)(chainID.Uint64())
}
// PublicMinerAPI provides an API to control the miner. // PublicMinerAPI provides an API to control the miner.
// It offers only methods that operate on data that pose no security risk when it is publicly accessible. // It offers only methods that operate on data that pose no security risk when it is publicly accessible.
type PublicMinerAPI struct { type PublicMinerAPI struct {

View file

@ -25,7 +25,6 @@ import (
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/bloombits" "github.com/ethereum/go-ethereum/core/bloombits"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
@ -107,18 +106,11 @@ func (b *EthAPIBackend) GetBlock(ctx context.Context, hash common.Hash) (*types.
} }
func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
if number := rawdb.ReadHeaderNumber(b.eth.chainDb, hash); number != nil { return b.eth.blockchain.GetReceiptsByHash(hash), nil
return rawdb.ReadReceipts(b.eth.chainDb, hash, *number), nil
}
return nil, nil
} }
func (b *EthAPIBackend) GetLogs(ctx context.Context, hash common.Hash) ([][]*types.Log, error) { func (b *EthAPIBackend) GetLogs(ctx context.Context, hash common.Hash) ([][]*types.Log, error) {
number := rawdb.ReadHeaderNumber(b.eth.chainDb, hash) receipts := b.eth.blockchain.GetReceiptsByHash(hash)
if number == nil {
return nil, nil
}
receipts := rawdb.ReadReceipts(b.eth.chainDb, hash, *number)
if receipts == nil { if receipts == nil {
return nil, nil return nil, nil
} }

View file

@ -658,8 +658,10 @@ func (d *Downloader) findAncestor(p *peerConnection, height uint64) (uint64, err
continue continue
} }
// Otherwise check if we already know the header or not // Otherwise check if we already know the header or not
if (d.mode == FullSync && d.blockchain.HasBlock(headers[i].Hash(), headers[i].Number.Uint64())) || (d.mode != FullSync && d.lightchain.HasHeader(headers[i].Hash(), headers[i].Number.Uint64())) { h := headers[i].Hash()
number, hash = headers[i].Number.Uint64(), headers[i].Hash() n := headers[i].Number.Uint64()
if (d.mode == FullSync && d.blockchain.HasBlock(h, n)) || (d.mode != FullSync && d.lightchain.HasHeader(h, n)) {
number, hash = n, h
// If every header is known, even future ones, the peer straight out lied about its head // If every header is known, even future ones, the peer straight out lied about its head
if number > height && i == limit-1 { if number > height && i == limit-1 {
@ -723,11 +725,13 @@ func (d *Downloader) findAncestor(p *peerConnection, height uint64) (uint64, err
arrived = true arrived = true
// Modify the search interval based on the response // Modify the search interval based on the response
if (d.mode == FullSync && !d.blockchain.HasBlock(headers[0].Hash(), headers[0].Number.Uint64())) || (d.mode != FullSync && !d.lightchain.HasHeader(headers[0].Hash(), headers[0].Number.Uint64())) { h := headers[0].Hash()
n := headers[0].Number.Uint64()
if (d.mode == FullSync && !d.blockchain.HasBlock(h, n)) || (d.mode != FullSync && !d.lightchain.HasHeader(h, n)) {
end = check end = check
break break
} }
header := d.lightchain.GetHeaderByHash(headers[0].Hash()) // Independent of sync mode, header surely exists header := d.lightchain.GetHeaderByHash(h) // Independent of sync mode, header surely exists
if header.Number.Uint64() != check { if header.Number.Uint64() != check {
p.log.Debug("Received non requested header", "number", header.Number, "hash", header.Hash(), "request", check) p.log.Debug("Received non requested header", "number", header.Number, "hash", header.Hash(), "request", check)
return 0, errBadPeer return 0, errBadPeer

View file

@ -49,6 +49,9 @@ const (
// txChanSize is the size of channel listening to NewTxsEvent. // txChanSize is the size of channel listening to NewTxsEvent.
// The number is referenced from the size of tx pool. // The number is referenced from the size of tx pool.
txChanSize = 4096 txChanSize = 4096
// minimim number of peers to broadcast new blocks to
minBroadcastPeers = 4
) )
var ( var (
@ -705,7 +708,14 @@ func (pm *ProtocolManager) BroadcastBlock(block *types.Block, propagate bool) {
return return
} }
// Send the block to a subset of our peers // Send the block to a subset of our peers
transfer := peers[:int(math.Sqrt(float64(len(peers))))] transferLen := int(math.Sqrt(float64(len(peers))))
if transferLen < minBroadcastPeers {
transferLen = minBroadcastPeers
}
if transferLen > len(peers) {
transferLen = len(peers)
}
transfer := peers[:transferLen]
for _, peer := range transfer { for _, peer := range transfer {
peer.AsyncSendNewBlock(block, td) peer.AsyncSendNewBlock(block, td)
} }

View file

@ -17,6 +17,7 @@
package eth package eth
import ( import (
"fmt"
"math" "math"
"math/big" "math/big"
"math/rand" "math/rand"
@ -466,14 +467,17 @@ func testDAOChallenge(t *testing.T, localForked, remoteForked bool, timeout bool
} }
// Create a DAO aware protocol manager // Create a DAO aware protocol manager
var ( var (
evmux = new(event.TypeMux) evmux = new(event.TypeMux)
pow = ethash.NewFaker() pow = ethash.NewFaker()
db = ethdb.NewMemDatabase() db = ethdb.NewMemDatabase()
config = &params.ChainConfig{DAOForkBlock: big.NewInt(1), DAOForkSupport: localForked} config = &params.ChainConfig{DAOForkBlock: big.NewInt(1), DAOForkSupport: localForked}
gspec = &core.Genesis{Config: config} gspec = &core.Genesis{Config: config}
genesis = gspec.MustCommit(db) genesis = gspec.MustCommit(db)
blockchain, _ = core.NewBlockChain(db, nil, config, pow, vm.Config{}, nil)
) )
blockchain, err := core.NewBlockChain(db, nil, config, pow, vm.Config{}, nil)
if err != nil {
t.Fatalf("failed to create new blockchain: %v", err)
}
pm, err := NewProtocolManager(config, downloader.FullSync, DefaultConfig.NetworkId, evmux, new(testTxPool), pow, blockchain, db) pm, err := NewProtocolManager(config, downloader.FullSync, DefaultConfig.NetworkId, evmux, new(testTxPool), pow, blockchain, db)
if err != nil { if err != nil {
t.Fatalf("failed to start test protocol manager: %v", err) t.Fatalf("failed to start test protocol manager: %v", err)
@ -520,3 +524,90 @@ func testDAOChallenge(t *testing.T, localForked, remoteForked bool, timeout bool
} }
} }
} }
func TestBroadcastBlock(t *testing.T) {
var tests = []struct {
totalPeers int
broadcastExpected int
}{
{1, 1},
{2, 2},
{3, 3},
{4, 4},
{5, 4},
{9, 4},
{12, 4},
{16, 4},
{26, 5},
{100, 10},
}
for _, test := range tests {
testBroadcastBlock(t, test.totalPeers, test.broadcastExpected)
}
}
func testBroadcastBlock(t *testing.T, totalPeers, broadcastExpected int) {
var (
evmux = new(event.TypeMux)
pow = ethash.NewFaker()
db = ethdb.NewMemDatabase()
config = &params.ChainConfig{}
gspec = &core.Genesis{Config: config}
genesis = gspec.MustCommit(db)
)
blockchain, err := core.NewBlockChain(db, nil, config, pow, vm.Config{}, nil)
if err != nil {
t.Fatalf("failed to create new blockchain: %v", err)
}
pm, err := NewProtocolManager(config, downloader.FullSync, DefaultConfig.NetworkId, evmux, new(testTxPool), pow, blockchain, db)
if err != nil {
t.Fatalf("failed to start test protocol manager: %v", err)
}
pm.Start(1000)
defer pm.Stop()
var peers []*testPeer
for i := 0; i < totalPeers; i++ {
peer, _ := newTestPeer(fmt.Sprintf("peer %d", i), eth63, pm, true)
defer peer.close()
peers = append(peers, peer)
}
chain, _ := core.GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 1, func(i int, gen *core.BlockGen) {})
pm.BroadcastBlock(chain[0], true /*propagate*/)
errCh := make(chan error, totalPeers)
doneCh := make(chan struct{}, totalPeers)
for _, peer := range peers {
go func(p *testPeer) {
if err := p2p.ExpectMsg(p.app, NewBlockMsg, &newBlockData{Block: chain[0], TD: big.NewInt(131136)}); err != nil {
errCh <- err
} else {
doneCh <- struct{}{}
}
}(peer)
}
timeoutCh := time.NewTimer(time.Millisecond * 100).C
var receivedCount int
outer:
for {
select {
case err = <-errCh:
break outer
case <-doneCh:
receivedCount++
if receivedCount == totalPeers {
break outer
}
case <-timeoutCh:
break outer
}
}
for _, peer := range peers {
peer.app.Close()
}
if err != nil {
t.Errorf("error matching block by peer: %v", err)
}
if receivedCount != broadcastExpected {
t.Errorf("block broadcast to %d peers, expected %d", receivedCount, broadcastExpected)
}
}

View file

@ -25,11 +25,11 @@ import (
"runtime" "runtime"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/log/term"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/metrics/exp" "github.com/ethereum/go-ethereum/metrics/exp"
"github.com/fjl/memsize/memsizeui" "github.com/fjl/memsize/memsizeui"
colorable "github.com/mattn/go-colorable" colorable "github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
@ -101,7 +101,7 @@ var (
) )
func init() { func init() {
usecolor := term.IsTty(os.Stderr.Fd()) && os.Getenv("TERM") != "dumb" usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
output := io.Writer(os.Stderr) output := io.Writer(os.Stderr)
if usecolor { if usecolor {
output = colorable.NewColorableStderr() output = colorable.NewColorableStderr()

View file

@ -433,6 +433,11 @@ const Eth_JS = `
web3._extend({ web3._extend({
property: 'eth', property: 'eth',
methods: [ methods: [
new web3._extend.Method({
name: 'chainId',
call: 'eth_chainId',
params: 0
}),
new web3._extend.Method({ new web3._extend.Method({
name: 'sign', name: 'sign',
call: 'eth_sign', call: 'eth_sign',

View file

@ -32,8 +32,9 @@ import (
) )
const ( const (
blockDelayTimeout = time.Second * 10 // timeout for a peer to announce a head that has already been confirmed by others blockDelayTimeout = time.Second * 10 // timeout for a peer to announce a head that has already been confirmed by others
maxNodeCount = 20 // maximum number of fetcherTreeNode entries remembered for each peer maxNodeCount = 20 // maximum number of fetcherTreeNode entries remembered for each peer
serverStateAvailable = 100 // number of recent blocks where state availability is assumed
) )
// lightFetcher implements retrieval of newly announced headers. It also provides a peerHasBlock function for the // lightFetcher implements retrieval of newly announced headers. It also provides a peerHasBlock function for the
@ -215,8 +216,8 @@ func (f *lightFetcher) syncLoop() {
// registerPeer adds a new peer to the fetcher's peer set // registerPeer adds a new peer to the fetcher's peer set
func (f *lightFetcher) registerPeer(p *peer) { func (f *lightFetcher) registerPeer(p *peer) {
p.lock.Lock() p.lock.Lock()
p.hasBlock = func(hash common.Hash, number uint64) bool { p.hasBlock = func(hash common.Hash, number uint64, hasState bool) bool {
return f.peerHasBlock(p, hash, number) return f.peerHasBlock(p, hash, number, hasState)
} }
p.lock.Unlock() p.lock.Unlock()
@ -344,21 +345,27 @@ func (f *lightFetcher) announce(p *peer, head *announceData) {
// peerHasBlock returns true if we can assume the peer knows the given block // peerHasBlock returns true if we can assume the peer knows the given block
// based on its announcements // based on its announcements
func (f *lightFetcher) peerHasBlock(p *peer, hash common.Hash, number uint64) bool { func (f *lightFetcher) peerHasBlock(p *peer, hash common.Hash, number uint64, hasState bool) bool {
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
fp := f.peers[p]
if fp == nil || fp.root == nil {
return false
}
if hasState {
if fp.lastAnnounced == nil || fp.lastAnnounced.number > number+serverStateAvailable {
return false
}
}
if f.syncing { if f.syncing {
// always return true when syncing // always return true when syncing
// false positives are acceptable, a more sophisticated condition can be implemented later // false positives are acceptable, a more sophisticated condition can be implemented later
return true return true
} }
fp := f.peers[p]
if fp == nil || fp.root == nil {
return false
}
if number >= fp.root.number { if number >= fp.root.number {
// it is recent enough that if it is known, is should be in the peer's block tree // it is recent enough that if it is known, is should be in the peer's block tree
return fp.nodeByHash[hash] != nil return fp.nodeByHash[hash] != nil

View file

@ -84,7 +84,7 @@ func (r *BlockRequest) GetCost(peer *peer) uint64 {
// CanSend tells if a certain peer is suitable for serving the given request // CanSend tells if a certain peer is suitable for serving the given request
func (r *BlockRequest) CanSend(peer *peer) bool { func (r *BlockRequest) CanSend(peer *peer) bool {
return peer.HasBlock(r.Hash, r.Number) return peer.HasBlock(r.Hash, r.Number, false)
} }
// Request sends an ODR request to the LES network (implementation of LesOdrRequest) // Request sends an ODR request to the LES network (implementation of LesOdrRequest)
@ -140,7 +140,7 @@ func (r *ReceiptsRequest) GetCost(peer *peer) uint64 {
// CanSend tells if a certain peer is suitable for serving the given request // CanSend tells if a certain peer is suitable for serving the given request
func (r *ReceiptsRequest) CanSend(peer *peer) bool { func (r *ReceiptsRequest) CanSend(peer *peer) bool {
return peer.HasBlock(r.Hash, r.Number) return peer.HasBlock(r.Hash, r.Number, false)
} }
// Request sends an ODR request to the LES network (implementation of LesOdrRequest) // Request sends an ODR request to the LES network (implementation of LesOdrRequest)
@ -202,7 +202,7 @@ func (r *TrieRequest) GetCost(peer *peer) uint64 {
// CanSend tells if a certain peer is suitable for serving the given request // CanSend tells if a certain peer is suitable for serving the given request
func (r *TrieRequest) CanSend(peer *peer) bool { func (r *TrieRequest) CanSend(peer *peer) bool {
return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber) return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber, true)
} }
// Request sends an ODR request to the LES network (implementation of LesOdrRequest) // Request sends an ODR request to the LES network (implementation of LesOdrRequest)
@ -272,7 +272,7 @@ func (r *CodeRequest) GetCost(peer *peer) uint64 {
// CanSend tells if a certain peer is suitable for serving the given request // CanSend tells if a certain peer is suitable for serving the given request
func (r *CodeRequest) CanSend(peer *peer) bool { func (r *CodeRequest) CanSend(peer *peer) bool {
return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber) return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber, true)
} }
// Request sends an ODR request to the LES network (implementation of LesOdrRequest) // Request sends an ODR request to the LES network (implementation of LesOdrRequest)

View file

@ -194,7 +194,7 @@ func testOdr(t *testing.T, protocol int, expFail uint64, fn odrTestFn) {
client.peers.Register(client.rPeer) client.peers.Register(client.rPeer)
time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed
client.peers.lock.Lock() client.peers.lock.Lock()
client.rPeer.hasBlock = func(common.Hash, uint64) bool { return true } client.rPeer.hasBlock = func(common.Hash, uint64, bool) bool { return true }
client.peers.lock.Unlock() client.peers.lock.Unlock()
test(5) test(5)
// still expect all retrievals to pass, now data should be cached locally // still expect all retrievals to pass, now data should be cached locally

View file

@ -67,7 +67,7 @@ type peer struct {
sendQueue *execQueue sendQueue *execQueue
poolEntry *poolEntry poolEntry *poolEntry
hasBlock func(common.Hash, uint64) bool hasBlock func(common.Hash, uint64, bool) bool
responseErrors int responseErrors int
fcClient *flowcontrol.ClientNode // nil if the peer is server only fcClient *flowcontrol.ClientNode // nil if the peer is server only
@ -171,11 +171,11 @@ func (p *peer) GetRequestCost(msgcode uint64, amount int) uint64 {
} }
// HasBlock checks if the peer has a given block // HasBlock checks if the peer has a given block
func (p *peer) HasBlock(hash common.Hash, number uint64) bool { func (p *peer) HasBlock(hash common.Hash, number uint64, hasState bool) bool {
p.lock.RLock() p.lock.RLock()
hasBlock := p.hasBlock hasBlock := p.hasBlock
p.lock.RUnlock() p.lock.RUnlock()
return hasBlock != nil && hasBlock(hash, number) return hasBlock != nil && hasBlock(hash, number, hasState)
} }
// SendAnnounce announces the availability of a number of blocks through // SendAnnounce announces the availability of a number of blocks through

View file

@ -115,7 +115,7 @@ func testAccess(t *testing.T, protocol int, fn accessTestFn) {
client.peers.Register(client.rPeer) client.peers.Register(client.rPeer)
time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed
client.rPeer.lock.Lock() client.rPeer.lock.Lock()
client.rPeer.hasBlock = func(common.Hash, uint64) bool { return true } client.rPeer.hasBlock = func(common.Hash, uint64, bool) bool { return true }
client.rPeer.lock.Unlock() client.rPeer.lock.Unlock()
// expect all retrievals to pass // expect all retrievals to pass
test(5) test(5)

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,13 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build appengine
package term
// IsTty always returns false on AppEngine.
func IsTty(fd uintptr) bool {
return false
}

View file

@ -1,13 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
package term
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View file

@ -1,18 +0,0 @@
package term
import (
"syscall"
)
const ioctlReadTermios = syscall.TIOCGETA
// Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin.
type Termios struct {
Iflag uint32
Oflag uint32
Cflag uint32
Lflag uint32
Cc [20]uint8
Ispeed uint32
Ospeed uint32
}

View file

@ -1,14 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
package term
import "syscall"
const ioctlReadTermios = syscall.TCGETS
type Termios syscall.Termios

View file

@ -1,7 +0,0 @@
package term
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View file

@ -1,20 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux,!appengine darwin freebsd openbsd netbsd
package term
import (
"syscall"
"unsafe"
)
// IsTty returns true if the given file descriptor is a terminal.
func IsTty(fd uintptr) bool {
var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

View file

@ -1,7 +0,0 @@
package term
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View file

@ -1,9 +0,0 @@
package term
import "golang.org/x/sys/unix"
// IsTty returns true if the given file descriptor is a terminal.
func IsTty(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETA)
return err == nil
}

View file

@ -1,26 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package term
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTty returns true if the given file descriptor is a terminal.
func IsTty(fd uintptr) bool {
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

View file

@ -36,6 +36,9 @@ import (
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
const numberOfAccountsToDerive = 10
// ExternalAPI defines the external API through which signing requests are made. // ExternalAPI defines the external API through which signing requests are made.
type ExternalAPI interface { type ExternalAPI interface {
// List available accounts // List available accounts
@ -79,6 +82,9 @@ type SignerUI interface {
// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
// information // information
OnSignerStartup(info StartupInfo) OnSignerStartup(info StartupInfo)
// OnInputRequried is invoked when clef requires user input, for example master password or
// pin-code for unlocking hardware wallets
OnInputRequired(info UserInputRequest) (UserInputResponse, error)
} }
// SignerAPI defines the actual implementation of ExternalAPI // SignerAPI defines the actual implementation of ExternalAPI
@ -194,6 +200,14 @@ type (
StartupInfo struct { StartupInfo struct {
Info map[string]interface{} `json:"info"` Info map[string]interface{} `json:"info"`
} }
UserInputRequest struct {
Prompt string `json:"prompt"`
Title string `json:"title"`
IsPassword bool `json:"isPassword"`
}
UserInputResponse struct {
Text string `json:"text"`
}
) )
var ErrRequestDenied = errors.New("Request denied") var ErrRequestDenied = errors.New("Request denied")
@ -215,6 +229,9 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi
if len(ksLocation) > 0 { if len(ksLocation) > 0 {
backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
} }
if advancedMode {
log.Info("Clef is in advanced mode: will warn instead of reject")
}
if !noUSB { if !noUSB {
// Start a USB hub for Ledger hardware wallets // Start a USB hub for Ledger hardware wallets
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
@ -231,10 +248,94 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi
log.Debug("Trezor support enabled") log.Debug("Trezor support enabled")
} }
} }
if advancedMode { signer := &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode}
log.Info("Clef is in advanced mode: will warn instead of reject") if !noUSB {
signer.startUSBListener()
} }
return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode} return signer
}
func (api *SignerAPI) openTrezor(url accounts.URL) {
resp, err := api.UI.OnInputRequired(UserInputRequest{
Prompt: "Pin required to open Trezor wallet\n" +
"Look at the device for number positions\n\n" +
"7 | 8 | 9\n" +
"--+---+--\n" +
"4 | 5 | 6\n" +
"--+---+--\n" +
"1 | 2 | 3\n\n",
IsPassword: true,
Title: "Trezor unlock",
})
if err != nil {
log.Warn("failed getting trezor pin", "err", err)
return
}
// We're using the URL instead of the pointer to the
// Wallet -- perhaps it is not actually present anymore
w, err := api.am.Wallet(url.String())
if err != nil {
log.Warn("wallet unavailable", "url", url)
return
}
err = w.Open(resp.Text)
if err != nil {
log.Warn("failed to open wallet", "wallet", url, "err", err)
return
}
}
// startUSBListener starts a listener for USB events, for hardware wallet interaction
func (api *SignerAPI) startUSBListener() {
events := make(chan accounts.WalletEvent, 16)
am := api.am
am.Subscribe(events)
go func() {
// Open any wallets already attached
for _, wallet := range am.Wallets() {
if err := wallet.Open(""); err != nil {
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
if err == usbwallet.ErrTrezorPINNeeded {
go api.openTrezor(wallet.URL())
}
}
}
// Listen for wallet event till termination
for event := range events {
switch event.Kind {
case accounts.WalletArrived:
if err := event.Wallet.Open(""); err != nil {
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
if err == usbwallet.ErrTrezorPINNeeded {
go api.openTrezor(event.Wallet.URL())
}
}
case accounts.WalletOpened:
status, _ := event.Wallet.Status()
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
derivationPath := accounts.DefaultBaseDerivationPath
if event.Wallet.URL().Scheme == "ledger" {
derivationPath = accounts.DefaultLedgerBaseDerivationPath
}
var nextPath = derivationPath
// Derive first N accounts, hardcoded for now
for i := 0; i < numberOfAccountsToDerive; i++ {
acc, err := event.Wallet.Derive(nextPath, true)
if err != nil {
log.Warn("account derivation failed", "error", err)
} else {
log.Info("derived account", "address", acc.Address)
}
nextPath[len(nextPath)-1]++
}
case accounts.WalletDropped:
log.Info("Old wallet dropped", "url", event.Wallet.URL())
event.Wallet.Close()
}
}
}()
} }
// List returns the set of wallet this signer manages. Each wallet can contain // List returns the set of wallet this signer manages. Each wallet can contain

View file

@ -19,6 +19,7 @@ package core
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
@ -41,6 +42,10 @@ type HeadlessUI struct {
controller chan string controller chan string
} }
func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
return UserInputResponse{}, errors.New("not implemented")
}
func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
} }

View file

@ -83,6 +83,22 @@ func (ui *CommandlineUI) readPasswordText(inputstring string) string {
return string(text) return string(text)
} }
func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
fmt.Println(info.Title)
fmt.Println(info.Prompt)
if info.IsPassword {
text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
log.Error("Failed to read password", "err", err)
}
fmt.Println("-----------------------")
return UserInputResponse{string(text)}, err
}
text := ui.readString()
fmt.Println("-----------------------")
return UserInputResponse{text}, nil
}
// confirm returns true if user enters 'Yes', otherwise false // confirm returns true if user enters 'Yes', otherwise false
func (ui *CommandlineUI) confirm() bool { func (ui *CommandlineUI) confirm() bool {
fmt.Printf("Approve? [y/N]:\n") fmt.Printf("Approve? [y/N]:\n")

View file

@ -111,3 +111,11 @@ func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info) log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info)
} }
} }
func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
var result UserInputResponse
err := ui.dispatch("OnInputRequired", info, &result)
if err != nil {
log.Info("Error calling 'OnInputRequired'", "exc", err.Error(), "info", info)
}
return result, err
}

View file

@ -194,6 +194,11 @@ func (r *rulesetUI) ApproveImport(request *core.ImportRequest) (core.ImportRespo
return r.next.ApproveImport(request) return r.next.ApproveImport(request)
} }
// OnInputRequired not handled by rules
func (r *rulesetUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
return r.next.OnInputRequired(info)
}
func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
jsonreq, err := json.Marshal(request) jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveListing", jsonreq, err) approved, err := r.checkApproval("ApproveListing", jsonreq, err)
@ -222,6 +227,7 @@ func (r *rulesetUI) ShowInfo(message string) {
log.Info(message) log.Info(message)
r.next.ShowInfo(message) r.next.ShowInfo(message)
} }
func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) { func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) {
jsonInfo, err := json.Marshal(info) jsonInfo, err := json.Marshal(info)
if err != nil { if err != nil {

View file

@ -74,6 +74,10 @@ func mixAddr(a string) (*common.MixedcaseAddress, error) {
type alwaysDenyUI struct{} type alwaysDenyUI struct{}
func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
return core.UserInputResponse{}, nil
}
func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
} }
@ -200,6 +204,11 @@ type dummyUI struct {
calls []string calls []string
} }
func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
d.calls = append(d.calls, "OnInputRequired")
return core.UserInputResponse{}, nil
}
func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
d.calls = append(d.calls, "ApproveTx") d.calls = append(d.calls, "ApproveTx")
return core.SignTxResponse{}, core.ErrRequestDenied return core.SignTxResponse{}, core.ErrRequestDenied
@ -509,6 +518,11 @@ type dontCallMe struct {
t *testing.T t *testing.T
} }
func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
d.t.Fatalf("Did not expect next-handler to be called")
return core.UserInputResponse{}, nil
}
func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
} }

View file

@ -16,6 +16,9 @@
package api package api
//go:generate mimegen --types=./../../cmd/swarm/mimegen/mime.types --package=api --out=gen_mime.go
//go:generate gofmt -s -w gen_mime.go
import ( import (
"archive/tar" "archive/tar"
"context" "context"
@ -43,7 +46,8 @@ import (
"github.com/ethereum/go-ethereum/swarm/spancontext" "github.com/ethereum/go-ethereum/swarm/spancontext"
"github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/storage/mru" "github.com/ethereum/go-ethereum/swarm/storage/mru"
opentracing "github.com/opentracing/opentracing-go" "github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/opentracing/opentracing-go"
) )
var ( var (
@ -401,77 +405,54 @@ func (a *API) Get(ctx context.Context, decrypt DecryptFunc, manifestAddr storage
// we need to do some extra work if this is a mutable resource manifest // we need to do some extra work if this is a mutable resource manifest
if entry.ContentType == ResourceContentType { if entry.ContentType == ResourceContentType {
if entry.ResourceView == nil {
// get the resource rootAddr return reader, mimeType, status, nil, fmt.Errorf("Cannot decode ResourceView in manifest")
log.Trace("resource type", "menifestAddr", manifestAddr, "hash", entry.Hash) }
ctx, cancel := context.WithCancel(context.Background()) _, err := a.resource.Lookup(ctx, mru.NewQueryLatest(entry.ResourceView, lookup.NoClue))
defer cancel()
rootAddr := storage.Address(common.FromHex(entry.Hash))
rsrc, err := a.resource.Load(ctx, rootAddr)
if err != nil { if err != nil {
apiGetNotFound.Inc(1) apiGetNotFound.Inc(1)
status = http.StatusNotFound status = http.StatusNotFound
log.Debug(fmt.Sprintf("get resource content error: %v", err)) log.Debug(fmt.Sprintf("get resource content error: %v", err))
return reader, mimeType, status, nil, err return reader, mimeType, status, nil, err
} }
// get the data of the update
// use this key to retrieve the latest update _, rsrcData, err := a.resource.GetContent(entry.ResourceView)
params := mru.LookupLatest(rootAddr)
rsrc, err = a.resource.Lookup(ctx, params)
if err != nil { if err != nil {
apiGetNotFound.Inc(1) apiGetNotFound.Inc(1)
status = http.StatusNotFound status = http.StatusNotFound
log.Debug(fmt.Sprintf("get resource content error: %v", err)) log.Warn(fmt.Sprintf("get resource content error: %v", err))
return reader, mimeType, status, nil, err return reader, mimeType, status, nil, err
} }
// if it's multihash, we will transparently serve the content this multihash points to // extract multihash
// \TODO this resolve is rather expensive all in all, review to see if it can be achieved cheaper decodedMultihash, err := multihash.FromMultihash(rsrcData)
if rsrc.Multihash() { if err != nil {
apiGetInvalid.Inc(1)
status = http.StatusUnprocessableEntity
log.Warn("invalid resource multihash", "err", err)
return reader, mimeType, status, nil, err
}
manifestAddr = storage.Address(decodedMultihash)
log.Trace("resource is multihash", "key", manifestAddr)
// get the data of the update // get the manifest the multihash digest points to
_, rsrcData, err := a.resource.GetContent(rootAddr) trie, err := loadManifest(ctx, a.fileStore, manifestAddr, nil, NOOPDecrypt)
if err != nil { if err != nil {
apiGetNotFound.Inc(1) apiGetNotFound.Inc(1)
status = http.StatusNotFound status = http.StatusNotFound
log.Warn(fmt.Sprintf("get resource content error: %v", err)) log.Warn(fmt.Sprintf("loadManifestTrie (resource multihash) error: %v", err))
return reader, mimeType, status, nil, err return reader, mimeType, status, nil, err
} }
// validate that data as multihash // finally, get the manifest entry
decodedMultihash, err := multihash.FromMultihash(rsrcData) // it will always be the entry on path ""
if err != nil { entry, _ = trie.getEntry(path)
apiGetInvalid.Inc(1) if entry == nil {
status = http.StatusUnprocessableEntity status = http.StatusNotFound
log.Warn("invalid resource multihash", "err", err) apiGetNotFound.Inc(1)
return reader, mimeType, status, nil, err err = fmt.Errorf("manifest (resource multihash) entry for '%s' not found", path)
} log.Trace("manifest (resource multihash) entry not found", "key", manifestAddr, "path", path)
manifestAddr = storage.Address(decodedMultihash) return reader, mimeType, status, nil, err
log.Trace("resource is multihash", "key", manifestAddr)
// get the manifest the multihash digest points to
trie, err := loadManifest(ctx, a.fileStore, manifestAddr, nil, decrypt)
if err != nil {
apiGetNotFound.Inc(1)
status = http.StatusNotFound
log.Warn(fmt.Sprintf("loadManifestTrie (resource multihash) error: %v", err))
return reader, mimeType, status, nil, err
}
// finally, get the manifest entry
// it will always be the entry on path ""
entry, _ = trie.getEntry(path)
if entry == nil {
status = http.StatusNotFound
apiGetNotFound.Inc(1)
err = fmt.Errorf("manifest (resource multihash) entry for '%s' not found", path)
log.Trace("manifest (resource multihash) entry not found", "key", manifestAddr, "path", path)
return reader, mimeType, status, nil, err
}
} else {
// data is returned verbatim since it's not a multihash
return rsrc, "application/octet-stream", http.StatusOK, nil, nil
} }
} }
@ -778,9 +759,14 @@ func (a *API) UploadTar(ctx context.Context, bodyReader io.ReadCloser, manifestP
// add the entry under the path from the request // add the entry under the path from the request
manifestPath := path.Join(manifestPath, hdr.Name) manifestPath := path.Join(manifestPath, hdr.Name)
contentType := hdr.Xattrs["user.swarm.content-type"]
if contentType == "" {
contentType = mime.TypeByExtension(filepath.Ext(hdr.Name))
}
//DetectContentType("")
entry := &ManifestEntry{ entry := &ManifestEntry{
Path: manifestPath, Path: manifestPath,
ContentType: hdr.Xattrs["user.swarm.content-type"], ContentType: contentType,
Mode: hdr.Mode, Mode: hdr.Mode,
Size: hdr.Size, Size: hdr.Size,
ModTime: hdr.ModTime, ModTime: hdr.ModTime,
@ -791,10 +777,15 @@ func (a *API) UploadTar(ctx context.Context, bodyReader io.ReadCloser, manifestP
return nil, fmt.Errorf("error adding manifest entry from tar stream: %s", err) return nil, fmt.Errorf("error adding manifest entry from tar stream: %s", err)
} }
if hdr.Name == defaultPath { if hdr.Name == defaultPath {
contentType := hdr.Xattrs["user.swarm.content-type"]
if contentType == "" {
contentType = mime.TypeByExtension(filepath.Ext(hdr.Name))
}
entry := &ManifestEntry{ entry := &ManifestEntry{
Hash: contentKey.Hex(), Hash: contentKey.Hex(),
Path: "", // default entry Path: "", // default entry
ContentType: hdr.Xattrs["user.swarm.content-type"], ContentType: contentType,
Mode: hdr.Mode, Mode: hdr.Mode,
Size: hdr.Size, Size: hdr.Size,
ModTime: hdr.ModTime, ModTime: hdr.ModTime,
@ -966,37 +957,27 @@ func (a *API) BuildDirectoryTree(ctx context.Context, mhash string, nameresolver
} }
// ResourceLookup finds mutable resource updates at specific periods and versions // ResourceLookup finds mutable resource updates at specific periods and versions
func (a *API) ResourceLookup(ctx context.Context, params *mru.LookupParams) (string, []byte, error) { func (a *API) ResourceLookup(ctx context.Context, query *mru.Query) ([]byte, error) {
var err error _, err := a.resource.Lookup(ctx, query)
rsrc, err := a.resource.Load(ctx, params.RootAddr())
if err != nil { if err != nil {
return "", nil, err return nil, err
}
_, err = a.resource.Lookup(ctx, params)
if err != nil {
return "", nil, err
} }
var data []byte var data []byte
_, data, err = a.resource.GetContent(params.RootAddr()) _, data, err = a.resource.GetContent(&query.View)
if err != nil { if err != nil {
return "", nil, err return nil, err
} }
return rsrc.Name(), data, nil return data, nil
}
// Create Mutable resource
func (a *API) ResourceCreate(ctx context.Context, request *mru.Request) error {
return a.resource.New(ctx, request)
} }
// ResourceNewRequest creates a Request object to update a specific mutable resource // ResourceNewRequest creates a Request object to update a specific mutable resource
func (a *API) ResourceNewRequest(ctx context.Context, rootAddr storage.Address) (*mru.Request, error) { func (a *API) ResourceNewRequest(ctx context.Context, view *mru.View) (*mru.Request, error) {
return a.resource.NewUpdateRequest(ctx, rootAddr) return a.resource.NewRequest(ctx, view)
} }
// ResourceUpdate updates a Mutable Resource with arbitrary data. // ResourceUpdate updates a Mutable Resource with arbitrary data.
// Upon retrieval the update will be retrieved verbatim as bytes. // Upon retrieval the update will be retrieved verbatim as bytes.
func (a *API) ResourceUpdate(ctx context.Context, request *mru.SignedResourceUpdate) (storage.Address, error) { func (a *API) ResourceUpdate(ctx context.Context, request *mru.Request) (storage.Address, error) {
return a.resource.Update(ctx, request) return a.resource.Update(ctx, request)
} }
@ -1005,17 +986,91 @@ func (a *API) ResourceHashSize() int {
return a.resource.HashSize return a.resource.HashSize
} }
// ResolveResourceManifest retrieves the Mutable Resource manifest for the given address, and returns the address of the metadata chunk. // ErrCannotLoadResourceManifest is returned when looking up a resource manifest fails
func (a *API) ResolveResourceManifest(ctx context.Context, addr storage.Address) (storage.Address, error) { var ErrCannotLoadResourceManifest = errors.New("Cannot load resource manifest")
// ErrNotAResourceManifest is returned when the address provided returned something other than a valid manifest
var ErrNotAResourceManifest = errors.New("Not a resource manifest")
// ResolveResourceManifest retrieves the Mutable Resource manifest for the given address, and returns the Resource's view ID.
func (a *API) ResolveResourceManifest(ctx context.Context, addr storage.Address) (*mru.View, error) {
trie, err := loadManifest(ctx, a.fileStore, addr, nil, NOOPDecrypt) trie, err := loadManifest(ctx, a.fileStore, addr, nil, NOOPDecrypt)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load resource manifest: %v", err) return nil, ErrCannotLoadResourceManifest
} }
entry, _ := trie.getEntry("") entry, _ := trie.getEntry("")
if entry.ContentType != ResourceContentType { if entry.ContentType != ResourceContentType {
return nil, fmt.Errorf("not a resource manifest: %s", addr) return nil, ErrNotAResourceManifest
} }
return storage.Address(common.FromHex(entry.Hash)), nil return entry.ResourceView, nil
}
// ErrCannotResolveResourceURI is returned when the ENS resolver is not able to translate a name to a resource
var ErrCannotResolveResourceURI = errors.New("Cannot resolve Resource URI")
// ErrCannotResolveResourceView is returned when values provided are not enough or invalid to recreate a
// resource view out of them.
var ErrCannotResolveResourceView = errors.New("Cannot resolve resource view")
// ResolveResourceView attempts to extract View information out of the manifest, if provided
// If not, it attempts to extract the View out of a set of key-value pairs
func (a *API) ResolveResourceView(ctx context.Context, uri *URI, values mru.Values) (*mru.View, error) {
var view *mru.View
var err error
if uri.Addr != "" {
// resolve the content key.
manifestAddr := uri.Address()
if manifestAddr == nil {
manifestAddr, err = a.Resolve(ctx, uri.Addr)
if err != nil {
return nil, ErrCannotResolveResourceURI
}
}
// get the resource view from the manifest
view, err = a.ResolveResourceManifest(ctx, manifestAddr)
if err != nil {
return nil, err
}
log.Debug("handle.get.resource: resolved", "manifestkey", manifestAddr, "view", view.Hex())
} else {
var v mru.View
if err := v.FromValues(values); err != nil {
return nil, ErrCannotResolveResourceView
}
view = &v
}
return view, nil
}
// MimeOctetStream default value of http Content-Type header
const MimeOctetStream = "application/octet-stream"
// DetectContentType by file file extension, or fallback to content sniff
func DetectContentType(fileName string, f io.ReadSeeker) (string, error) {
ctype := mime.TypeByExtension(filepath.Ext(fileName))
if ctype != "" {
return ctype, nil
}
// save/rollback to get content probe from begin of file
currentPosition, err := f.Seek(0, io.SeekCurrent)
if err != nil {
return MimeOctetStream, fmt.Errorf("seeker can't seek, %s", err)
}
// read a chunk to decide between utf-8 text and binary
var buf [512]byte
n, _ := f.Read(buf[:])
ctype = http.DetectContentType(buf[:n])
_, err = f.Seek(currentPosition, io.SeekStart) // rewind to output whole file
if err != nil {
return MimeOctetStream, fmt.Errorf("seeker can't seek, %s", err)
}
return ctype, nil
} }

View file

@ -17,6 +17,7 @@
package api package api
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"flag" "flag"
@ -433,3 +434,69 @@ func TestDecryptOrigin(t *testing.T) {
} }
} }
} }
func TestDetectContentType(t *testing.T) {
for _, tc := range []struct {
file string
content string
expectedContentType string
}{
{
file: "file-with-correct-css.css",
content: "body {background-color: orange}",
expectedContentType: "text/css; charset=utf-8",
},
{
file: "empty-file.css",
content: "",
expectedContentType: "text/css; charset=utf-8",
},
{
file: "empty-file.pdf",
content: "",
expectedContentType: "application/pdf",
},
{
file: "empty-file.md",
content: "",
expectedContentType: "text/markdown; charset=utf-8",
},
{
file: "empty-file-with-unknown-content.strangeext",
content: "",
expectedContentType: "text/plain; charset=utf-8",
},
{
file: "file-with-unknown-extension-and-content.strangeext",
content: "Lorem Ipsum",
expectedContentType: "text/plain; charset=utf-8",
},
{
file: "file-no-extension",
content: "Lorem Ipsum",
expectedContentType: "text/plain; charset=utf-8",
},
{
file: "file-no-extension-no-content",
content: "",
expectedContentType: "text/plain; charset=utf-8",
},
{
file: "css-file-with-html-inside.css",
content: "<!doctype html><html><head></head><body></body></html>",
expectedContentType: "text/css; charset=utf-8",
},
} {
t.Run(tc.file, func(t *testing.T) {
detected, err := DetectContentType(tc.file, bytes.NewReader([]byte(tc.content)))
if err != nil {
t.Fatal(err)
}
if detected != tc.expectedContentType {
t.Fatalf("File: %s, Expected mime type %s, got %s", tc.file, tc.expectedContentType, detected)
}
})
}
}

View file

@ -24,10 +24,10 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -123,10 +123,16 @@ func Open(path string) (*File, error) {
f.Close() f.Close()
return nil, err return nil, err
} }
contentType, err := api.DetectContentType(f.Name(), f)
if err != nil {
return nil, err
}
return &File{ return &File{
ReadCloser: f, ReadCloser: f,
ManifestEntry: api.ManifestEntry{ ManifestEntry: api.ManifestEntry{
ContentType: mime.TypeByExtension(filepath.Ext(path)), ContentType: contentType,
Mode: int64(stat.Mode()), Mode: int64(stat.Mode()),
Size: stat.Size(), Size: stat.Size(),
ModTime: stat.ModTime(), ModTime: stat.ModTime(),
@ -595,13 +601,16 @@ func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error)
return string(data), nil return string(data), nil
} }
// ErrNoResourceUpdatesFound is returned when Swarm cannot find updates of the given resource
var ErrNoResourceUpdatesFound = errors.New("No updates found for this resource")
// CreateResource creates a Mutable Resource with the given name and frequency, initializing it with the provided // CreateResource creates a Mutable Resource with the given name and frequency, initializing it with the provided
// data. Data is interpreted as multihash or not depending on the multihash parameter. // data. Data is interpreted as multihash or not depending on the multihash parameter.
// startTime=0 means "now" // startTime=0 means "now"
// Returns the resulting Mutable Resource manifest address that you can use to include in an ENS Resolver (setContent) // Returns the resulting Mutable Resource manifest address that you can use to include in an ENS Resolver (setContent)
// or reference future updates (Client.UpdateResource) // or reference future updates (Client.UpdateResource)
func (c *Client) CreateResource(request *mru.Request) (string, error) { func (c *Client) CreateResource(request *mru.Request) (string, error) {
responseStream, err := c.updateResource(request) responseStream, err := c.updateResource(request, true)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -621,17 +630,24 @@ func (c *Client) CreateResource(request *mru.Request) (string, error) {
// UpdateResource allows you to set a new version of your content // UpdateResource allows you to set a new version of your content
func (c *Client) UpdateResource(request *mru.Request) error { func (c *Client) UpdateResource(request *mru.Request) error {
_, err := c.updateResource(request) _, err := c.updateResource(request, false)
return err return err
} }
func (c *Client) updateResource(request *mru.Request) (io.ReadCloser, error) { func (c *Client) updateResource(request *mru.Request, createManifest bool) (io.ReadCloser, error) {
body, err := request.MarshalJSON() URL, err := url.Parse(c.Gateway)
if err != nil { if err != nil {
return nil, err return nil, err
} }
URL.Path = "/bzz-resource:/"
values := URL.Query()
body := request.AppendValues(values)
if createManifest {
values.Set("manifest", "1")
}
URL.RawQuery = values.Encode()
req, err := http.NewRequest("POST", c.Gateway+"/bzz-resource:/", bytes.NewBuffer(body)) req, err := http.NewRequest("POST", URL.String(), bytes.NewBuffer(body))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -642,28 +658,61 @@ func (c *Client) updateResource(request *mru.Request) (io.ReadCloser, error) {
} }
return res.Body, nil return res.Body, nil
} }
// GetResource returns a byte stream with the raw content of the resource // GetResource returns a byte stream with the raw content of the resource
// manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver // manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver
// points to that address // points to that address
func (c *Client) GetResource(manifestAddressOrDomain string) (io.ReadCloser, error) { func (c *Client) GetResource(query *mru.Query, manifestAddressOrDomain string) (io.ReadCloser, error) {
return c.getResource(query, manifestAddressOrDomain, false)
}
res, err := http.Get(c.Gateway + "/bzz-resource:/" + manifestAddressOrDomain) // getResource returns a byte stream with the raw content of the resource
// manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver
// points to that address
// meta set to true will instruct the node return resource metainformation instead
func (c *Client) getResource(query *mru.Query, manifestAddressOrDomain string, meta bool) (io.ReadCloser, error) {
URL, err := url.Parse(c.Gateway)
if err != nil {
return nil, err
}
URL.Path = "/bzz-resource:/" + manifestAddressOrDomain
values := URL.Query()
if query != nil {
query.AppendValues(values) //adds query parameters
}
if meta {
values.Set("meta", "1")
}
URL.RawQuery = values.Encode()
res, err := http.Get(URL.String())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return res.Body, nil
if res.StatusCode != http.StatusOK {
if res.StatusCode == http.StatusNotFound {
return nil, ErrNoResourceUpdatesFound
}
errorMessageBytes, err := ioutil.ReadAll(res.Body)
var errorMessage string
if err != nil {
errorMessage = "cannot retrieve error message: " + err.Error()
} else {
errorMessage = string(errorMessageBytes)
}
return nil, fmt.Errorf("Error retrieving resource: %s", errorMessage)
}
return res.Body, nil
} }
// GetResourceMetadata returns a structure that describes the Mutable Resource // GetResourceMetadata returns a structure that describes the Mutable Resource
// manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver // manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver
// points to that address // points to that address
func (c *Client) GetResourceMetadata(manifestAddressOrDomain string) (*mru.Request, error) { func (c *Client) GetResourceMetadata(query *mru.Query, manifestAddressOrDomain string) (*mru.Request, error) {
responseStream, err := c.GetResource(manifestAddressOrDomain + "/meta") responseStream, err := c.getResource(query, manifestAddressOrDomain, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -25,6 +25,8 @@ import (
"sort" "sort"
"testing" "testing"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/swarm/api" "github.com/ethereum/go-ethereum/swarm/api"
@ -391,19 +393,12 @@ func TestClientCreateResourceMultihash(t *testing.T) {
s := common.FromHex(swarmHash) s := common.FromHex(swarmHash)
mh := multihash.ToMultihash(s) mh := multihash.ToMultihash(s)
// our mutable resource "name" // our mutable resource topic
resourceName := "foo.eth" topic, _ := mru.NewTopic("foo.eth", nil)
createRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ createRequest := mru.NewFirstRequest(topic)
Name: resourceName,
Frequency: 13, createRequest.SetData(mh)
StartTime: srv.GetCurrentTime(),
Owner: signer.Address(),
})
if err != nil {
t.Fatal(err)
}
createRequest.SetData(mh, true)
if err := createRequest.Sign(signer); err != nil { if err := createRequest.Sign(signer); err != nil {
t.Fatalf("Error signing update: %s", err) t.Fatalf("Error signing update: %s", err)
} }
@ -414,12 +409,18 @@ func TestClientCreateResourceMultihash(t *testing.T) {
t.Fatalf("Error creating resource: %s", err) t.Fatalf("Error creating resource: %s", err)
} }
correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" correctManifestAddrHex := "6ef40ba1492cf2a029dc9a8b5896c822cf689d3cd010842f4f1744e6db8824bd"
if resourceManifestHash != correctManifestAddrHex { if resourceManifestHash != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash) t.Fatalf("Response resource manifest mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash)
} }
reader, err := client.GetResource(correctManifestAddrHex) // Check we get a not found error when trying to get the resource with a made-up manifest
_, err = client.GetResource(nil, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
if err != ErrNoResourceUpdatesFound {
t.Fatalf("Expected to receive ErrNoResourceUpdatesFound error. Got: %s", err)
}
reader, err := client.GetResource(nil, correctManifestAddrHex)
if err != nil { if err != nil {
t.Fatalf("Error retrieving resource: %s", err) t.Fatalf("Error retrieving resource: %s", err)
} }
@ -447,30 +448,22 @@ func TestClientCreateUpdateResource(t *testing.T) {
databytes := []byte("En un lugar de La Mancha, de cuyo nombre no quiero acordarme...") databytes := []byte("En un lugar de La Mancha, de cuyo nombre no quiero acordarme...")
// our mutable resource name // our mutable resource name
resourceName := "El Quijote" topic, _ := mru.NewTopic("El Quijote", nil)
createRequest := mru.NewFirstRequest(topic)
createRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ createRequest.SetData(databytes)
Name: resourceName,
Frequency: 13,
StartTime: srv.GetCurrentTime(),
Owner: signer.Address(),
})
if err != nil {
t.Fatal(err)
}
createRequest.SetData(databytes, false)
if err := createRequest.Sign(signer); err != nil { if err := createRequest.Sign(signer); err != nil {
t.Fatalf("Error signing update: %s", err) t.Fatalf("Error signing update: %s", err)
} }
resourceManifestHash, err := client.CreateResource(createRequest) resourceManifestHash, err := client.CreateResource(createRequest)
correctManifestAddrHex := "cc7904c17b49f9679e2d8006fe25e87e3f5c2072c2b49cab50f15e544471b30a" correctManifestAddrHex := "fcb8e75f53e480e197c083ad1976d265674d0ce776f2bf359c09c413fb5230b8"
if resourceManifestHash != correctManifestAddrHex { if resourceManifestHash != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash) t.Fatalf("Response resource manifest mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash)
} }
reader, err := client.GetResource(correctManifestAddrHex) reader, err := client.GetResource(nil, correctManifestAddrHex)
if err != nil { if err != nil {
t.Fatalf("Error retrieving resource: %s", err) t.Fatalf("Error retrieving resource: %s", err)
} }
@ -486,12 +479,12 @@ func TestClientCreateUpdateResource(t *testing.T) {
// define different data // define different data
databytes = []byte("... no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero ...") databytes = []byte("... no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero ...")
updateRequest, err := client.GetResourceMetadata(correctManifestAddrHex) updateRequest, err := client.GetResourceMetadata(nil, correctManifestAddrHex)
if err != nil { if err != nil {
t.Fatalf("Error retrieving update request template: %s", err) t.Fatalf("Error retrieving update request template: %s", err)
} }
updateRequest.SetData(databytes, false) updateRequest.SetData(databytes)
if err := updateRequest.Sign(signer); err != nil { if err := updateRequest.Sign(signer); err != nil {
t.Fatalf("Error signing update: %s", err) t.Fatalf("Error signing update: %s", err)
} }
@ -500,7 +493,7 @@ func TestClientCreateUpdateResource(t *testing.T) {
t.Fatalf("Error updating resource: %s", err) t.Fatalf("Error updating resource: %s", err)
} }
reader, err = client.GetResource(correctManifestAddrHex) reader, err = client.GetResource(nil, correctManifestAddrHex)
if err != nil { if err != nil {
t.Fatalf("Error retrieving resource: %s", err) t.Fatalf("Error retrieving resource: %s", err)
} }
@ -513,4 +506,24 @@ func TestClientCreateUpdateResource(t *testing.T) {
t.Fatalf("Expected: %v, got %v", databytes, gotData) t.Fatalf("Expected: %v, got %v", databytes, gotData)
} }
// now try retrieving resource without a manifest
view := &mru.View{
Topic: topic,
User: signer.Address(),
}
lookupParams := mru.NewQueryLatest(view, lookup.NoClue)
reader, err = client.GetResource(lookupParams, "")
if err != nil {
t.Fatalf("Error retrieving resource: %s", err)
}
defer reader.Close()
gotData, err = ioutil.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(databytes, gotData) {
t.Fatalf("Expected: %v, got %v", databytes, gotData)
}
} }

View file

@ -50,26 +50,27 @@ type Config struct {
Swap *swap.LocalProfile Swap *swap.LocalProfile
Pss *pss.PssParams Pss *pss.PssParams
//*network.SyncParams //*network.SyncParams
Contract common.Address Contract common.Address
EnsRoot common.Address EnsRoot common.Address
EnsAPIs []string EnsAPIs []string
Path string Path string
ListenAddr string ListenAddr string
Port string Port string
PublicKey string PublicKey string
BzzKey string BzzKey string
NodeID string NodeID string
NetworkID uint64 NetworkID uint64
SwapEnabled bool SwapEnabled bool
SyncEnabled bool SyncEnabled bool
SyncingSkipCheck bool SyncingSkipCheck bool
DeliverySkipCheck bool DeliverySkipCheck bool
LightNodeEnabled bool MaxStreamPeerServers int
SyncUpdateDelay time.Duration LightNodeEnabled bool
SwapAPI string SyncUpdateDelay time.Duration
Cors string SwapAPI string
BzzAccount string Cors string
privateKey *ecdsa.PrivateKey BzzAccount string
privateKey *ecdsa.PrivateKey
} }
//create a default config with all parameters to set to defaults //create a default config with all parameters to set to defaults
@ -80,20 +81,21 @@ func NewConfig() (c *Config) {
FileStoreParams: storage.NewFileStoreParams(), FileStoreParams: storage.NewFileStoreParams(),
HiveParams: network.NewHiveParams(), HiveParams: network.NewHiveParams(),
//SyncParams: network.NewDefaultSyncParams(), //SyncParams: network.NewDefaultSyncParams(),
Swap: swap.NewDefaultSwapParams(), Swap: swap.NewDefaultSwapParams(),
Pss: pss.NewPssParams(), Pss: pss.NewPssParams(),
ListenAddr: DefaultHTTPListenAddr, ListenAddr: DefaultHTTPListenAddr,
Port: DefaultHTTPPort, Port: DefaultHTTPPort,
Path: node.DefaultDataDir(), Path: node.DefaultDataDir(),
EnsAPIs: nil, EnsAPIs: nil,
EnsRoot: ens.TestNetAddress, EnsRoot: ens.TestNetAddress,
NetworkID: network.DefaultNetworkID, NetworkID: network.DefaultNetworkID,
SwapEnabled: false, SwapEnabled: false,
SyncEnabled: true, SyncEnabled: true,
SyncingSkipCheck: false, SyncingSkipCheck: false,
DeliverySkipCheck: true, MaxStreamPeerServers: 10000,
SyncUpdateDelay: 15 * time.Second, DeliverySkipCheck: true,
SwapAPI: "", SyncUpdateDelay: 15 * time.Second,
SwapAPI: "",
} }
return return

View file

@ -21,7 +21,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -97,51 +96,50 @@ func (fs *FileSystem) Upload(lpath, index string, toEncrypt bool) (string, error
list = append(list, entry) list = append(list, entry)
} }
cnt := len(list) errors := make([]error, len(list))
errors := make([]error, cnt) sem := make(chan bool, maxParallelFiles)
done := make(chan bool, maxParallelFiles) defer close(sem)
dcnt := 0
awg := &sync.WaitGroup{}
for i, entry := range list { for i, entry := range list {
if i >= dcnt+maxParallelFiles { sem <- true
<-done go func(i int, entry *manifestTrieEntry) {
dcnt++ defer func() { <-sem }()
}
awg.Add(1)
go func(i int, entry *manifestTrieEntry, done chan bool) {
f, err := os.Open(entry.Path) f, err := os.Open(entry.Path)
if err == nil { if err != nil {
stat, _ := f.Stat() errors[i] = err
var hash storage.Address return
var wait func(context.Context) error
ctx := context.TODO()
hash, wait, err = fs.api.fileStore.Store(ctx, f, stat.Size(), toEncrypt)
if hash != nil {
list[i].Hash = hash.Hex()
}
err = wait(ctx)
awg.Done()
if err == nil {
first512 := make([]byte, 512)
fread, _ := f.ReadAt(first512, 0)
if fread > 0 {
mimeType := http.DetectContentType(first512[:fread])
if filepath.Ext(entry.Path) == ".css" {
mimeType = "text/css"
}
list[i].ContentType = mimeType
}
}
f.Close()
} }
errors[i] = err defer f.Close()
done <- true
}(i, entry, done) stat, err := f.Stat()
if err != nil {
errors[i] = err
return
}
var hash storage.Address
var wait func(context.Context) error
ctx := context.TODO()
hash, wait, err = fs.api.fileStore.Store(ctx, f, stat.Size(), toEncrypt)
if hash != nil {
list[i].Hash = hash.Hex()
}
if err := wait(ctx); err != nil {
errors[i] = err
return
}
list[i].ContentType, err = DetectContentType(f.Name(), f)
if err != nil {
errors[i] = err
return
}
}(i, entry)
} }
for dcnt < cnt { for i := 0; i < cap(sem); i++ {
<-done sem <- true
dcnt++
} }
trie := &manifestTrie{ trie := &manifestTrie{
@ -168,7 +166,6 @@ func (fs *FileSystem) Upload(lpath, index string, toEncrypt bool) (string, error
if err2 == nil { if err2 == nil {
hs = trie.ref.Hex() hs = trie.ref.Hex()
} }
awg.Wait()
return hs, err2 return hs, err2
} }

View file

@ -60,7 +60,7 @@ func TestApiDirUpload0(t *testing.T) {
content = readPath(t, "testdata", "test0", "index.css") content = readPath(t, "testdata", "test0", "index.css")
resp = testGet(t, api, bzzhash, "index.css") resp = testGet(t, api, bzzhash, "index.css")
exp = expResponse(content, "text/css", 0) exp = expResponse(content, "text/css; charset=utf-8", 0)
checkResponse(t, resp, exp) checkResponse(t, resp, exp)
addr := storage.Address(common.Hex2Bytes(bzzhash)) addr := storage.Address(common.Hex2Bytes(bzzhash))
@ -140,7 +140,7 @@ func TestApiDirUploadModify(t *testing.T) {
content = readPath(t, "testdata", "test0", "index.css") content = readPath(t, "testdata", "test0", "index.css")
resp = testGet(t, api, bzzhash, "index.css") resp = testGet(t, api, bzzhash, "index.css")
exp = expResponse(content, "text/css", 0) exp = expResponse(content, "text/css; charset=utf-8", 0)
checkResponse(t, resp, exp) checkResponse(t, resp, exp)
_, _, _, _, err = api.Get(context.TODO(), nil, addr, "") _, _, _, _, err = api.Get(context.TODO(), nil, addr, "")

1201
swarm/api/gen_mime.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -201,6 +201,13 @@ func (s *Server) HandleBzzGet(w http.ResponseWriter, r *http.Request) {
defer reader.Close() defer reader.Close()
w.Header().Set("Content-Type", "application/x-tar") w.Header().Set("Content-Type", "application/x-tar")
fileName := uri.Addr
if found := path.Base(uri.Path); found != "" && found != "." && found != "/" {
fileName = found
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.tar\"", fileName))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
io.Copy(w, reader) io.Copy(w, reader)
return return
@ -487,6 +494,7 @@ func resourcePostMode(path string) (isRaw bool, frequency uint64, err error) {
// The requests can be to a) create a resource, b) update a resource or c) both a+b: create a resource and set the initial content // The requests can be to a) create a resource, b) update a resource or c) both a+b: create a resource and set the initial content
func (s *Server) HandlePostResource(w http.ResponseWriter, r *http.Request) { func (s *Server) HandlePostResource(w http.ResponseWriter, r *http.Request) {
ruid := GetRUID(r.Context()) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.post.resource", "ruid", ruid) log.Debug("handle.post.resource", "ruid", ruid)
var err error var err error
@ -496,9 +504,24 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *http.Request) {
RespondError(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
view, err := s.api.ResolveResourceView(r.Context(), uri, r.URL.Query())
if err != nil { // couldn't parse query string or retrieve manifest
getFail.Inc(1)
httpStatus := http.StatusBadRequest
if err == api.ErrCannotLoadResourceManifest || err == api.ErrCannotResolveResourceURI {
httpStatus = http.StatusNotFound
}
RespondError(w, r, fmt.Sprintf("cannot retrieve resource view: %s", err), httpStatus)
return
}
var updateRequest mru.Request var updateRequest mru.Request
if err := updateRequest.UnmarshalJSON(body); err != nil { // decodes request JSON updateRequest.View = *view
RespondError(w, r, err.Error(), http.StatusBadRequest) //TODO: send different status response depending on error query := r.URL.Query()
if err := updateRequest.FromValues(query, body); err != nil { // decodes request from query parameters
RespondError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
@ -510,56 +533,40 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *http.Request) {
RespondError(w, r, err.Error(), http.StatusForbidden) RespondError(w, r, err.Error(), http.StatusForbidden)
return return
} }
} _, err = s.api.ResourceUpdate(r.Context(), &updateRequest)
if updateRequest.IsNew() {
err = s.api.ResourceCreate(r.Context(), &updateRequest)
if err != nil {
code, err2 := s.translateResourceError(w, r, "resource creation fail", err)
RespondError(w, r, err2.Error(), code)
return
}
}
if updateRequest.IsUpdate() {
_, err = s.api.ResourceUpdate(r.Context(), &updateRequest.SignedResourceUpdate)
if err != nil { if err != nil {
RespondError(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
} }
// at this point both possible operations (create, update or both) were successful if query.Get("manifest") == "1" {
// so in case it was a new resource, then create a manifest and send it over.
if updateRequest.IsNew() {
// we create a manifest so we can retrieve the resource with bzz:// later // we create a manifest so we can retrieve the resource with bzz:// later
// this manifest has a special "resource type" manifest, and its hash is the key of the mutable resource // this manifest has a special "resource type" manifest, and saves the
// metadata chunk (rootAddr) // resource view ID used to retrieve the resource later
m, err := s.api.NewResourceManifest(r.Context(), updateRequest.RootAddr().Hex()) m, err := s.api.NewResourceManifest(r.Context(), &updateRequest.View)
if err != nil { if err != nil {
RespondError(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError)
return return
} }
// the key to the manifest will be passed back to the client // the key to the manifest will be passed back to the client
// the client can access the root chunk key directly through its Hash member // the client can access the view directly through its resourceView member
// the manifest key should be set as content in the resolver of the ENS name // the manifest key can be set as content in the resolver of the ENS name
// \TODO update manifest key automatically in ENS
outdata, err := json.Marshal(m) outdata, err := json.Marshal(m)
if err != nil { if err != nil {
RespondError(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError)
return return
} }
fmt.Fprint(w, string(outdata)) fmt.Fprint(w, string(outdata))
w.Header().Add("Content-type", "application/json")
} }
w.Header().Add("Content-type", "application/json")
} }
// Retrieve mutable resource updates: // Retrieve mutable resource updates:
// bzz-resource://<id> - get latest update // bzz-resource://<id> - get latest update
// bzz-resource://<id>/<n> - get latest update on period n // bzz-resource://<id>/?period=n - get latest update on period n
// bzz-resource://<id>/<n>/<m> - get update version m of period n // bzz-resource://<id>/?period=n&version=m - get update version m of period n
// bzz-resource://<id>/meta - get metadata and next version information // bzz-resource://<id>/meta - get metadata and next version information
// <id> = ens name or hash // <id> = ens name or hash
// TODO: Enable pass maxPeriod parameter // TODO: Enable pass maxPeriod parameter
@ -569,84 +576,44 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.get.resource", "ruid", ruid) log.Debug("handle.get.resource", "ruid", ruid)
var err error var err error
// resolve the content key. view, err := s.api.ResolveResourceView(r.Context(), uri, r.URL.Query())
manifestAddr := uri.Address() if err != nil { // couldn't parse query string or retrieve manifest
if manifestAddr == nil {
manifestAddr, err = s.api.Resolve(r.Context(), uri.Addr)
if err != nil {
getFail.Inc(1)
RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound)
return
}
} else {
w.Header().Set("Cache-Control", "max-age=2147483648")
}
// get the root chunk rootAddr from the manifest
rootAddr, err := s.api.ResolveResourceManifest(r.Context(), manifestAddr)
if err != nil {
getFail.Inc(1) getFail.Inc(1)
RespondError(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", uri.Addr, err), http.StatusNotFound) httpStatus := http.StatusBadRequest
if err == api.ErrCannotLoadResourceManifest || err == api.ErrCannotResolveResourceURI {
httpStatus = http.StatusNotFound
}
RespondError(w, r, fmt.Sprintf("cannot retrieve resource view: %s", err), httpStatus)
return return
} }
log.Debug("handle.get.resource: resolved", "ruid", ruid, "manifestkey", manifestAddr, "rootchunk addr", rootAddr)
// determine if the query specifies period and version or it is a metadata query // determine if the query specifies period and version or it is a metadata query
var params []string if r.URL.Query().Get("meta") == "1" {
if len(uri.Path) > 0 { unsignedUpdateRequest, err := s.api.ResourceNewRequest(r.Context(), view)
if uri.Path == "meta" { if err != nil {
unsignedUpdateRequest, err := s.api.ResourceNewRequest(r.Context(), rootAddr) getFail.Inc(1)
if err != nil { RespondError(w, r, fmt.Sprintf("cannot retrieve resource metadata for view=%s: %s", view.Hex(), err), http.StatusNotFound)
getFail.Inc(1)
RespondError(w, r, fmt.Sprintf("cannot retrieve resource metadata for rootAddr=%s: %s", rootAddr.Hex(), err), http.StatusNotFound)
return
}
rawResponse, err := unsignedUpdateRequest.MarshalJSON()
if err != nil {
RespondError(w, r, fmt.Sprintf("cannot encode unsigned UpdateRequest: %v", err), http.StatusInternalServerError)
return
}
w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(rawResponse))
return return
} }
rawResponse, err := unsignedUpdateRequest.MarshalJSON()
params = strings.Split(uri.Path, "/") if err != nil {
RespondError(w, r, fmt.Sprintf("cannot encode unsigned UpdateRequest: %v", err), http.StatusInternalServerError)
return
}
w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(rawResponse))
return
} }
var name string
var data []byte
now := time.Now()
switch len(params) { lookupParams := &mru.Query{View: *view}
case 0: // latest only if err = lookupParams.FromValues(r.URL.Query()); err != nil { // parse period, version
name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupLatest(rootAddr)) RespondError(w, r, fmt.Sprintf("invalid mutable resource request:%s", err), http.StatusBadRequest)
case 2: // specific period and version return
var version uint64
var period uint64
version, err = strconv.ParseUint(params[1], 10, 32)
if err != nil {
break
}
period, err = strconv.ParseUint(params[0], 10, 32)
if err != nil {
break
}
name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupVersion(rootAddr, uint32(period), uint32(version)))
case 1: // last version of specific period
var period uint64
period, err = strconv.ParseUint(params[0], 10, 32)
if err != nil {
break
}
name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupLatestVersionInPeriod(rootAddr, uint32(period)))
default: // bogus
err = mru.NewError(storage.ErrInvalidValue, "invalid mutable resource request")
} }
data, err := s.api.ResourceLookup(r.Context(), lookupParams)
// any error from the switch statement will end up here // any error from the switch statement will end up here
if err != nil { if err != nil {
code, err2 := s.translateResourceError(w, r, "mutable resource lookup fail", err) code, err2 := s.translateResourceError(w, r, "mutable resource lookup fail", err)
@ -655,9 +622,9 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *http.Request) {
} }
// All ok, serve the retrieved update // All ok, serve the retrieved update
log.Debug("Found update", "name", name, "ruid", ruid) log.Debug("Found update", "view", view.Hex(), "ruid", ruid)
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", api.MimeOctetStream)
http.ServeContent(w, r, "", now, bytes.NewReader(data)) http.ServeContent(w, r, "", time.Now(), bytes.NewReader(data))
} }
func (s *Server) translateResourceError(w http.ResponseWriter, r *http.Request, supErr string, err error) (int, error) { func (s *Server) translateResourceError(w http.ResponseWriter, r *http.Request, supErr string, err error) (int, error) {
@ -730,11 +697,9 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *http.Request) {
case uri.Raw(): case uri.Raw():
// allow the request to overwrite the content type using a query // allow the request to overwrite the content type using a query
// parameter // parameter
contentType := "application/octet-stream"
if typ := r.URL.Query().Get("content_type"); typ != "" { if typ := r.URL.Query().Get("content_type"); typ != "" {
contentType = typ w.Header().Set("Content-Type", typ)
} }
w.Header().Set("Content-Type", contentType)
http.ServeContent(w, r, "", time.Now(), reader) http.ServeContent(w, r, "", time.Now(), reader)
case uri.Hash(): case uri.Hash():
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
@ -890,8 +855,17 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", contentType) if contentType != "" {
http.ServeContent(w, r, "", time.Now(), newBufferedReadSeeker(reader, getFileBufferSize)) w.Header().Set("Content-Type", contentType)
}
fileName := uri.Addr
if found := path.Base(uri.Path); found != "" && found != "." && found != "/" {
fileName = found
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
http.ServeContent(w, r, fileName, time.Now(), newBufferedReadSeeker(reader, getFileBufferSize))
} }
// The size of buffer used for bufio.Reader on LazyChunkReader passed to // The size of buffer used for bufio.Reader on LazyChunkReader passed to

View file

@ -30,12 +30,16 @@ import (
"math/big" "math/big"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url"
"os" "os"
"path"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
@ -121,8 +125,8 @@ func TestBzzResourceMultihash(t *testing.T) {
// add the data our multihash aliased manifest will point to // add the data our multihash aliased manifest will point to
databytes := "bar" databytes := "bar"
url := fmt.Sprintf("%s/bzz:/", srv.URL) testBzzUrl := fmt.Sprintf("%s/bzz:/", srv.URL)
resp, err := http.Post(url, "text/plain", bytes.NewReader([]byte(databytes))) resp, err := http.Post(testBzzUrl, "text/plain", bytes.NewReader([]byte(databytes)))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -140,33 +144,27 @@ func TestBzzResourceMultihash(t *testing.T) {
log.Info("added data", "manifest", string(b), "data", common.ToHex(mh)) log.Info("added data", "manifest", string(b), "data", common.ToHex(mh))
// our mutable resource "name" topic, _ := mru.NewTopic("foo.eth", nil)
keybytes := "foo.eth" updateRequest := mru.NewFirstRequest(topic)
updateRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ updateRequest.SetData(mh)
Name: keybytes,
Frequency: 13,
StartTime: srv.GetCurrentTime(),
Owner: signer.Address(),
})
if err != nil {
t.Fatal(err)
}
updateRequest.SetData(mh, true)
if err := updateRequest.Sign(signer); err != nil { if err := updateRequest.Sign(signer); err != nil {
t.Fatal(err) t.Fatal(err)
} }
log.Info("added data", "manifest", string(b), "data", common.ToHex(mh)) log.Info("added data", "manifest", string(b), "data", common.ToHex(mh))
body, err := updateRequest.MarshalJSON() testUrl, err := url.Parse(fmt.Sprintf("%s/bzz-resource:/", srv.URL))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
query := testUrl.Query()
body := updateRequest.AppendValues(query) // this adds all query parameters and returns the data to be posted
query.Set("manifest", "1") // indicate we want a manifest back
testUrl.RawQuery = query.Encode()
// create the multihash update // create the multihash update
url = fmt.Sprintf("%s/bzz-resource:/", srv.URL) resp, err = http.Post(testUrl.String(), "application/octet-stream", bytes.NewReader(body))
resp, err = http.Post(url, "application/json", bytes.NewReader(body))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -184,14 +182,14 @@ func TestBzzResourceMultihash(t *testing.T) {
t.Fatalf("data %s could not be unmarshaled: %v", b, err) t.Fatalf("data %s could not be unmarshaled: %v", b, err)
} }
correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" correctManifestAddrHex := "6ef40ba1492cf2a029dc9a8b5896c822cf689d3cd010842f4f1744e6db8824bd"
if rsrcResp.Hex() != correctManifestAddrHex { if rsrcResp.Hex() != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp.Hex()) t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp.Hex())
} }
// get bzz manifest transparent resource resolve // get bzz manifest transparent resource resolve
url = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp) testBzzUrl = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url) resp, err = http.Get(testBzzUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -215,39 +213,38 @@ func TestBzzResource(t *testing.T) {
defer srv.Close() defer srv.Close()
// our mutable resource "name"
keybytes := "foo.eth"
// data of update 1 // data of update 1
databytes := make([]byte, 666) update1Data := make([]byte, 666)
_, err := rand.Read(databytes) update1Timestamp := srv.CurrentTime
_, err := rand.Read(update1Data)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
//data for update 2
update2Data := []byte("foo")
updateRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ topic, _ := mru.NewTopic("foo.eth", nil)
Name: keybytes, updateRequest := mru.NewFirstRequest(topic)
Frequency: 13,
StartTime: srv.GetCurrentTime(),
Owner: signer.Address(),
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
updateRequest.SetData(databytes, false) updateRequest.SetData(update1Data)
if err := updateRequest.Sign(signer); err != nil { if err := updateRequest.Sign(signer); err != nil {
t.Fatal(err) t.Fatal(err)
} }
body, err := updateRequest.MarshalJSON() // creates resource and sets update 1
testUrl, err := url.Parse(fmt.Sprintf("%s/bzz-resource:/", srv.URL))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
urlQuery := testUrl.Query()
body := updateRequest.AppendValues(urlQuery) // this adds all query parameters
urlQuery.Set("manifest", "1") // indicate we want a manifest back
testUrl.RawQuery = urlQuery.Encode()
// creates resource and sets update 1 resp, err := http.Post(testUrl.String(), "application/octet-stream", bytes.NewReader(body))
url := fmt.Sprintf("%s/bzz-resource:/", srv.URL)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -265,14 +262,14 @@ func TestBzzResource(t *testing.T) {
t.Fatalf("data %s could not be unmarshaled: %v", b, err) t.Fatalf("data %s could not be unmarshaled: %v", b, err)
} }
correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" correctManifestAddrHex := "6ef40ba1492cf2a029dc9a8b5896c822cf689d3cd010842f4f1744e6db8824bd"
if rsrcResp.Hex() != correctManifestAddrHex { if rsrcResp.Hex() != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp.Hex()) t.Fatalf("Response resource manifest mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp.Hex())
} }
// get the manifest // get the manifest
url = fmt.Sprintf("%s/bzz-raw:/%s", srv.URL, rsrcResp) testRawUrl := fmt.Sprintf("%s/bzz-raw:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url) resp, err = http.Get(testRawUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -292,20 +289,20 @@ func TestBzzResource(t *testing.T) {
if len(manifest.Entries) != 1 { if len(manifest.Entries) != 1 {
t.Fatalf("Manifest has %d entries", len(manifest.Entries)) t.Fatalf("Manifest has %d entries", len(manifest.Entries))
} }
correctRootKeyHex := "68f7ba07ac8867a4c841a4d4320e3cdc549df23702dc7285fcb6acf65df48562" correctViewHex := "0x666f6f2e65746800000000000000000000000000000000000000000000000000c96aaa54e2d44c299564da76e1cd3184a2386b8d"
if manifest.Entries[0].Hash != correctRootKeyHex { if manifest.Entries[0].ResourceView.Hex() != correctViewHex {
t.Fatalf("Expected manifest path '%s', got '%s'", correctRootKeyHex, manifest.Entries[0].Hash) t.Fatalf("Expected manifest Resource View '%s', got '%s'", correctViewHex, manifest.Entries[0].ResourceView.Hex())
} }
// get bzz manifest transparent resource resolve // get bzz manifest transparent resource resolve
url = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp) testBzzUrl := fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url) resp, err = http.Get(testBzzUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode == http.StatusOK {
t.Fatalf("err %s", resp.Status) t.Fatal("Expected error status since resource is not multihash. Received 200 OK")
} }
b, err = ioutil.ReadAll(resp.Body) b, err = ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -313,8 +310,8 @@ func TestBzzResource(t *testing.T) {
} }
// get non-existent name, should fail // get non-existent name, should fail
url = fmt.Sprintf("%s/bzz-resource:/bar", srv.URL) testBzzResUrl := fmt.Sprintf("%s/bzz-resource:/bar", srv.URL)
resp, err = http.Get(url) resp, err = http.Get(testBzzResUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -327,8 +324,8 @@ func TestBzzResource(t *testing.T) {
// get latest update (1.1) through resource directly // get latest update (1.1) through resource directly
log.Info("get update latest = 1.1", "addr", correctManifestAddrHex) log.Info("get update latest = 1.1", "addr", correctManifestAddrHex)
url = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex) testBzzResUrl = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url) resp, err = http.Get(testBzzResUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -340,16 +337,18 @@ func TestBzzResource(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(databytes, b) { if !bytes.Equal(update1Data, b) {
t.Fatalf("Expected body '%x', got '%x'", databytes, b) t.Fatalf("Expected body '%x', got '%x'", update1Data, b)
} }
// update 2 // update 2
// Move the clock ahead 1 second
srv.CurrentTime++
log.Info("update 2") log.Info("update 2")
// 1.- get metadata about this resource // 1.- get metadata about this resource
url = fmt.Sprintf("%s/bzz-resource:/%s/", srv.URL, correctManifestAddrHex) testBzzResUrl = fmt.Sprintf("%s/bzz-resource:/%s/", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url + "meta") resp, err = http.Get(testBzzResUrl + "?meta=1")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -365,17 +364,19 @@ func TestBzzResource(t *testing.T) {
if err = updateRequest.UnmarshalJSON(b); err != nil { if err = updateRequest.UnmarshalJSON(b); err != nil {
t.Fatalf("Error decoding resource metadata: %s", err) t.Fatalf("Error decoding resource metadata: %s", err)
} }
data := []byte("foo") updateRequest.SetData(update2Data)
updateRequest.SetData(data, false)
if err = updateRequest.Sign(signer); err != nil { if err = updateRequest.Sign(signer); err != nil {
t.Fatal(err) t.Fatal(err)
} }
body, err = updateRequest.MarshalJSON() testUrl, err = url.Parse(fmt.Sprintf("%s/bzz-resource:/", srv.URL))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
urlQuery = testUrl.Query()
body = updateRequest.AppendValues(urlQuery) // this adds all query parameters
testUrl.RawQuery = urlQuery.Encode()
resp, err = http.Post(url, "application/json", bytes.NewReader(body)) resp, err = http.Post(testUrl.String(), "application/octet-stream", bytes.NewReader(body))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -386,8 +387,8 @@ func TestBzzResource(t *testing.T) {
// get latest update (1.2) through resource directly // get latest update (1.2) through resource directly
log.Info("get update 1.2") log.Info("get update 1.2")
url = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex) testBzzResUrl = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url) resp, err = http.Get(testBzzResUrl)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -399,33 +400,23 @@ func TestBzzResource(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(data, b) { if !bytes.Equal(update2Data, b) {
t.Fatalf("Expected body '%x', got '%x'", data, b) t.Fatalf("Expected body '%x', got '%x'", update2Data, b)
} }
// get latest update (1.2) with specified period // test manifest-less queries
log.Info("get update latest = 1.2") log.Info("get first update in update1Timestamp via direct query")
url = fmt.Sprintf("%s/bzz-resource:/%s/1", srv.URL, correctManifestAddrHex) query := mru.NewQuery(&updateRequest.View, update1Timestamp, lookup.NoClue)
resp, err = http.Get(url)
urlq, err := url.Parse(fmt.Sprintf("%s/bzz-resource:/", srv.URL))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, b) {
t.Fatalf("Expected body '%x', got '%x'", data, b)
}
// get first update (1.1) with specified period and version values := urlq.Query()
log.Info("get first update 1.1") query.AppendValues(values) // this adds view query parameters
url = fmt.Sprintf("%s/bzz-resource:/%s/1/1", srv.URL, correctManifestAddrHex) urlq.RawQuery = values.Encode()
resp, err = http.Get(url) resp, err = http.Get(urlq.String())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -437,9 +428,10 @@ func TestBzzResource(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !bytes.Equal(databytes, b) { if !bytes.Equal(update1Data, b) {
t.Fatalf("Expected body '%x', got '%x'", databytes, b) t.Fatalf("Expected body '%x', got '%x'", update1Data, b)
} }
} }
func TestBzzGetPath(t *testing.T) { func TestBzzGetPath(t *testing.T) {
@ -773,6 +765,16 @@ func testBzzTar(encrypted bool, t *testing.T) {
} }
defer resp2.Body.Close() defer resp2.Body.Close()
if h := resp2.Header.Get("Content-Type"); h != "application/x-tar" {
t.Fatalf("Content-Type header expected: application/x-tar, got: %s", h)
}
expectedFileName := string(swarmHash) + ".tar"
expectedContentDisposition := fmt.Sprintf("inline; filename=\"%s\"", expectedFileName)
if h := resp2.Header.Get("Content-Disposition"); h != expectedContentDisposition {
t.Fatalf("Content-Disposition header expected: %s, got: %s", expectedContentDisposition, h)
}
file, err := ioutil.TempFile("", "swarm-downloaded-tarball") file, err := ioutil.TempFile("", "swarm-downloaded-tarball")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -1108,7 +1110,7 @@ func TestModify(t *testing.T) {
res, body := httpDo(testCase.method, testCase.uri, reqBody, testCase.headers, testCase.verbose, t) res, body := httpDo(testCase.method, testCase.uri, reqBody, testCase.headers, testCase.verbose, t)
if res.StatusCode != testCase.expectedStatusCode { if res.StatusCode != testCase.expectedStatusCode {
t.Fatalf("expected status code %d but got %d", testCase.expectedStatusCode, res.StatusCode) t.Fatalf("expected status code %d but got %d, %s", testCase.expectedStatusCode, res.StatusCode, body)
} }
if testCase.assertResponseBody != "" && !strings.Contains(body, testCase.assertResponseBody) { if testCase.assertResponseBody != "" && !strings.Contains(body, testCase.assertResponseBody) {
t.Log(body) t.Log(body)
@ -1219,19 +1221,25 @@ func TestBzzGetFileWithResolver(t *testing.T) {
hash := common.HexToHash(string(swarmHash)) hash := common.HexToHash(string(swarmHash))
resolver.hash = &hash resolver.hash = &hash
for _, v := range []struct { for _, v := range []struct {
addr string addr string
path string path string
expectedStatusCode int expectedStatusCode int
expectedContentType string
expectedFileName string
}{ }{
{ {
addr: string(swarmHash), addr: string(swarmHash),
path: fileNames[0], path: fileNames[0],
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
expectedContentType: "text/plain",
expectedFileName: path.Base(fileNames[0]),
}, },
{ {
addr: "somebogusensname", addr: "somebogusensname",
path: fileNames[0], path: fileNames[0],
expectedStatusCode: http.StatusOK, expectedStatusCode: http.StatusOK,
expectedContentType: "text/plain",
expectedFileName: path.Base(fileNames[0]),
}, },
} { } {
req, err := http.NewRequest("GET", fmt.Sprintf(srv.URL+"/bzz:/%s/%s", v.addr, v.path), nil) req, err := http.NewRequest("GET", fmt.Sprintf(srv.URL+"/bzz:/%s/%s", v.addr, v.path), nil)
@ -1246,6 +1254,16 @@ func TestBzzGetFileWithResolver(t *testing.T) {
if serverResponse.StatusCode != v.expectedStatusCode { if serverResponse.StatusCode != v.expectedStatusCode {
t.Fatalf("expected %d, got %d", v.expectedStatusCode, serverResponse.StatusCode) t.Fatalf("expected %d, got %d", v.expectedStatusCode, serverResponse.StatusCode)
} }
if h := serverResponse.Header.Get("Content-Type"); h != v.expectedContentType {
t.Fatalf("Content-Type header expected: %s, got %s", v.expectedContentType, h)
}
expectedContentDisposition := fmt.Sprintf("inline; filename=\"%s\"", v.expectedFileName)
if h := serverResponse.Header.Get("Content-Disposition"); h != expectedContentDisposition {
t.Fatalf("Content-Disposition header expected: %s, got: %s", expectedContentDisposition, h)
}
} }
} }

View file

@ -27,6 +27,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/swarm/storage/mru"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/log"
"github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage"
@ -46,14 +48,15 @@ type Manifest struct {
// ManifestEntry represents an entry in a swarm manifest // ManifestEntry represents an entry in a swarm manifest
type ManifestEntry struct { type ManifestEntry struct {
Hash string `json:"hash,omitempty"` Hash string `json:"hash,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
ContentType string `json:"contentType,omitempty"` ContentType string `json:"contentType,omitempty"`
Mode int64 `json:"mode,omitempty"` Mode int64 `json:"mode,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
ModTime time.Time `json:"mod_time,omitempty"` ModTime time.Time `json:"mod_time,omitempty"`
Status int `json:"status,omitempty"` Status int `json:"status,omitempty"`
Access *AccessEntry `json:"access,omitempty"` Access *AccessEntry `json:"access,omitempty"`
ResourceView *mru.View `json:"resourceView,omitempty"`
} }
// ManifestList represents the result of listing files in a manifest // ManifestList represents the result of listing files in a manifest
@ -79,11 +82,11 @@ func (a *API) NewManifest(ctx context.Context, toEncrypt bool) (storage.Address,
// Manifest hack for supporting Mutable Resource Updates from the bzz: scheme // Manifest hack for supporting Mutable Resource Updates from the bzz: scheme
// see swarm/api/api.go:API.Get() for more information // see swarm/api/api.go:API.Get() for more information
func (a *API) NewResourceManifest(ctx context.Context, resourceAddr string) (storage.Address, error) { func (a *API) NewResourceManifest(ctx context.Context, view *mru.View) (storage.Address, error) {
var manifest Manifest var manifest Manifest
entry := ManifestEntry{ entry := ManifestEntry{
Hash: resourceAddr, ResourceView: view,
ContentType: ResourceContentType, ContentType: ResourceContentType,
} }
manifest.Entries = append(manifest.Entries, entry) manifest.Entries = append(manifest.Entries, entry)
data, err := json.Marshal(&manifest) data, err := json.Marshal(&manifest)

View file

@ -32,6 +32,8 @@ var searchTimeout = 1 * time.Second
// Also used in stream delivery. // Also used in stream delivery.
var RequestTimeout = 10 * time.Second var RequestTimeout = 10 * time.Second
var maxHopCount uint8 = 20 // maximum number of forwarded requests (hops), to make sure requests are not forwarded forever in peer loops
type RequestFunc func(context.Context, *Request) (*enode.ID, chan struct{}, error) type RequestFunc func(context.Context, *Request) (*enode.ID, chan struct{}, error)
// Fetcher is created when a chunk is not found locally. It starts a request handler loop once and // Fetcher is created when a chunk is not found locally. It starts a request handler loop once and
@ -44,7 +46,7 @@ type Fetcher struct {
protoRequestFunc RequestFunc // request function fetcher calls to issue retrieve request for a chunk protoRequestFunc RequestFunc // request function fetcher calls to issue retrieve request for a chunk
addr storage.Address // the address of the chunk to be fetched addr storage.Address // the address of the chunk to be fetched
offerC chan *enode.ID // channel of sources (peer node id strings) offerC chan *enode.ID // channel of sources (peer node id strings)
requestC chan struct{} requestC chan uint8 // channel for incoming requests (with the hopCount value in it)
skipCheck bool skipCheck bool
} }
@ -53,6 +55,7 @@ type Request struct {
Source *enode.ID // nodeID of peer to request from (can be nil) Source *enode.ID // nodeID of peer to request from (can be nil)
SkipCheck bool // whether to offer the chunk first or deliver directly SkipCheck bool // whether to offer the chunk first or deliver directly
peersToSkip *sync.Map // peers not to request chunk from (only makes sense if source is nil) peersToSkip *sync.Map // peers not to request chunk from (only makes sense if source is nil)
HopCount uint8 // number of forwarded requests (hops)
} }
// NewRequest returns a new instance of Request based on chunk address skip check and // NewRequest returns a new instance of Request based on chunk address skip check and
@ -113,7 +116,7 @@ func NewFetcher(addr storage.Address, rf RequestFunc, skipCheck bool) *Fetcher {
addr: addr, addr: addr,
protoRequestFunc: rf, protoRequestFunc: rf,
offerC: make(chan *enode.ID), offerC: make(chan *enode.ID),
requestC: make(chan struct{}), requestC: make(chan uint8),
skipCheck: skipCheck, skipCheck: skipCheck,
} }
} }
@ -136,7 +139,7 @@ func (f *Fetcher) Offer(ctx context.Context, source *enode.ID) {
} }
// Request is called when an upstream peer request the chunk as part of `RetrieveRequestMsg`, or from a local request through FileStore, and the node does not have the chunk locally. // Request is called when an upstream peer request the chunk as part of `RetrieveRequestMsg`, or from a local request through FileStore, and the node does not have the chunk locally.
func (f *Fetcher) Request(ctx context.Context) { func (f *Fetcher) Request(ctx context.Context, hopCount uint8) {
// First we need to have this select to make sure that we return if context is done // First we need to have this select to make sure that we return if context is done
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -144,10 +147,15 @@ func (f *Fetcher) Request(ctx context.Context) {
default: default:
} }
if hopCount >= maxHopCount {
log.Debug("fetcher request hop count limit reached", "hops", hopCount)
return
}
// This select alone would not guarantee that we return of context is done, it could potentially // This select alone would not guarantee that we return of context is done, it could potentially
// push to offerC instead if offerC is available (see number 2 in https://golang.org/ref/spec#Select_statements) // push to offerC instead if offerC is available (see number 2 in https://golang.org/ref/spec#Select_statements)
select { select {
case f.requestC <- struct{}{}: case f.requestC <- hopCount + 1:
case <-ctx.Done(): case <-ctx.Done():
} }
} }
@ -161,6 +169,7 @@ func (f *Fetcher) run(ctx context.Context, peers *sync.Map) {
waitC <-chan time.Time // timer channel waitC <-chan time.Time // timer channel
sources []*enode.ID // known sources, ie. peers that offered the chunk sources []*enode.ID // known sources, ie. peers that offered the chunk
requested bool // true if the chunk was actually requested requested bool // true if the chunk was actually requested
hopCount uint8
) )
gone := make(chan *enode.ID) // channel to signal that a peer we requested from disconnected gone := make(chan *enode.ID) // channel to signal that a peer we requested from disconnected
@ -183,7 +192,7 @@ func (f *Fetcher) run(ctx context.Context, peers *sync.Map) {
doRequest = requested doRequest = requested
// incoming request // incoming request
case <-f.requestC: case hopCount = <-f.requestC:
log.Trace("new request", "request addr", f.addr) log.Trace("new request", "request addr", f.addr)
// 2) chunk is requested, set requested flag // 2) chunk is requested, set requested flag
// launch a request iff none been launched yet // launch a request iff none been launched yet
@ -213,7 +222,7 @@ func (f *Fetcher) run(ctx context.Context, peers *sync.Map) {
// need to issue a new request // need to issue a new request
if doRequest { if doRequest {
var err error var err error
sources, err = f.doRequest(ctx, gone, peers, sources) sources, err = f.doRequest(ctx, gone, peers, sources, hopCount)
if err != nil { if err != nil {
log.Info("unable to request", "request addr", f.addr, "err", err) log.Info("unable to request", "request addr", f.addr, "err", err)
} }
@ -251,7 +260,7 @@ func (f *Fetcher) run(ctx context.Context, peers *sync.Map) {
// * the peer's address is added to the set of peers to skip // * the peer's address is added to the set of peers to skip
// * the peer's address is removed from prospective sources, and // * the peer's address is removed from prospective sources, and
// * a go routine is started that reports on the gone channel if the peer is disconnected (or terminated their streamer) // * a go routine is started that reports on the gone channel if the peer is disconnected (or terminated their streamer)
func (f *Fetcher) doRequest(ctx context.Context, gone chan *enode.ID, peersToSkip *sync.Map, sources []*enode.ID) ([]*enode.ID, error) { func (f *Fetcher) doRequest(ctx context.Context, gone chan *enode.ID, peersToSkip *sync.Map, sources []*enode.ID, hopCount uint8) ([]*enode.ID, error) {
var i int var i int
var sourceID *enode.ID var sourceID *enode.ID
var quit chan struct{} var quit chan struct{}
@ -260,6 +269,7 @@ func (f *Fetcher) doRequest(ctx context.Context, gone chan *enode.ID, peersToSki
Addr: f.addr, Addr: f.addr,
SkipCheck: f.skipCheck, SkipCheck: f.skipCheck,
peersToSkip: peersToSkip, peersToSkip: peersToSkip,
HopCount: hopCount,
} }
foundSource := false foundSource := false

View file

@ -33,7 +33,7 @@ type mockRequester struct {
// requests []Request // requests []Request
requestC chan *Request // when a request is coming it is pushed to requestC requestC chan *Request // when a request is coming it is pushed to requestC
waitTimes []time.Duration // with waitTimes[i] you can define how much to wait on the ith request (optional) waitTimes []time.Duration // with waitTimes[i] you can define how much to wait on the ith request (optional)
ctr int //counts the number of requests count int //counts the number of requests
quitC chan struct{} quitC chan struct{}
} }
@ -47,9 +47,9 @@ func newMockRequester(waitTimes ...time.Duration) *mockRequester {
func (m *mockRequester) doRequest(ctx context.Context, request *Request) (*enode.ID, chan struct{}, error) { func (m *mockRequester) doRequest(ctx context.Context, request *Request) (*enode.ID, chan struct{}, error) {
waitTime := time.Duration(0) waitTime := time.Duration(0)
if m.ctr < len(m.waitTimes) { if m.count < len(m.waitTimes) {
waitTime = m.waitTimes[m.ctr] waitTime = m.waitTimes[m.count]
m.ctr++ m.count++
} }
time.Sleep(waitTime) time.Sleep(waitTime)
m.requestC <- request m.requestC <- request
@ -83,7 +83,7 @@ func TestFetcherSingleRequest(t *testing.T) {
go fetcher.run(ctx, peersToSkip) go fetcher.run(ctx, peersToSkip)
rctx := context.Background() rctx := context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
select { select {
case request := <-requester.requestC: case request := <-requester.requestC:
@ -100,6 +100,11 @@ func TestFetcherSingleRequest(t *testing.T) {
t.Fatalf("request.peersToSkip does not contain peer returned by the request function") t.Fatalf("request.peersToSkip does not contain peer returned by the request function")
} }
// hopCount in the forwarded request should be incremented
if request.HopCount != 1 {
t.Fatalf("Expected request.HopCount 1 got %v", request.HopCount)
}
// fetch should trigger a request, if it doesn't happen in time, test should fail // fetch should trigger a request, if it doesn't happen in time, test should fail
case <-time.After(200 * time.Millisecond): case <-time.After(200 * time.Millisecond):
t.Fatalf("fetch timeout") t.Fatalf("fetch timeout")
@ -123,7 +128,7 @@ func TestFetcherCancelStopsFetcher(t *testing.T) {
rctx, rcancel := context.WithTimeout(ctx, 100*time.Millisecond) rctx, rcancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer rcancel() defer rcancel()
// we call Request with an active context // we call Request with an active context
fetcher.Request(rctx) fetcher.Request(rctx, 0)
// fetcher should not initiate request, we can only check by waiting a bit and making sure no request is happening // fetcher should not initiate request, we can only check by waiting a bit and making sure no request is happening
select { select {
@ -151,7 +156,7 @@ func TestFetcherCancelStopsRequest(t *testing.T) {
rcancel() rcancel()
// we call Request with a cancelled context // we call Request with a cancelled context
fetcher.Request(rctx) fetcher.Request(rctx, 0)
// fetcher should not initiate request, we can only check by waiting a bit and making sure no request is happening // fetcher should not initiate request, we can only check by waiting a bit and making sure no request is happening
select { select {
@ -162,7 +167,7 @@ func TestFetcherCancelStopsRequest(t *testing.T) {
// if there is another Request with active context, there should be a request, because the fetcher itself is not cancelled // if there is another Request with active context, there should be a request, because the fetcher itself is not cancelled
rctx = context.Background() rctx = context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
select { select {
case <-requester.requestC: case <-requester.requestC:
@ -200,7 +205,7 @@ func TestFetcherOfferUsesSource(t *testing.T) {
// call Request after the Offer // call Request after the Offer
rctx = context.Background() rctx = context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
// there should be exactly 1 request coming from fetcher // there should be exactly 1 request coming from fetcher
var request *Request var request *Request
@ -241,7 +246,7 @@ func TestFetcherOfferAfterRequestUsesSourceFromContext(t *testing.T) {
// call Request first // call Request first
rctx := context.Background() rctx := context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
// there should be a request coming from fetcher // there should be a request coming from fetcher
var request *Request var request *Request
@ -296,7 +301,7 @@ func TestFetcherRetryOnTimeout(t *testing.T) {
// call the fetch function with an active context // call the fetch function with an active context
rctx := context.Background() rctx := context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
// after 100ms the first request should be initiated // after 100ms the first request should be initiated
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -338,7 +343,7 @@ func TestFetcherFactory(t *testing.T) {
fetcher := fetcherFactory.New(context.Background(), addr, peersToSkip) fetcher := fetcherFactory.New(context.Background(), addr, peersToSkip)
fetcher.Request(context.Background()) fetcher.Request(context.Background(), 0)
// check if the created fetchFunction really starts a fetcher and initiates a request // check if the created fetchFunction really starts a fetcher and initiates a request
select { select {
@ -368,7 +373,7 @@ func TestFetcherRequestQuitRetriesRequest(t *testing.T) {
go fetcher.run(ctx, peersToSkip) go fetcher.run(ctx, peersToSkip)
rctx := context.Background() rctx := context.Background()
fetcher.Request(rctx) fetcher.Request(rctx, 0)
select { select {
case <-requester.requestC: case <-requester.requestC:
@ -457,3 +462,26 @@ func TestRequestSkipPeerPermanent(t *testing.T) {
t.Errorf("peer not skipped") t.Errorf("peer not skipped")
} }
} }
func TestFetcherMaxHopCount(t *testing.T) {
requester := newMockRequester()
addr := make([]byte, 32)
fetcher := NewFetcher(addr, requester.doRequest, true)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
peersToSkip := &sync.Map{}
go fetcher.run(ctx, peersToSkip)
rctx := context.Background()
fetcher.Request(rctx, maxHopCount)
// if hopCount is already at max no request should be initiated
select {
case <-requester.requestC:
t.Fatalf("cancelled fetcher initiated request")
case <-time.After(200 * time.Millisecond):
}
}

View file

@ -84,7 +84,7 @@ func createGlobalStore() (string, *mockdb.GlobalStore, error) {
return globalStoreDir, globalStore, nil return globalStoreDir, globalStore, nil
} }
func newStreamerTester(t *testing.T) (*p2ptest.ProtocolTester, *Registry, *storage.LocalStore, func(), error) { func newStreamerTester(t *testing.T, registryOptions *RegistryOptions) (*p2ptest.ProtocolTester, *Registry, *storage.LocalStore, func(), error) {
// setup // setup
addr := network.RandomAddr() // tested peers peer address addr := network.RandomAddr() // tested peers peer address
to := network.NewKademlia(addr.OAddr, network.NewKadParams()) to := network.NewKademlia(addr.OAddr, network.NewKadParams())
@ -114,7 +114,7 @@ func newStreamerTester(t *testing.T) (*p2ptest.ProtocolTester, *Registry, *stora
delivery := NewDelivery(to, netStore) delivery := NewDelivery(to, netStore)
netStore.NewNetFetcherFunc = network.NewFetcherFactory(delivery.RequestFromPeers, true).New netStore.NewNetFetcherFunc = network.NewFetcherFactory(delivery.RequestFromPeers, true).New
streamer := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), nil) streamer := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), registryOptions)
teardown := func() { teardown := func() {
streamer.Close() streamer.Close()
removeDataDir() removeDataDir()

View file

@ -128,6 +128,7 @@ func (s *SwarmChunkServer) GetData(ctx context.Context, key []byte) ([]byte, err
type RetrieveRequestMsg struct { type RetrieveRequestMsg struct {
Addr storage.Address Addr storage.Address
SkipCheck bool SkipCheck bool
HopCount uint8
} }
func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *RetrieveRequestMsg) error { func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *RetrieveRequestMsg) error {
@ -148,7 +149,9 @@ func (d *Delivery) handleRetrieveRequestMsg(ctx context.Context, sp *Peer, req *
var cancel func() var cancel func()
// TODO: do something with this hardcoded timeout, maybe use TTL in the future // TODO: do something with this hardcoded timeout, maybe use TTL in the future
ctx, cancel = context.WithTimeout(context.WithValue(ctx, "peer", sp.ID().String()), network.RequestTimeout) ctx = context.WithValue(ctx, "peer", sp.ID().String())
ctx = context.WithValue(ctx, "hopcount", req.HopCount)
ctx, cancel = context.WithTimeout(ctx, network.RequestTimeout)
go func() { go func() {
select { select {
@ -247,6 +250,7 @@ func (d *Delivery) RequestFromPeers(ctx context.Context, req *network.Request) (
err := sp.SendPriority(ctx, &RetrieveRequestMsg{ err := sp.SendPriority(ctx, &RetrieveRequestMsg{
Addr: req.Addr, Addr: req.Addr,
SkipCheck: req.SkipCheck, SkipCheck: req.SkipCheck,
HopCount: req.HopCount,
}, Top) }, Top)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View file

@ -39,7 +39,7 @@ import (
) )
func TestStreamerRetrieveRequest(t *testing.T) { func TestStreamerRetrieveRequest(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -75,7 +75,7 @@ func TestStreamerRetrieveRequest(t *testing.T) {
} }
func TestStreamerUpstreamRetrieveRequestMsgExchangeWithoutStore(t *testing.T) { func TestStreamerUpstreamRetrieveRequestMsgExchangeWithoutStore(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -127,7 +127,7 @@ func TestStreamerUpstreamRetrieveRequestMsgExchangeWithoutStore(t *testing.T) {
// upstream request server receives a retrieve Request and responds with // upstream request server receives a retrieve Request and responds with
// offered hashes or delivery if skipHash is set to true // offered hashes or delivery if skipHash is set to true
func TestStreamerUpstreamRetrieveRequestMsgExchange(t *testing.T) { func TestStreamerUpstreamRetrieveRequestMsgExchange(t *testing.T) {
tester, streamer, localStore, teardown, err := newStreamerTester(t) tester, streamer, localStore, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -221,7 +221,7 @@ func TestStreamerUpstreamRetrieveRequestMsgExchange(t *testing.T) {
} }
func TestStreamerDownstreamChunkDeliveryMsgExchange(t *testing.T) { func TestStreamerDownstreamChunkDeliveryMsgExchange(t *testing.T) {
tester, streamer, localStore, teardown, err := newStreamerTester(t) tester, streamer, localStore, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -84,11 +84,13 @@ func (p *Peer) handleSubscribeMsg(ctx context.Context, req *SubscribeMsg) (err e
defer func() { defer func() {
if err != nil { if err != nil {
if e := p.Send(context.TODO(), SubscribeErrorMsg{ // The error will be sent as a subscribe error message
// and will not be returned as it will prevent any new message
// exchange between peers over p2p. Instead, error will be returned
// only if there is one from sending subscribe error message.
err = p.Send(context.TODO(), SubscribeErrorMsg{
Error: err.Error(), Error: err.Error(),
}); e != nil { })
log.Error("send stream subscribe error message", "err", err)
}
} }
}() }()

View file

@ -18,6 +18,7 @@ package stream
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sync" "sync"
"time" "time"
@ -46,6 +47,10 @@ func (e *notFoundError) Error() string {
return fmt.Sprintf("%s not found for stream %q", e.t, e.s) return fmt.Sprintf("%s not found for stream %q", e.t, e.s)
} }
// ErrMaxPeerServers will be returned if peer server limit is reached.
// It will be sent in the SubscribeErrorMsg.
var ErrMaxPeerServers = errors.New("max peer servers")
// Peer is the Peer extension for the streaming protocol // Peer is the Peer extension for the streaming protocol
type Peer struct { type Peer struct {
*protocols.Peer *protocols.Peer
@ -204,6 +209,11 @@ func (p *Peer) setServer(s Stream, o Server, priority uint8) (*server, error) {
if p.servers[s] != nil { if p.servers[s] != nil {
return nil, fmt.Errorf("server %s already registered", s) return nil, fmt.Errorf("server %s already registered", s)
} }
if p.streamer.maxPeerServers > 0 && len(p.servers) >= p.streamer.maxPeerServers {
return nil, ErrMaxPeerServers
}
os := &server{ os := &server{
Server: o, Server: o,
stream: s, stream: s,
@ -346,6 +356,7 @@ func (p *Peer) removeClient(s Stream) error {
return newNotFoundError("client", s) return newNotFoundError("client", s)
} }
client.close() client.close()
delete(p.clients, s)
return nil return nil
} }

View file

@ -60,6 +60,7 @@ type Registry struct {
delivery *Delivery delivery *Delivery
intervalsStore state.Store intervalsStore state.Store
doRetrieve bool doRetrieve bool
maxPeerServers int
} }
// RegistryOptions holds optional values for NewRegistry constructor. // RegistryOptions holds optional values for NewRegistry constructor.
@ -68,6 +69,7 @@ type RegistryOptions struct {
DoSync bool DoSync bool
DoRetrieve bool DoRetrieve bool
SyncUpdateDelay time.Duration SyncUpdateDelay time.Duration
MaxPeerServers int // The limit of servers for each peer in registry
} }
// NewRegistry is Streamer constructor // NewRegistry is Streamer constructor
@ -87,6 +89,7 @@ func NewRegistry(localID enode.ID, delivery *Delivery, syncChunkStore storage.Sy
delivery: delivery, delivery: delivery,
intervalsStore: intervalsStore, intervalsStore: intervalsStore,
doRetrieve: options.DoRetrieve, doRetrieve: options.DoRetrieve,
maxPeerServers: options.MaxPeerServers,
} }
streamer.api = NewAPI(streamer) streamer.api = NewAPI(streamer)
delivery.getPeer = streamer.getPeer delivery.getPeer = streamer.getPeer
@ -639,7 +642,7 @@ func (c *clientParams) clientCreated() {
// Spec is the spec of the streamer protocol // Spec is the spec of the streamer protocol
var Spec = &protocols.Spec{ var Spec = &protocols.Spec{
Name: "stream", Name: "stream",
Version: 6, Version: 7,
MaxMsgSize: 10 * 1024 * 1024, MaxMsgSize: 10 * 1024 * 1024,
Messages: []interface{}{ Messages: []interface{}{
UnsubscribeMsg{}, UnsubscribeMsg{},

View file

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"strconv"
"testing" "testing"
"time" "time"
@ -28,7 +29,7 @@ import (
) )
func TestStreamerSubscribe(t *testing.T) { func TestStreamerSubscribe(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -42,7 +43,7 @@ func TestStreamerSubscribe(t *testing.T) {
} }
func TestStreamerRequestSubscription(t *testing.T) { func TestStreamerRequestSubscription(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -127,7 +128,7 @@ func (self *testServer) Close() {
} }
func TestStreamerDownstreamSubscribeUnsubscribeMsgExchange(t *testing.T) { func TestStreamerDownstreamSubscribeUnsubscribeMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -220,7 +221,7 @@ func TestStreamerDownstreamSubscribeUnsubscribeMsgExchange(t *testing.T) {
} }
func TestStreamerUpstreamSubscribeUnsubscribeMsgExchange(t *testing.T) { func TestStreamerUpstreamSubscribeUnsubscribeMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -287,7 +288,7 @@ func TestStreamerUpstreamSubscribeUnsubscribeMsgExchange(t *testing.T) {
} }
func TestStreamerUpstreamSubscribeUnsubscribeMsgExchangeLive(t *testing.T) { func TestStreamerUpstreamSubscribeUnsubscribeMsgExchangeLive(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -353,7 +354,7 @@ func TestStreamerUpstreamSubscribeUnsubscribeMsgExchangeLive(t *testing.T) {
} }
func TestStreamerUpstreamSubscribeErrorMsgExchange(t *testing.T) { func TestStreamerUpstreamSubscribeErrorMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -397,7 +398,7 @@ func TestStreamerUpstreamSubscribeErrorMsgExchange(t *testing.T) {
} }
func TestStreamerUpstreamSubscribeLiveAndHistory(t *testing.T) { func TestStreamerUpstreamSubscribeLiveAndHistory(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -462,7 +463,7 @@ func TestStreamerUpstreamSubscribeLiveAndHistory(t *testing.T) {
} }
func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) { func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -477,9 +478,9 @@ func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) {
return tc, nil return tc, nil
}) })
peerID := tester.IDs[0] node := tester.Nodes[0]
err = streamer.Subscribe(peerID, stream, NewRange(5, 8), Top) err = streamer.Subscribe(node.ID(), stream, NewRange(5, 8), Top)
if err != nil { if err != nil {
t.Fatalf("Expected no error, got %v", err) t.Fatalf("Expected no error, got %v", err)
} }
@ -494,7 +495,7 @@ func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) {
History: NewRange(5, 8), History: NewRange(5, 8),
Priority: Top, Priority: Top,
}, },
Peer: peerID, Peer: node.ID(),
}, },
}, },
}, },
@ -512,7 +513,7 @@ func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) {
To: 8, To: 8,
Stream: stream, Stream: stream,
}, },
Peer: peerID, Peer: node.ID(),
}, },
}, },
}) })
@ -521,13 +522,13 @@ func TestStreamerDownstreamCorruptHashesMsgExchange(t *testing.T) {
} }
expectedError := errors.New("Message handler error: (msg code 1): error invalid hashes length (len: 40)") expectedError := errors.New("Message handler error: (msg code 1): error invalid hashes length (len: 40)")
if err := tester.TestDisconnected(&p2ptest.Disconnect{Peer: tester.IDs[0], Error: expectedError}); err != nil { if err := tester.TestDisconnected(&p2ptest.Disconnect{Peer: tester.Nodes[0].ID(), Error: expectedError}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestStreamerDownstreamOfferedHashesMsgExchange(t *testing.T) { func TestStreamerDownstreamOfferedHashesMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -626,7 +627,7 @@ func TestStreamerDownstreamOfferedHashesMsgExchange(t *testing.T) {
} }
func TestStreamerRequestSubscriptionQuitMsgExchange(t *testing.T) { func TestStreamerRequestSubscriptionQuitMsgExchange(t *testing.T) {
tester, streamer, _, teardown, err := newStreamerTester(t) tester, streamer, _, teardown, err := newStreamerTester(t, nil)
defer teardown() defer teardown()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -752,3 +753,165 @@ func TestStreamerRequestSubscriptionQuitMsgExchange(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
// TestMaxPeerServersWithUnsubscribe creates a registry with a limited
// number of stream servers, and performs a test with subscriptions and
// unsubscriptions, checking if unsubscriptions will remove streams,
// leaving place for new streams.
func TestMaxPeerServersWithUnsubscribe(t *testing.T) {
var maxPeerServers = 6
tester, streamer, _, teardown, err := newStreamerTester(t, &RegistryOptions{
MaxPeerServers: maxPeerServers,
})
defer teardown()
if err != nil {
t.Fatal(err)
}
streamer.RegisterServerFunc("foo", func(p *Peer, t string, live bool) (Server, error) {
return newTestServer(t), nil
})
node := tester.Nodes[0]
for i := 0; i < maxPeerServers+10; i++ {
stream := NewStream("foo", strconv.Itoa(i), true)
err = tester.TestExchanges(p2ptest.Exchange{
Label: "Subscribe message",
Triggers: []p2ptest.Trigger{
{
Code: 4,
Msg: &SubscribeMsg{
Stream: stream,
Priority: Top,
},
Peer: node.ID(),
},
},
Expects: []p2ptest.Expect{
{
Code: 1,
Msg: &OfferedHashesMsg{
Stream: stream,
HandoverProof: &HandoverProof{
Handover: &Handover{},
},
Hashes: make([]byte, HashSize),
From: 1,
To: 1,
},
Peer: node.ID(),
},
},
})
if err != nil {
t.Fatal(err)
}
err = tester.TestExchanges(p2ptest.Exchange{
Label: "unsubscribe message",
Triggers: []p2ptest.Trigger{
{
Code: 0,
Msg: &UnsubscribeMsg{
Stream: stream,
},
Peer: node.ID(),
},
},
})
if err != nil {
t.Fatal(err)
}
}
}
// TestMaxPeerServersWithoutUnsubscribe creates a registry with a limited
// number of stream servers, and performs subscriptions to detect subscriptions
// error message exchange.
func TestMaxPeerServersWithoutUnsubscribe(t *testing.T) {
var maxPeerServers = 6
tester, streamer, _, teardown, err := newStreamerTester(t, &RegistryOptions{
MaxPeerServers: maxPeerServers,
})
defer teardown()
if err != nil {
t.Fatal(err)
}
streamer.RegisterServerFunc("foo", func(p *Peer, t string, live bool) (Server, error) {
return newTestServer(t), nil
})
node := tester.Nodes[0]
for i := 0; i < maxPeerServers+10; i++ {
stream := NewStream("foo", strconv.Itoa(i), true)
if i >= maxPeerServers {
err = tester.TestExchanges(p2ptest.Exchange{
Label: "Subscribe message",
Triggers: []p2ptest.Trigger{
{
Code: 4,
Msg: &SubscribeMsg{
Stream: stream,
Priority: Top,
},
Peer: node.ID(),
},
},
Expects: []p2ptest.Expect{
{
Code: 7,
Msg: &SubscribeErrorMsg{
Error: ErrMaxPeerServers.Error(),
},
Peer: node.ID(),
},
},
})
if err != nil {
t.Fatal(err)
}
continue
}
err = tester.TestExchanges(p2ptest.Exchange{
Label: "Subscribe message",
Triggers: []p2ptest.Trigger{
{
Code: 4,
Msg: &SubscribeMsg{
Stream: stream,
Priority: Top,
},
Peer: node.ID(),
},
},
Expects: []p2ptest.Expect{
{
Code: 1,
Msg: &OfferedHashesMsg{
Stream: stream,
HandoverProof: &HandoverProof{
Handover: &Handover{},
},
Hashes: make([]byte, HashSize),
From: 1,
To: 1,
},
Peer: node.ID(),
},
},
})
if err != nil {
t.Fatal(err)
}
}
}

View file

@ -32,10 +32,13 @@ type hasherStore struct {
hashFunc SwarmHasher hashFunc SwarmHasher
hashSize int // content hash size hashSize int // content hash size
refSize int64 // reference size (content hash + possibly encryption key) refSize int64 // reference size (content hash + possibly encryption key)
nrChunks uint64 // number of chunks to store
errC chan error // global error channel errC chan error // global error channel
doneC chan struct{} // closed by Close() call to indicate that count is the final number of chunks doneC chan struct{} // closed by Close() call to indicate that count is the final number of chunks
quitC chan struct{} // closed to quit unterminated routines quitC chan struct{} // closed to quit unterminated routines
// nrChunks is used with atomic functions
// it is required to be at the end of the struct to ensure 64bit alignment for arm architecture
// see: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
nrChunks uint64 // number of chunks to store
} }
// NewHasherStore creates a hasherStore object, which implements Putter and Getter interfaces. // NewHasherStore creates a hasherStore object, which implements Putter and Getter interfaces.

View file

@ -0,0 +1,44 @@
// 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/common/hexutil"
type binarySerializer interface {
binaryPut(serializedData []byte) error
binaryLength() int
binaryGet(serializedData []byte) error
}
// Values interface represents a string key-value store
// useful for building query strings
type Values interface {
Get(key string) string
Set(key, value string)
}
type valueSerializer interface {
FromValues(values Values) error
AppendValues(values Values)
}
// Hex serializes the structure and converts it to a hex string
func Hex(bin binarySerializer) string {
b := make([]byte, bin.binaryLength())
bin.binaryPut(b)
return hexutil.Encode(b)
}

View file

@ -0,0 +1,98 @@
// 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/json"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// KV mocks a key value store
type KV map[string]string
func (kv KV) Get(key string) string {
return kv[key]
}
func (kv KV) Set(key, value string) {
kv[key] = value
}
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 testBinarySerializerRecovery(t *testing.T, bin binarySerializer, expectedHex string) {
name := reflect.TypeOf(bin).Elem().Name()
serialized := make([]byte, bin.binaryLength())
if err := bin.binaryPut(serialized); err != nil {
t.Fatalf("%s.binaryPut error when trying to serialize structure: %s", name, err)
}
compareByteSliceToExpectedHex(t, name, serialized, expectedHex)
recovered := reflect.New(reflect.TypeOf(bin).Elem()).Interface().(binarySerializer)
if err := recovered.binaryGet(serialized); err != nil {
t.Fatalf("%s.binaryGet error when trying to deserialize structure: %s", name, err)
}
if !reflect.DeepEqual(bin, recovered) {
t.Fatalf("Expected that the recovered %s equals the marshalled %s", name, name)
}
serializedWrongLength := make([]byte, 1)
copy(serializedWrongLength[:], serialized)
if err := recovered.binaryGet(serializedWrongLength); err == nil {
t.Fatalf("Expected %s.binaryGet to fail since data is too small", name)
}
}
func testBinarySerializerLengthCheck(t *testing.T, bin binarySerializer) {
name := reflect.TypeOf(bin).Elem().Name()
// make a slice that is too small to contain the metadata
serialized := make([]byte, bin.binaryLength()-1)
if err := bin.binaryPut(serialized); err == nil {
t.Fatalf("Expected %s.binaryPut to fail, since target slice is too small", name)
}
}
func testValueSerializer(t *testing.T, v valueSerializer, expected KV) {
name := reflect.TypeOf(v).Elem().Name()
kv := make(KV)
v.AppendValues(kv)
if !reflect.DeepEqual(expected, kv) {
expj, _ := json.Marshal(expected)
gotj, _ := json.Marshal(kv)
t.Fatalf("Expected %s.AppendValues to return %s, got %s", name, string(expj), string(gotj))
}
recovered := reflect.New(reflect.TypeOf(v).Elem()).Interface().(valueSerializer)
err := recovered.FromValues(kv)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(recovered, v) {
t.Fatalf("Expected recovered %s to be the same", name)
}
}

View file

@ -0,0 +1,48 @@
// 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"
"context"
"time"
"github.com/ethereum/go-ethereum/swarm/storage"
)
const (
hasherCount = 8
resourceHashAlgorithm = storage.SHA3Hash
defaultRetrieveTimeout = 100 * time.Millisecond
)
// cacheEntry caches resource data and the metadata of its root chunk.
type cacheEntry struct {
ResourceUpdate
*bytes.Reader
lastKey storage.Address
}
// implements storage.LazySectionReader
func (r *cacheEntry) Size(ctx context.Context, _ chan bool) (int64, error) {
return int64(len(r.ResourceUpdate.data)), nil
}
//returns the resource's topic
func (r *cacheEntry) Topic() Topic {
return r.View.Topic
}

View file

@ -1,61 +1,44 @@
// Package mru defines Mutable resource updates. /*
// A Mutable Resource is an entity which allows updates to a resource Package mru defines Mutable resource updates.
// without resorting to ENS on each update.
// The update scheme is built on swarm chunks with chunk keys following A Mutable Resource is an entity which allows updates to a resource
// a predictable, versionable pattern. without resorting to ENS on each update.
// The update scheme is built on swarm chunks with chunk keys following
// Updates are defined to be periodic in nature, where the update frequency a predictable, versionable pattern.
// is expressed in seconds.
// A Resource is tied to a unique identifier that is deterministically generated out of
// The root entry of a mutable resource is tied to a unique identifier that the chosen topic.
// is deterministically generated out of the metadata content that describes
// the resource. This metadata includes a user-defined resource name, a resource A Resource View is defined as a specific user's point of view about a particular resource.
// start time that indicates when the resource becomes valid, Thus, a View is a Topic + the user's address (userAddr)
// 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 Actual data updates are also made in the form of swarm chunks. The keys
// total of 16 bytes). It also contains the owner's address (ownerAddr) of the updates are the hash of a concatenation of properties as follows:
// This MRU info is stored in a separate content-addressed chunk
// (call it the metadata chunk), with the following layout: updateAddr = H(View, Epoch ID)
// where H is the SHA3 hash function
// (00|length|startTime|frequency|name|ownerAddr) View is the combination of Topic and the user address
// Epoch ID is a time slot. See the lookup package for more information.
// (The two first zero-value bytes are used for disambiguation by the chunk validator,
// and update chunk will always have a value > 0 there.) A user looking up a resource would only need to know the View in order to
// another user's updates
// Each metadata chunk is identified by its rootAddr, calculated as follows:
// metaHash=H(len(metadata), startTime, frequency,name) The resource update data is:
// rootAddr = H(metaHash, ownerAddr). resourcedata = View|Epoch|data
// where H is the SHA3 hash function
// This scheme effectively locks the root chunk so that only the owner of the private key the full update data that goes in the chunk payload is:
// that ownerAddr was derived from can sign updates. resourcedata|sign(resourcedata)
//
// The root entry tells the requester from when the mutable resource was Structure Summary:
// first added (Unix time in seconds) and in which moments to look for the
// actual updates. Thus, a resource update for identifier "føø.bar" Request: Resource update with signature
// starting at unix time 1528800000 with frequency 300 (every 5 mins) will have updates on 1528800300, ResourceUpdate: headers + data
// 1528800600, 1528800900 and so on. Header: Protocol version and reserved for future use placeholders
// ID: Information about how to locate a specific update
// Actual data updates are also made in the form of swarm chunks. The keys View: Author of the update and what is updating
// of the updates are the hash of a concatenation of properties as follows: Topic: Item that the updates are about
// User: User who updates the resource
// updateAddr = H(period, version, rootAddr) Epoch: time slot where the update is stored
// 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 package mru

View file

@ -21,11 +21,12 @@ package mru
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
"unsafe"
"github.com/ethereum/go-ethereum/swarm/chunk" "github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/log"
"github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage"
) )
@ -33,7 +34,7 @@ import (
type Handler struct { type Handler struct {
chunkStore *storage.NetStore chunkStore *storage.NetStore
HashSize int HashSize int
resources map[uint64]*resource resources map[uint64]*cacheEntry
resourceLock sync.RWMutex resourceLock sync.RWMutex
storeTimeout time.Duration storeTimeout time.Duration
queryMaxPeriods uint32 queryMaxPeriods uint32
@ -42,12 +43,10 @@ type Handler struct {
// HandlerParams pass parameters to the Handler constructor NewHandler // HandlerParams pass parameters to the Handler constructor NewHandler
// Signer and TimestampProvider are mandatory parameters // Signer and TimestampProvider are mandatory parameters
type HandlerParams struct { type HandlerParams struct {
QueryMaxPeriods uint32
} }
// hashPool contains a pool of ready hashers // hashPool contains a pool of ready hashers
var hashPool sync.Pool var hashPool sync.Pool
var minimumChunkLength int
// init initializes the package and hashPool // init initializes the package and hashPool
func init() { func init() {
@ -56,19 +55,12 @@ func init() {
return storage.MakeHashFunc(resourceHashAlgorithm)() return storage.MakeHashFunc(resourceHashAlgorithm)()
}, },
} }
if minimumMetadataLength < minimumUpdateDataLength {
minimumChunkLength = minimumMetadataLength
} else {
minimumChunkLength = minimumUpdateDataLength
}
} }
// NewHandler creates a new Mutable Resource API // NewHandler creates a new Mutable Resource API
func NewHandler(params *HandlerParams) *Handler { func NewHandler(params *HandlerParams) *Handler {
rh := &Handler{ rh := &Handler{
resources: make(map[uint64]*resource), resources: make(map[uint64]*cacheEntry),
storeTimeout: defaultStoreTimeout,
queryMaxPeriods: params.QueryMaxPeriods,
} }
for i := 0; i < hasherCount; i++ { for i := 0; i < hasherCount; i++ {
@ -88,44 +80,25 @@ func (h *Handler) SetStore(store *storage.NetStore) {
} }
// Validate is a chunk validation method // 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 // If it looks like a resource update, the chunk address is checked against the userAddr of the update's signature
// It implements the storage.ChunkValidator interface // It implements the storage.ChunkValidator interface
func (h *Handler) Validate(chunkAddr storage.Address, data []byte) bool { func (h *Handler) Validate(chunkAddr storage.Address, data []byte) bool {
dataLength := len(data) dataLength := len(data)
if dataLength < minimumChunkLength || dataLength > chunk.DefaultSize+8 { if dataLength < minimumSignedUpdateLength {
return false return false
} }
//metadata chunks have the first two bytes set to zero // check if it is a properly formatted update chunk with
if data[0] == 0 && data[1] == 0 && dataLength >= minimumMetadataLength {
//metadata chunk
rootAddr, _ := metadataHash(data)
valid := bytes.Equal(chunkAddr, rootAddr)
if !valid {
log.Debug("Invalid root metadata chunk with address", "addr", 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 // valid signature and proof of ownership of the resource it is trying
// to update // to update
// First, deserialize the chunk // First, deserialize the chunk
var r SignedResourceUpdate var r Request
if err := r.fromChunk(chunkAddr, data); err != nil { if err := r.fromChunk(chunkAddr, data); err != nil {
log.Debug("Invalid resource chunk", "addr", chunkAddr.Hex(), "err", err.Error()) log.Debug("Invalid resource chunk", "addr", chunkAddr.Hex(), "err", err.Error())
return false 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", "addr", chunkAddr.Hex())
return false
}
// Verify signatures and that the signer actually owns the resource // Verify signatures and that the signer actually owns the resource
// If it fails, it means either the signature is not valid, data is corrupted // If it fails, it means either the signature is not valid, data is corrupted
// or someone is trying to update someone else's resource. // or someone is trying to update someone else's resource.
@ -138,301 +111,134 @@ func (h *Handler) Validate(chunkAddr storage.Address, data []byte) bool {
} }
// GetContent retrieves the data payload of the last synced update of the Mutable Resource // 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) { func (h *Handler) GetContent(view *View) (storage.Address, []byte, error) {
rsrc := h.get(rootAddr) if view == nil {
if rsrc == nil || !rsrc.isSynced() { return nil, nil, NewError(ErrInvalidValue, "view is nil")
return nil, nil, NewError(ErrNotFound, " does not exist or is not synced") }
rsrc := h.get(view)
if rsrc == nil {
return nil, nil, NewError(ErrNotFound, "resource does not exist")
} }
return rsrc.lastKey, rsrc.data, nil return rsrc.lastKey, rsrc.data, nil
} }
// GetLastPeriod retrieves the period of the last synced update of the Mutable Resource // NewRequest prepares a Request structure with all the necessary information to
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
}
// 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.Address()) {
return NewError(ErrInvalidValue, "metaHash in UpdateRequest does not match actual metadata")
}
request.metaHash = metaHash
request.rootAddr = chunk.Address()
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.Address(),
},
},
},
ResourceMetadata: request.metadata,
updated: time.Now(),
}
h.set(chunk.Address(), rsrc)
return nil
}
// NewUpdateRequest prepares an UpdateRequest structure with all the necessary information to
// just add the desired data and sign it. // 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 // 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) { func (h *Handler) NewRequest(ctx context.Context, view *View) (request *Request, err error) {
if view == nil {
if rootAddr == nil { return nil, NewError(ErrInvalidValue, "view cannot be nil")
return nil, NewError(ErrInvalidValue, "rootAddr cannot be nil")
} }
// Make sure we have a cache of the metadata chunk now := TimestampProvider.Now().Time
rsrc, err := h.Load(ctx, rootAddr) request = new(Request)
request.Header.Version = ProtocolVersion
query := NewQueryLatest(view, lookup.NoClue)
rsrc, err := h.Lookup(ctx, query)
if err != nil { 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 { if err.(*Error).code != ErrNotFound {
return nil, err return nil, err
} }
// not finding updates means that there is a network error // not finding updates means that there is a network error
// or that the resource really does not have updates in this period. // or that the resource really does not have updates
} }
updateRequest.multihash = rsrc.multihash request.View = *view
updateRequest.rootAddr = rsrc.rootAddr
updateRequest.metaHash = rsrc.metaHash
updateRequest.metadata = rsrc.ResourceMetadata
// if we already have an update for this period then increment version // if we already have an update, then find next epoch
// resource object MUST be in sync for version to be correct, but we checked this earlier in the method already if rsrc != nil {
if h.hasUpdate(rootAddr, updateRequest.period) { request.Epoch = lookup.GetNextEpoch(rsrc.Epoch, now)
updateRequest.version = rsrc.version + 1
} else { } else {
updateRequest.version = 1 request.Epoch = lookup.GetFirstEpoch(now)
} }
return updateRequest, nil return request, nil
} }
// Lookup retrieves a specific or latest version of the resource update with metadata chunk at params.Root // Lookup retrieves a specific or latest version of the resource
// Lookup works differently depending on the configuration of `LookupParams` // Lookup works differently depending on the configuration of `ID`
// See the `LookupParams` documentation and helper functions: // See the `ID` documentation and helper functions:
// `LookupLatest`, `LookupLatestVersionInPeriod` and `LookupVersion` // `LookupLatest` and `LookupBefore`
// When looking for the latest update, it starts at the next period after the current time. // 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 // 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). // (or startTime is reached, in which case there are no updates).
func (h *Handler) Lookup(ctx context.Context, params *LookupParams) (*resource, error) { func (h *Handler) Lookup(ctx context.Context, query *Query) (*cacheEntry, error) {
rsrc := h.get(params.rootAddr) timeLimit := query.TimeLimit
if rsrc == nil { if timeLimit == 0 { // if time limit is set to zero, the user wants to get the latest update
return nil, NewError(ErrNothingToReturn, "resource not loaded") timeLimit = TimestampProvider.Now().Time
} }
return h.lookup(rsrc, params)
}
// LookupPrevious returns the resource before the one currently loaded in the resource cache if query.Hint == lookup.NoClue { // try to use our cache
// This is useful where resource updates are used incrementally in contrast to entry := h.get(&query.View)
// merely replacing content. if entry != nil && entry.Epoch.Time <= timeLimit { // avoid bad hints
// Requires a cached resource object to determine the current state of the resource. query.Hint = entry.Epoch
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 // we can't look for anything without a store
if h.chunkStore == nil { if h.chunkStore == nil {
return nil, NewError(ErrInit, "Call Handler.SetStore() before performing lookups") return nil, NewError(ErrInit, "Call Handler.SetStore() before performing lookups")
} }
var specificperiod bool var ul ID
if lp.period > 0 { ul.View = query.View
specificperiod = true var readCount int
} else {
// get the current time and the next period
now := TimestampProvider.Now()
var period uint32 // Invoke the lookup engine.
period, err := getNextPeriod(rsrc.StartTime.Time, now.Time, rsrc.Frequency) // The callback will be called every time the lookup algorithm needs to guess
if err != nil { requestPtr, err := lookup.Lookup(timeLimit, query.Hint, func(epoch lookup.Epoch, now uint64) (interface{}, error) {
return nil, err readCount++
} ul.Epoch = epoch
lp.period = period ctx, cancel := context.WithTimeout(ctx, defaultRetrieveTimeout)
}
// 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()
ctx, cancel := context.WithTimeout(context.Background(), defaultRetrieveTimeout)
defer cancel() defer cancel()
chunk, err := h.chunkStore.Get(ctx, updateAddr) chunk, err := h.chunkStore.Get(ctx, ul.Addr())
if err == nil { if err != nil { // TODO: check for catastrophic errors other than chunk not found
if specificversion { return nil, nil
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()
ctx, cancel := context.WithTimeout(context.Background(), defaultRetrieveTimeout)
defer cancel()
newchunk, err := h.chunkStore.Get(ctx, updateAddr)
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 var request Request
// Upon retrieval it creates/updates the index entry for it with metadata corresponding to the chunk contents if err := request.fromChunk(chunk.Address(), chunk.Data()); err != nil {
func (h *Handler) Load(ctx context.Context, rootAddr storage.Address) (*resource, error) { return nil, nil
//TODO: Maybe add timeout to context, defaultRetrieveTimeout? }
ctx, cancel := context.WithTimeout(ctx, defaultRetrieveTimeout) if request.Time <= timeLimit {
defer cancel() return &request, nil
chunk, err := h.chunkStore.Get(ctx, rootAddr) }
return nil, nil
})
if err != nil { if err != nil {
return nil, NewError(ErrNotFound, err.Error())
}
// create the index entry
rsrc := &resource{}
if err := rsrc.ResourceMetadata.binaryGet(chunk.Data()); err != nil { // Will fail if this is not really a metadata chunk
return nil, err return nil, err
} }
rsrc.rootAddr, rsrc.metaHash = metadataHash(chunk.Data()) log.Info(fmt.Sprintf("Resource lookup finished in %d lookups", readCount))
if !bytes.Equal(rsrc.rootAddr, rootAddr) {
return nil, NewError(ErrCorruptData, "Corrupt metadata chunk") request, _ := requestPtr.(*Request)
if request == nil {
return nil, NewError(ErrNotFound, "no updates found")
} }
h.set(rootAddr, rsrc) return h.updateCache(request)
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 // update mutable resource cache map with specified content
func (h *Handler) updateIndex(rsrc *resource, chunk storage.Chunk) (*resource, error) { func (h *Handler) updateCache(request *Request) (*cacheEntry, error) {
// retrieve metadata from chunk data and check that it matches this mutable resource updateAddr := request.Addr()
var r SignedResourceUpdate log.Trace("resource cache update", "topic", request.Topic.Hex(), "updatekey", updateAddr, "epoch time", request.Epoch.Time, "epoch level", request.Epoch.Level)
if err := r.fromChunk(chunk.Address(), chunk.Data()); err != nil {
return nil, err rsrc := h.get(&request.View)
if rsrc == nil {
rsrc = &cacheEntry{}
h.set(&request.View, rsrc)
} }
log.Trace("resource index update", "name", rsrc.ResourceMetadata.Name, "updatekey", chunk.Address(), "period", r.period, "version", r.version)
// update our rsrcs entry map // update our rsrcs entry map
rsrc.lastKey = chunk.Address() rsrc.lastKey = updateAddr
rsrc.period = r.period rsrc.ResourceUpdate = request.ResourceUpdate
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) rsrc.Reader = bytes.NewReader(rsrc.data)
log.Debug("resource synced", "name", rsrc.ResourceMetadata.Name, "updateAddr", chunk.Address(), "period", rsrc.period, "version", rsrc.version)
h.set(chunk.Address(), rsrc)
return rsrc, nil return rsrc, nil
} }
@ -442,23 +248,16 @@ func (h *Handler) updateIndex(rsrc *resource, chunk storage.Chunk) (*resource, e
// 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. // 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 // 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. // on the network.
func (h *Handler) Update(ctx context.Context, r *SignedResourceUpdate) (storage.Address, error) { func (h *Handler) Update(ctx context.Context, r *Request) (updateAddr storage.Address, err 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 // we can't update anything without a store
if h.chunkStore == nil { if h.chunkStore == nil {
return nil, NewError(ErrInit, "Call Handler.SetStore() before updating") return nil, NewError(ErrInit, "Call Handler.SetStore() before updating")
} }
rsrc := h.get(r.rootAddr) rsrc := h.get(&r.View)
if rsrc != nil && rsrc.period != 0 && rsrc.version != 0 && // This is the only cheap check we can do for sure if rsrc != nil && rsrc.Epoch.Equals(r.Epoch) { // 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 epoch is already known to exist")
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 chunk, err := r.toChunk() // Serialize the update into a chunk. Fails if data is too big
@ -468,49 +267,32 @@ func (h *Handler) update(ctx context.Context, r *SignedResourceUpdate) (updateAd
// send the chunk // send the chunk
h.chunkStore.Put(ctx, chunk) h.chunkStore.Put(ctx, chunk)
log.Trace("resource update", "updateAddr", r.updateAddr, "lastperiod", r.period, "version", r.version, "data", chunk.Data(), "multihash", r.multihash) log.Trace("resource update", "updateAddr", r.idAddr, "epoch time", r.Epoch.Time, "epoch level", r.Epoch.Level, "data", chunk.Data())
// update our resources map cache entry if the new update is older than the one we have, if we have it.
// update our resources map entry if the new update is older than the one we have, if we have it. if rsrc != nil && r.Epoch.After(rsrc.Epoch) {
if rsrc != nil && (r.period > rsrc.period || (rsrc.period == r.period && r.version > rsrc.version)) { rsrc.Epoch = r.Epoch
rsrc.period = r.period
rsrc.version = r.version
rsrc.data = make([]byte, len(r.data)) rsrc.data = make([]byte, len(r.data))
rsrc.updated = time.Now() rsrc.lastKey = r.idAddr
rsrc.lastKey = r.updateAddr
rsrc.multihash = r.multihash
copy(rsrc.data, r.data) copy(rsrc.data, r.data)
rsrc.Reader = bytes.NewReader(rsrc.data) rsrc.Reader = bytes.NewReader(rsrc.data)
} }
return r.updateAddr, nil
return r.idAddr, nil
} }
// Retrieves the resource index value for the given nameHash // Retrieves the resource cache value for the given nameHash
func (h *Handler) get(rootAddr storage.Address) *resource { func (h *Handler) get(view *View) *cacheEntry {
if len(rootAddr) < storage.AddressLength { mapKey := view.mapKey()
log.Warn("Handler.get with invalid rootAddr")
return nil
}
hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0]))
h.resourceLock.RLock() h.resourceLock.RLock()
defer h.resourceLock.RUnlock() defer h.resourceLock.RUnlock()
rsrc := h.resources[hashKey] rsrc := h.resources[mapKey]
return rsrc return rsrc
} }
// Sets the resource index value for the given nameHash // Sets the resource cache value for the given View
func (h *Handler) set(rootAddr storage.Address, rsrc *resource) { func (h *Handler) set(view *View, rsrc *cacheEntry) {
if len(rootAddr) < storage.AddressLength { mapKey := view.mapKey()
log.Warn("Handler.set with invalid rootAddr")
return
}
hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0]))
h.resourceLock.Lock() h.resourceLock.Lock()
defer h.resourceLock.Unlock() defer h.resourceLock.Unlock()
h.resources[hashKey] = rsrc h.resources[mapKey] = 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
} }

View file

@ -0,0 +1,520 @@
// 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"
"context"
"flag"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/swarm/chunk"
"github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
)
var (
loglevel = flag.Int("loglevel", 3, "loglevel")
startTime = Timestamp{
Time: uint64(4200),
}
cleanF func()
resourceName = "føø.bar"
hashfunc = storage.MakeHashFunc(storage.DefaultHash)
)
func init() {
flag.Parse()
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
}
// simulated timeProvider
type fakeTimeProvider struct {
currentTime uint64
}
func (f *fakeTimeProvider) Tick() {
f.currentTime++
}
func (f *fakeTimeProvider) Set(time uint64) {
f.currentTime = time
}
func (f *fakeTimeProvider) FastForward(offset uint64) {
f.currentTime += offset
}
func (f *fakeTimeProvider) Now() Timestamp {
return Timestamp{
Time: f.currentTime,
}
}
// make updates and retrieve them based on periods and versions
func TestResourceHandler(t *testing.T) {
// make fake timeProvider
clock := &fakeTimeProvider{
currentTime: startTime.Time, // clock starts at t=4200
}
// signer containing private key
signer := newAliceSigner()
rh, datadir, teardownTest, err := setupTest(clock, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
// create a new resource
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
topic, _ := NewTopic("Mess with mru code and see what ghost catches you", nil)
view := View{
Topic: topic,
User: signer.Address(),
}
// data for updates:
updates := []string{
"blinky", // t=4200
"pinky", // t=4242
"inky", // t=4284
"clyde", // t=4285
}
request := NewFirstRequest(view.Topic) // this timestamps the update at t = 4200 (start time)
resourcekey := make(map[string]storage.Address)
data := []byte(updates[0])
request.SetData(data)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[0]], err = rh.Update(ctx, request)
if err != nil {
t.Fatal(err)
}
// move the clock ahead 21 seconds
clock.FastForward(21) // t=4221
request, err = rh.NewRequest(ctx, &request.View) // this timestamps the update at t = 4221
if err != nil {
t.Fatal(err)
}
if request.Epoch.Base() != 0 || request.Epoch.Level != lookup.HighestLevel-1 {
t.Fatalf("Suggested epoch BaseTime should be 0 and Epoch level should be %d", lookup.HighestLevel-1)
}
request.Epoch.Level = lookup.HighestLevel // force level 25 instead of 24 to make it fail
data = []byte(updates[1])
request.SetData(data)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[1]], err = rh.Update(ctx, request)
if err == nil {
t.Fatal("Expected update to fail since an update in this epoch already exists")
}
// move the clock ahead 21 seconds
clock.FastForward(21) // t=4242
request, err = rh.NewRequest(ctx, &request.View)
if err != nil {
t.Fatal(err)
}
request.SetData(data)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[1]], err = rh.Update(ctx, request)
if err != nil {
t.Fatal(err)
}
// move the clock ahead 42 seconds
clock.FastForward(42) // t=4284
request, err = rh.NewRequest(ctx, &request.View)
if err != nil {
t.Fatal(err)
}
data = []byte(updates[2])
request.SetData(data)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[2]], err = rh.Update(ctx, request)
if err != nil {
t.Fatal(err)
}
// move the clock ahead 1 second
clock.FastForward(1) // t=4285
request, err = rh.NewRequest(ctx, &request.View)
if err != nil {
t.Fatal(err)
}
if request.Epoch.Base() != 0 || request.Epoch.Level != 22 {
t.Fatalf("Expected epoch base time to be %d, got %d. Expected epoch level to be %d, got %d", 0, request.Epoch.Base(), 22, request.Epoch.Level)
}
data = []byte(updates[3])
request.SetData(data)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[3]], err = rh.Update(ctx, request)
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
rh.Close()
// check we can retrieve the updates after close
clock.FastForward(2000) // t=6285
rhparams := &HandlerParams{}
rh2, err := NewTestHandler(datadir, rhparams)
if err != nil {
t.Fatal(err)
}
rsrc2, err := rh2.Lookup(ctx, NewQueryLatest(&request.View, lookup.NoClue))
if err != nil {
t.Fatal(err)
}
// last update should be "clyde"
if !bytes.Equal(rsrc2.data, []byte(updates[len(updates)-1])) {
t.Fatalf("resource data was %v, expected %v", string(rsrc2.data), updates[len(updates)-1])
}
if rsrc2.Level != 22 {
t.Fatalf("resource epoch level was %d, expected 22", rsrc2.Level)
}
if rsrc2.Base() != 0 {
t.Fatalf("resource epoch base time was %d, expected 0", rsrc2.Base())
}
log.Debug("Latest lookup", "epoch base time", rsrc2.Base(), "epoch level", rsrc2.Level, "data", rsrc2.data)
// specific point in time
rsrc, err := rh2.Lookup(ctx, NewQuery(&request.View, 4284, lookup.NoClue))
if err != nil {
t.Fatal(err)
}
// check data
if !bytes.Equal(rsrc.data, []byte(updates[2])) {
t.Fatalf("resource data (historical) was %v, expected %v", string(rsrc2.data), updates[2])
}
log.Debug("Historical lookup", "epoch base time", rsrc2.Base(), "epoch level", rsrc2.Level, "data", rsrc2.data)
// beyond the first should yield an error
rsrc, err = rh2.Lookup(ctx, NewQuery(&request.View, startTime.Time-1, lookup.NoClue))
if err == nil {
t.Fatalf("expected previous to fail, returned epoch %s data %v", rsrc.Epoch.String(), rsrc.data)
}
}
const Day = 60 * 60 * 24
const Year = Day * 365
const Month = Day * 30
func generateData(x uint64) []byte {
return []byte(fmt.Sprintf("%d", x))
}
func TestSparseUpdates(t *testing.T) {
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
rh, datadir, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
defer os.RemoveAll(datadir)
// create a new resource
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
topic, _ := NewTopic("Very slow updates", nil)
view := View{
Topic: topic,
User: signer.Address(),
}
// publish one update every 5 years since Unix 0 until today
today := uint64(1533799046)
var epoch lookup.Epoch
var lastUpdateTime uint64
for T := uint64(0); T < today; T += 5 * Year {
request := NewFirstRequest(view.Topic)
request.Epoch = lookup.GetNextEpoch(epoch, T)
request.data = generateData(T) // this generates some data that depends on T, so we can check later
request.Sign(signer)
if err != nil {
t.Fatal(err)
}
if _, err := rh.Update(ctx, request); err != nil {
t.Fatal(err)
}
epoch = request.Epoch
lastUpdateTime = T
}
query := NewQuery(&view, today, lookup.NoClue)
_, err = rh.Lookup(ctx, query)
if err != nil {
t.Fatal(err)
}
_, content, err := rh.GetContent(&view)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(generateData(lastUpdateTime), content) {
t.Fatalf("Expected to recover last written value %d, got %s", lastUpdateTime, string(content))
}
// lookup the closest update to 35*Year + 6* Month (~ June 2005):
// it should find the update we put on 35*Year, since we were updating every 5 years.
query.TimeLimit = 35*Year + 6*Month
_, err = rh.Lookup(ctx, query)
if err != nil {
t.Fatal(err)
}
_, content, err = rh.GetContent(&view)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(generateData(35*Year), content) {
t.Fatalf("Expected to recover %d, got %s", 35*Year, string(content))
}
}
func TestValidator(t *testing.T) {
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key. Alice will be the good girl
signer := newAliceSigner()
// set up sim timeProvider
rh, _, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
// create new resource
topic, _ := NewTopic(resourceName, nil)
view := View{
Topic: topic,
User: signer.Address(),
}
mr := NewFirstRequest(view.Topic)
// chunk with address
data := []byte("foo")
mr.SetData(data)
if err := mr.Sign(signer); err != nil {
t.Fatalf("sign fail: %v", err)
}
chunk, err := mr.toChunk()
if err != nil {
t.Fatal(err)
}
if !rh.Validate(chunk.Address(), chunk.Data()) {
t.Fatal("Chunk validator fail on update chunk")
}
address := chunk.Address()
// mess with the address
address[0] = 11
address[15] = 99
if rh.Validate(address, chunk.Data()) {
t.Fatal("Expected Validate to fail with false chunk address")
}
}
// tests that the content address validator correctly checks the data
// tests that resource update chunks are passed through content address validator
// there is some redundancy in this test as it also tests content addressed chunks,
// which should be evaluated as invalid chunks by this validator
func TestValidatorInStore(t *testing.T) {
// make fake timeProvider
TimestampProvider = &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
// set up localstore
datadir, err := ioutil.TempDir("", "storage-testresourcevalidator")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(datadir)
handlerParams := storage.NewDefaultLocalStoreParams()
handlerParams.Init(datadir)
store, err := storage.NewLocalStore(handlerParams, nil)
if err != nil {
t.Fatal(err)
}
// set up resource handler and add is as a validator to the localstore
rhParams := &HandlerParams{}
rh := NewHandler(rhParams)
store.Validators = append(store.Validators, rh)
// create content addressed chunks, one good, one faulty
chunks := storage.GenerateRandomChunks(chunk.DefaultSize, 2)
goodChunk := chunks[0]
badChunk := storage.NewChunk(chunks[1].Address(), goodChunk.Data())
topic, _ := NewTopic("xyzzy", nil)
view := View{
Topic: topic,
User: signer.Address(),
}
// create a resource update chunk with correct publickey
id := ID{
Epoch: lookup.Epoch{Time: 42,
Level: 1,
},
View: view,
}
updateAddr := id.Addr()
data := []byte("bar")
r := new(Request)
r.idAddr = updateAddr
r.ResourceUpdate.ID = id
r.data = data
r.Sign(signer)
uglyChunk, err := r.toChunk()
if err != nil {
t.Fatal(err)
}
// put the chunks in the store and check their error status
err = store.Put(context.Background(), goodChunk)
if err == nil {
t.Fatal("expected error on good content address chunk with resource validator only, but got nil")
}
err = store.Put(context.Background(), badChunk)
if err == nil {
t.Fatal("expected error on bad content address chunk with resource validator only, but got nil")
}
err = store.Put(context.Background(), uglyChunk)
if err != nil {
t.Fatalf("expected no error on resource update chunk with resource validator only, but got: %s", err)
}
}
// create rpc and resourcehandler
func setupTest(timeProvider timestampProvider, signer Signer) (rh *TestHandler, datadir string, teardown func(), err error) {
var fsClean func()
var rpcClean func()
cleanF = func() {
if fsClean != nil {
fsClean()
}
if rpcClean != nil {
rpcClean()
}
}
// temp datadir
datadir, err = ioutil.TempDir("", "rh")
if err != nil {
return nil, "", nil, err
}
fsClean = func() {
os.RemoveAll(datadir)
}
TimestampProvider = timeProvider
rhparams := &HandlerParams{}
rh, err = NewTestHandler(datadir, rhparams)
return rh, datadir, cleanF, err
}
func newAliceSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
return NewGenericSigner(privKey)
}
func newBobSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("accedeaccedeaccedeaccedeaccedeaccedeaccedeaccedeaccedeaccedecaca")
return NewGenericSigner(privKey)
}
func newCharlieSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca")
return NewGenericSigner(privKey)
}
func getUpdateDirect(rh *Handler, addr storage.Address) ([]byte, error) {
chunk, err := rh.chunkStore.Get(context.TODO(), addr)
if err != nil {
return nil, err
}
var r Request
if err := r.fromChunk(addr, chunk.Data()); err != nil {
return nil, err
}
return r.data, nil
}

123
swarm/storage/mru/id.go Normal file
View file

@ -0,0 +1,123 @@
// 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"
"hash"
"strconv"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
"github.com/ethereum/go-ethereum/swarm/storage"
)
// ID uniquely identifies an update on the network.
type ID struct {
View `json:"view"`
lookup.Epoch `json:"epoch"`
}
// ID layout:
// View viewLength bytes
// Epoch EpochLength
const idLength = viewLength + lookup.EpochLength
// Addr calculates the resource update chunk address corresponding to this ID
func (u *ID) Addr() (updateAddr storage.Address) {
serializedData := make([]byte, idLength)
var cursor int
u.View.binaryPut(serializedData[cursor : cursor+viewLength])
cursor += viewLength
eid := u.Epoch.ID()
copy(serializedData[cursor:cursor+lookup.EpochLength], eid[:])
hasher := hashPool.Get().(hash.Hash)
defer hashPool.Put(hasher)
hasher.Reset()
hasher.Write(serializedData)
return hasher.Sum(nil)
}
// binaryPut serializes this instance into the provided slice
func (u *ID) binaryPut(serializedData []byte) error {
if len(serializedData) != idLength {
return NewErrorf(ErrInvalidValue, "Incorrect slice size to serialize ID. Expected %d, got %d", idLength, len(serializedData))
}
var cursor int
if err := u.View.binaryPut(serializedData[cursor : cursor+viewLength]); err != nil {
return err
}
cursor += viewLength
epochBytes, err := u.Epoch.MarshalBinary()
if err != nil {
return err
}
copy(serializedData[cursor:cursor+lookup.EpochLength], epochBytes[:])
cursor += lookup.EpochLength
return nil
}
// binaryLength returns the expected size of this structure when serialized
func (u *ID) binaryLength() int {
return idLength
}
// binaryGet restores the current instance from the information contained in the passed slice
func (u *ID) binaryGet(serializedData []byte) error {
if len(serializedData) != idLength {
return NewErrorf(ErrInvalidValue, "Incorrect slice size to read ID. Expected %d, got %d", idLength, len(serializedData))
}
var cursor int
if err := u.View.binaryGet(serializedData[cursor : cursor+viewLength]); err != nil {
return err
}
cursor += viewLength
if err := u.Epoch.UnmarshalBinary(serializedData[cursor : cursor+lookup.EpochLength]); err != nil {
return err
}
cursor += lookup.EpochLength
return nil
}
// FromValues deserializes this instance from a string key-value store
// useful to parse query strings
func (u *ID) FromValues(values Values) error {
level, _ := strconv.ParseUint(values.Get("level"), 10, 32)
u.Epoch.Level = uint8(level)
u.Epoch.Time, _ = strconv.ParseUint(values.Get("time"), 10, 64)
if u.View.User == (common.Address{}) {
return u.View.FromValues(values)
}
return nil
}
// AppendValues serializes this structure into the provided string key-value store
// useful to build query strings
func (u *ID) AppendValues(values Values) {
values.Set("level", fmt.Sprintf("%d", u.Epoch.Level))
values.Set("time", fmt.Sprintf("%d", u.Epoch.Time))
u.View.AppendValues(values)
}

View file

@ -0,0 +1,28 @@
package mru
import (
"testing"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
)
func getTestID() *ID {
return &ID{
View: *getTestView(),
Epoch: lookup.GetFirstEpoch(1000),
}
}
func TestIDAddr(t *testing.T) {
ul := getTestID()
updateAddr := ul.Addr()
compareByteSliceToExpectedHex(t, "updateAddr", updateAddr, "0x8b24583ec293e085f4c78aaee66d1bc5abfb8b4233304d14a349afa57af2a783")
}
func TestIDSerializer(t *testing.T) {
testBinarySerializerRecovery(t, getTestID(), "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce803000000000019")
}
func TestIDLengthCheck(t *testing.T) {
testBinarySerializerLengthCheck(t, getTestID())
}

View file

@ -1,117 +0,0 @@
// 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.AddressLength
// 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.AddressLength {
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.AddressLength))
copy(u.rootAddr[:], serializedData[8:])
return nil
}

View file

@ -0,0 +1,91 @@
// 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 lookup
import (
"encoding/binary"
"errors"
"fmt"
)
// Epoch represents a time slot at a particular frequency level
type Epoch struct {
Time uint64 `json:"time"` // Time stores the time at which the update or lookup takes place
Level uint8 `json:"level"` // Level indicates the frequency level as the exponent of a power of 2
}
// EpochID is a unique identifier for an Epoch, based on its level and base time.
type EpochID [8]byte
// EpochLength stores the serialized binary length of an Epoch
const EpochLength = 8
// MaxTime contains the highest possible time value an Epoch can handle
const MaxTime uint64 = (1 << 56) - 1
// Base returns the base time of the Epoch
func (e *Epoch) Base() uint64 {
return getBaseTime(e.Time, e.Level)
}
// ID Returns the unique identifier of this epoch
func (e *Epoch) ID() EpochID {
base := e.Base()
var id EpochID
binary.LittleEndian.PutUint64(id[:], base)
id[7] = e.Level
return id
}
// MarshalBinary implements the encoding.BinaryMarshaller interface
func (e *Epoch) MarshalBinary() (data []byte, err error) {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b[:], e.Time)
b[7] = e.Level
return b, nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaller interface
func (e *Epoch) UnmarshalBinary(data []byte) error {
if len(data) != EpochLength {
return errors.New("Invalid data unmarshalling Epoch")
}
b := make([]byte, 8)
copy(b, data)
e.Level = b[7]
b[7] = 0
e.Time = binary.LittleEndian.Uint64(b)
return nil
}
// After returns true if this epoch occurs later or exactly at the other epoch.
func (e *Epoch) After(epoch Epoch) bool {
if e.Time == epoch.Time {
return e.Level < epoch.Level
}
return e.Time >= epoch.Time
}
// Equals compares two epochs and returns true if they refer to the same time period.
func (e *Epoch) Equals(epoch Epoch) bool {
return e.Level == epoch.Level && e.Base() == epoch.Base()
}
// String implements the Stringer interface.
func (e *Epoch) String() string {
return fmt.Sprintf("Epoch{Time:%d, Level:%d}", e.Time, e.Level)
}

View file

@ -0,0 +1,57 @@
package lookup_test
import (
"testing"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
)
func TestMarshallers(t *testing.T) {
for i := uint64(1); i < lookup.MaxTime; i *= 3 {
e := lookup.Epoch{
Time: i,
Level: uint8(i % 20),
}
b, err := e.MarshalBinary()
if err != nil {
t.Fatal(err)
}
var e2 lookup.Epoch
if err := e2.UnmarshalBinary(b); err != nil {
t.Fatal(err)
}
if e != e2 {
t.Fatal("Expected unmarshalled epoch to be equal to marshalled onet.Fatal(err)")
}
}
}
func TestAfter(t *testing.T) {
a := lookup.Epoch{
Time: 5,
Level: 3,
}
b := lookup.Epoch{
Time: 6,
Level: 3,
}
c := lookup.Epoch{
Time: 6,
Level: 4,
}
if !b.After(a) {
t.Fatal("Expected 'after' to be true, got false")
}
if b.After(b) {
t.Fatal("Expected 'after' to be false when both epochs are identical, got true")
}
if !b.After(c) {
t.Fatal("Expected 'after' to be true when both epochs have the same time but the level is lower in the first one, but got false")
}
}

View file

@ -0,0 +1,180 @@
// 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 lookup defines resource lookup algorithms and provides tools to place updates
so they can be found
*/
package lookup
const maxuint64 = ^uint64(0)
// LowestLevel establishes the frequency resolution of the lookup algorithm as a power of 2.
const LowestLevel uint8 = 0 // default is 0 (1 second)
// HighestLevel sets the lowest frequency the algorithm will operate at, as a power of 2.
// 25 -> 2^25 equals to roughly one year.
const HighestLevel = 25 // default is 25 (~1 year)
// DefaultLevel sets what level will be chosen to search when there is no hint
const DefaultLevel = HighestLevel
//Algorithm is the function signature of a lookup algorithm
type Algorithm func(now uint64, hint Epoch, read ReadFunc) (value interface{}, err error)
// Lookup finds the update with the highest timestamp that is smaller or equal than 'now'
// It takes a hint which should be the epoch where the last known update was
// If you don't know in what epoch the last update happened, simply submit lookup.NoClue
// read() will be called on each lookup attempt
// Returns an error only if read() returns an error
// Returns nil if an update was not found
var Lookup Algorithm = FluzCapacitorAlgorithm
// ReadFunc is a handler called by Lookup each time it attempts to find a value
// It should return <nil> if a value is not found
// It should return <nil> if a value is found, but its timestamp is higher than "now"
// It should only return an error in case the handler wants to stop the
// lookup process entirely.
type ReadFunc func(epoch Epoch, now uint64) (interface{}, error)
// NoClue is a hint that can be provided when the Lookup caller does not have
// a clue about where the last update may be
var NoClue = Epoch{}
// getBaseTime returns the epoch base time of the given
// time and level
func getBaseTime(t uint64, level uint8) uint64 {
return t & (maxuint64 << level)
}
// Hint creates a hint based only on the last known update time
func Hint(last uint64) Epoch {
return Epoch{
Time: last,
Level: DefaultLevel,
}
}
// GetNextLevel returns the frequency level a next update should be placed at, provided where
// the last update was and what time it is now.
// This is the first nonzero bit of the XOR of 'last' and 'now', counting from the highest significant bit
// but limited to not return a level that is smaller than the last-1
func GetNextLevel(last Epoch, now uint64) uint8 {
// First XOR the last epoch base time with the current clock.
// This will set all the common most significant bits to zero.
mix := (last.Base() ^ now)
// Then, make sure we stop the below loop before one level below the current, by setting
// that level's bit to 1.
// If the next level is lower than the current one, it must be exactly level-1 and not lower.
mix |= (1 << (last.Level - 1))
// if the last update was more than 2^highestLevel seconds ago, choose the highest level
if mix > (maxuint64 >> (64 - HighestLevel - 1)) {
return HighestLevel
}
// set up a mask to scan for nonzero bits, starting at the highest level
mask := uint64(1 << (HighestLevel))
for i := uint8(HighestLevel); i > LowestLevel; i-- {
if mix&mask != 0 { // if we find a nonzero bit, this is the level the next update should be at.
return i
}
mask = mask >> 1 // move our bit one position to the right
}
return 0
}
// GetNextEpoch returns the epoch where the next update should be located
// according to where the previous update was
// and what time it is now.
func GetNextEpoch(last Epoch, now uint64) Epoch {
if last == NoClue {
return GetFirstEpoch(now)
}
level := GetNextLevel(last, now)
return Epoch{
Level: level,
Time: now,
}
}
// GetFirstEpoch returns the epoch where the first update should be located
// based on what time it is now.
func GetFirstEpoch(now uint64) Epoch {
return Epoch{Level: HighestLevel, Time: now}
}
var worstHint = Epoch{Time: 0, Level: 63}
// FluzCapacitorAlgorithm works by narrowing the epoch search area if an update is found
// going back and forth in time
// First, it will attempt to find an update where it should be now if the hint was
// really the last update. If that lookup fails, then the last update must be either the hint itself
// or the epochs right below. If however, that lookup succeeds, then the update must be
// that one or within the epochs right below.
// see the guide for a more graphical representation
func FluzCapacitorAlgorithm(now uint64, hint Epoch, read ReadFunc) (value interface{}, err error) {
var lastFound interface{}
var epoch Epoch
if hint == NoClue {
hint = worstHint
}
t := now
for {
epoch = GetNextEpoch(hint, t)
value, err = read(epoch, now)
if err != nil {
return nil, err
}
if value != nil {
lastFound = value
if epoch.Level == LowestLevel || epoch.Equals(hint) {
return value, nil
}
hint = epoch
continue
}
if epoch.Base() == hint.Base() {
if lastFound != nil {
return lastFound, nil
}
// we have reached the hint itself
if hint == worstHint {
return nil, nil
}
// check it out
value, err = read(hint, now)
if err != nil {
return nil, err
}
if value != nil {
return value, nil
}
// bad hint.
epoch = hint
hint = worstHint
}
base := epoch.Base()
if base == 0 {
return nil, nil
}
t = base - 1
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,85 +0,0 @@
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")
}
}

View file

@ -1,187 +0,0 @@
// 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, 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
}

View file

@ -1,126 +0,0 @@
// 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")
}
}

View file

@ -0,0 +1,78 @@
// 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"
"strconv"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
)
// Query is used to specify constraints when performing an update lookup
// TimeLimit indicates an upper bound for the search. Set to 0 for "now"
type Query struct {
View
Hint lookup.Epoch
TimeLimit uint64
}
// FromValues deserializes this instance from a string key-value store
// useful to parse query strings
func (q *Query) FromValues(values Values) error {
time, _ := strconv.ParseUint(values.Get("time"), 10, 64)
q.TimeLimit = time
level, _ := strconv.ParseUint(values.Get("hint.level"), 10, 32)
q.Hint.Level = uint8(level)
q.Hint.Time, _ = strconv.ParseUint(values.Get("hint.time"), 10, 64)
if q.View.User == (common.Address{}) {
return q.View.FromValues(values)
}
return nil
}
// AppendValues serializes this structure into the provided string key-value store
// useful to build query strings
func (q *Query) AppendValues(values Values) {
if q.TimeLimit != 0 {
values.Set("time", fmt.Sprintf("%d", q.TimeLimit))
}
if q.Hint.Level != 0 {
values.Set("hint.level", fmt.Sprintf("%d", q.Hint.Level))
}
if q.Hint.Time != 0 {
values.Set("hint.time", fmt.Sprintf("%d", q.Hint.Time))
}
q.View.AppendValues(values)
}
// NewQuery constructs an Query structure to find updates on or before `time`
// if time == 0, the latest update will be looked up
func NewQuery(view *View, time uint64, hint lookup.Epoch) *Query {
return &Query{
TimeLimit: time,
View: *view,
Hint: hint,
}
}
// NewQueryLatest generates lookup parameters that look for the latest version of a resource
func NewQueryLatest(view *View, hint lookup.Epoch) *Query {
return NewQuery(view, 0, hint)
}

View file

@ -0,0 +1,38 @@
// 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"
)
func getTestQuery() *Query {
ul := getTestID()
return &Query{
TimeLimit: 5000,
View: ul.View,
Hint: ul.Epoch,
}
}
func TestQueryValues(t *testing.T) {
var expected = KV{"hint.level": "25", "hint.time": "1000", "time": "5000", "topic": "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000", "user": "0x876A8936A7Cd0b79Ef0735AD0896c1AFe278781c"}
query := getTestQuery()
testValueSerializer(t, query, expected)
}

View file

@ -19,157 +19,218 @@ package mru
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"hash"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
) )
// 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 // Request represents an update and/or resource create message
type Request struct { type Request struct {
SignedResourceUpdate ResourceUpdate // actual content that will be put on the chunk, less signature
metadata ResourceMetadata Signature *Signature
isNew bool idAddr storage.Address // cached chunk address for the update (not serialized, for internal use)
binaryData []byte // cached serialized data (does not get serialized again!, for efficiency/internal use)
} }
var zeroAddr = common.Address{} // updateRequestJSON represents a JSON-serialized UpdateRequest
type updateRequestJSON struct {
ID
ProtocolVersion uint8 `json:"protocolVersion"`
Data string `json:"data,omitempty"`
Signature string `json:"signature,omitempty"`
}
// NewCreateUpdateRequest returns a ready to sign request to create and initialize a resource with data // Request layout
func NewCreateUpdateRequest(metadata *ResourceMetadata) (*Request, error) { // resourceUpdate bytes
// SignatureLength bytes
const minimumSignedUpdateLength = minimumUpdateDataLength + signatureLength
request, err := NewCreateRequest(metadata) // NewFirstRequest returns a ready to sign request to publish a first update
if err != nil { func NewFirstRequest(topic Topic) *Request {
return nil, err
} request := new(Request)
// get the current time // get the current time
now := TimestampProvider.Now().Time now := TimestampProvider.Now().Time
request.Epoch = lookup.GetFirstEpoch(now)
request.View.Topic = topic
request.Header.Version = ProtocolVersion
request.version = 1 return request
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 // SetData stores the payload data the resource will be updated with
func (r *Request) SetData(data []byte, multihash bool) { func (r *Request) SetData(data []byte) {
r.data = data r.data = data
r.multihash = multihash r.Signature = nil
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)
} }
// IsUpdate returns true if this request models a signed update or otherwise it is a signature request
func (r *Request) IsUpdate() bool { func (r *Request) IsUpdate() bool {
return r.signature != nil return r.Signature != nil
}
// Verify checks that signatures are valid and that the signer owns the resource to be updated
func (r *Request) 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)
r.View.User, err = getUserAddr(digest, *r.Signature)
if err != nil {
return err
}
// 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(r.idAddr, r.Addr()) {
return NewError(ErrInvalidSignature, "Signature address does not match with update user address")
}
return nil
}
// Sign executes the signature to validate the resource
func (r *Request) Sign(signer Signer) error {
r.View.User = signer.Address()
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
userAddr, err := getUserAddr(digest, signature)
if err != nil {
return NewError(ErrInvalidSignature, "Error verifying signature")
}
if userAddr != 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 update user address")
}
r.Signature = &signature
r.idAddr = r.Addr()
return nil
}
// GetDigest creates the resource update digest used in signatures
// the serialized payload is cached in .binaryData
func (r *Request) 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
}
// create an update chunk.
func (r *Request) 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, "toChunk called without a valid signature or payload data. Call .Sign() first.")
}
resourceUpdateLength := r.ResourceUpdate.binaryLength()
// signature is the last item in the chunk data
copy(r.binaryData[resourceUpdateLength:], r.Signature[:])
chunk := storage.NewChunk(r.idAddr, r.binaryData)
return chunk, nil
}
// fromChunk populates this structure from chunk data. It does not verify the signature is valid.
func (r *Request) fromChunk(updateAddr storage.Address, chunkdata []byte) error {
// for update chunk layout see Request definition
//deserialize the resource update portion
if err := r.ResourceUpdate.binaryGet(chunkdata[:len(chunkdata)-signatureLength]); 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.idAddr = updateAddr
r.binaryData = chunkdata
return nil
}
// FromValues deserializes this instance from a string key-value store
// useful to parse query strings
func (r *Request) FromValues(values Values, data []byte) error {
signatureBytes, err := hexutil.Decode(values.Get("signature"))
if err != nil {
r.Signature = nil
} else {
if len(signatureBytes) != signatureLength {
return NewError(ErrInvalidSignature, "Incorrect signature length")
}
r.Signature = new(Signature)
copy(r.Signature[:], signatureBytes)
}
err = r.ResourceUpdate.FromValues(values, data)
if err != nil {
return err
}
r.idAddr = r.Addr()
return err
}
// AppendValues serializes this structure into the provided string key-value store
// useful to build query strings
func (r *Request) AppendValues(values Values) []byte {
if r.Signature != nil {
values.Set("signature", hexutil.Encode(r.Signature[:]))
}
return r.ResourceUpdate.AppendValues(values)
} }
// fromJSON takes an update request JSON and populates an UpdateRequest // fromJSON takes an update request JSON and populates an UpdateRequest
func (r *Request) fromJSON(j *updateRequestJSON) error { func (r *Request) fromJSON(j *updateRequestJSON) error {
r.version = j.Version r.ID = j.ID
r.period = j.Period r.Header.Version = j.ProtocolVersion
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 var err error
if j.Data != "" { if j.Data != "" {
@ -179,73 +240,18 @@ func (r *Request) fromJSON(j *updateRequestJSON) error {
} }
} }
var declaredRootAddr storage.Address
var declaredMetaHash []byte
declaredRootAddr, err = decodeHexSlice(j.RootAddr, storage.AddressLength, "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 != "" { if j.Signature != "" {
sigBytes, err := hexutil.Decode(j.Signature) sigBytes, err := hexutil.Decode(j.Signature)
if err != nil || len(sigBytes) != signatureLength { if err != nil || len(sigBytes) != signatureLength {
return NewError(ErrInvalidSignature, "Cannot decode signature") return NewError(ErrInvalidSignature, "Cannot decode signature")
} }
r.signature = new(Signature) r.Signature = new(Signature)
r.updateAddr = r.UpdateAddr() r.idAddr = r.Addr()
copy(r.signature[:], sigBytes) copy(r.Signature[:], sigBytes)
} }
return nil 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 // UnmarshalJSON takes a JSON structure stored in a byte array and populates the Request object
// Implements json.Unmarshaler interface // Implements json.Unmarshaler interface
func (r *Request) UnmarshalJSON(rawData []byte) error { func (r *Request) UnmarshalJSON(rawData []byte) error {
@ -259,38 +265,19 @@ func (r *Request) UnmarshalJSON(rawData []byte) error {
// MarshalJSON takes an update request and encodes it as a JSON structure into a byte array // MarshalJSON takes an update request and encodes it as a JSON structure into a byte array
// Implements json.Marshaler interface // Implements json.Marshaler interface
func (r *Request) MarshalJSON() (rawData []byte, err error) { func (r *Request) MarshalJSON() (rawData []byte, err error) {
var signatureString, dataHashString, rootAddrString, metaHashString string var signatureString, dataString string
if r.signature != nil { if r.Signature != nil {
signatureString = hexutil.Encode(r.signature[:]) signatureString = hexutil.Encode(r.Signature[:])
} }
if r.data != nil { if r.data != nil {
dataHashString = hexutil.Encode(r.data) dataString = 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{ requestJSON := &updateRequestJSON{
Name: r.metadata.Name, ID: r.ID,
Frequency: r.metadata.Frequency, ProtocolVersion: r.Header.Version,
StartTime: r.metadata.StartTime.Time, Data: dataString,
Version: r.version, Signature: signatureString,
Period: r.period,
Owner: ownerAddrString,
Data: dataHashString,
Multihash: r.multihash,
Signature: signatureString,
RootAddr: rootAddrString,
MetaHash: metaHashString,
} }
return json.Marshal(requestJSON) return json.Marshal(requestJSON)

View file

@ -1,11 +1,32 @@
// 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 package mru
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/storage/mru/lookup"
) )
func areEqualJSON(s1, s2 string) (bool, error) { func areEqualJSON(s1, s2 string) (bool, error) {
@ -29,19 +50,13 @@ func areEqualJSON(s1, s2 string) (bool, error) {
// while also checking cryptographically that only the owner of a resource can update it. // while also checking cryptographically that only the owner of a resource can update it.
func TestEncodingDecodingUpdateRequests(t *testing.T) { func TestEncodingDecodingUpdateRequests(t *testing.T) {
signer := newCharlieSigner() //Charlie, our good guy charlie := newCharlieSigner() //Charlie
falseSigner := newBobSigner() //Bob will play the bad guy again bob := newBobSigner() //Bob
// Create a resource to our good guy Charlie's name // Create a resource to our good guy Charlie's name
createRequest, err := NewCreateRequest(&ResourceMetadata{ topic, _ := NewTopic("a good resource name", nil)
Name: "a good resource name", createRequest := NewFirstRequest(topic)
Frequency: 300, createRequest.User = charlie.Address()
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 // We now encode the create message to simulate we send it over the wire
messageRawData, err := createRequest.MarshalJSON() messageRawData, err := createRequest.MarshalJSON()
@ -64,27 +79,21 @@ func TestEncodingDecodingUpdateRequests(t *testing.T) {
// and recover the information above. To sign an update, we need the rootAddr and the metaHash to construct // and recover the information above. To sign an update, we need the rootAddr and the metaHash to construct
// proof of ownership // proof of ownership
metaHash := createRequest.metaHash const expectedSignature = "0x32c2d2c7224e24e4d3ae6a10595fc6e945f1b3ecdf548a04d8247c240a50c9240076aa7730abad6c8a46dfea00cfb8f43b6211f02db5c4cc5ed8584cb0212a4d00"
rootAddr := createRequest.rootAddr const expectedJSON = `{"view":{"topic":"0x6120676f6f64207265736f75726365206e616d65000000000000000000000000","user":"0x876a8936a7cd0b79ef0735ad0896c1afe278781c"},"epoch":{"time":1000,"level":1},"protocolVersion":0,"data":"0x5468697320686f75722773207570646174653a20537761726d2039392e3020686173206265656e2072656c656173656421"}`
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. //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!") data := []byte("This hour's update: Swarm 99.0 has been released!")
request := &Request{ request := &Request{
SignedResourceUpdate: SignedResourceUpdate{ ResourceUpdate: ResourceUpdate{
resourceUpdate: resourceUpdate{ ID: ID{
updateHeader: updateHeader{ Epoch: lookup.Epoch{
UpdateLookup: UpdateLookup{ Time: 1000,
period: 7, Level: 1,
version: 1,
rootAddr: rootAddr,
},
multihash: false,
metaHash: metaHash,
}, },
data: data, View: createRequest.ResourceUpdate.View,
}, },
data: data,
}, },
} }
@ -110,11 +119,11 @@ func TestEncodingDecodingUpdateRequests(t *testing.T) {
} }
//sign the request and see if it matches our predefined signature above. //sign the request and see if it matches our predefined signature above.
if err := recoveredRequest.Sign(signer); err != nil { if err := recoveredRequest.Sign(charlie); err != nil {
t.Fatalf("Error signing request: %s", err) t.Fatalf("Error signing request: %s", err)
} }
compareByteSliceToExpectedHex(t, "signature", recoveredRequest.signature[:], expectedSignature) compareByteSliceToExpectedHex(t, "signature", recoveredRequest.Signature[:], expectedSignature)
// mess with the signature and see what happens. To alter the signature, we briefly decode it as JSON // mess with the signature and see what happens. To alter the signature, we briefly decode it as JSON
// to alter the signature field. // to alter the signature field.
@ -129,9 +138,9 @@ func TestEncodingDecodingUpdateRequests(t *testing.T) {
t.Fatal("Expected DecodeUpdateRequest to fail when trying to interpret a corrupt message with an invalid signature") 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, // Now imagine Bob wants to create an update of his own about the same resource,
// signing a message with his private key // signing a message with his private key
if err := request.Sign(falseSigner); err != nil { if err := request.Sign(bob); err != nil {
t.Fatalf("Error signing: %s", err) t.Fatalf("Error signing: %s", err)
} }
@ -147,29 +156,159 @@ func TestEncodingDecodingUpdateRequests(t *testing.T) {
t.Fatalf("Error decoding message:%s", err) t.Fatalf("Error decoding message:%s", err)
} }
// Before discovering Bob's misdemeanor, let's see what would happen if we mess // Before checking what happened with Bob's update, let's see what would happen if we mess
// with the signature big time to see if Verify catches it // with the signature big time to see if Verify catches it
savedSignature := *recoveredRequest.signature // save the signature for later savedSignature := *recoveredRequest.Signature // save the signature for later
binary.LittleEndian.PutUint64(recoveredRequest.signature[5:], 556845463424) // write some random data to break the signature binary.LittleEndian.PutUint64(recoveredRequest.Signature[5:], 556845463424) // write some random data to break the signature
if err = recoveredRequest.Verify(); err == nil { if err = recoveredRequest.Verify(); err == nil {
t.Fatal("Expected Verify to fail on corrupt signature") t.Fatal("Expected Verify to fail on corrupt signature")
} }
// restore the Evil Bob's signature from corruption // restore the Bob's signature from corruption
*recoveredRequest.signature = savedSignature *recoveredRequest.Signature = savedSignature
// Now the signature is not corrupt, however Verify should now fail because Bob doesn't own the resource // Now the signature is not corrupt
if err = recoveredRequest.Verify(); err == nil { if err = recoveredRequest.Verify(); err != nil {
t.Fatalf("Expected Verify to fail because this resource belongs to Charlie, not Bob the attacker:%s", err) t.Fatal(err)
} }
// Sign with our friend Charlie's private key // Reuse object and sign with our friend Charlie's private key
if err := recoveredRequest.Sign(signer); err != nil { if err := recoveredRequest.Sign(charlie); err != nil {
t.Fatalf("Error signing with the correct private key: %s", err) t.Fatalf("Error signing with the correct private key: %s", err)
} }
// And now, Verify should work since this resource belongs to Charlie // And now, Verify should work since this update now belongs to Charlie
if err = recoveredRequest.Verify(); err != nil { if err = recoveredRequest.Verify(); err != nil {
t.Fatalf("Error verifying that Charlie, the good guy, can sign his resource:%s", err) t.Fatalf("Error verifying that Charlie, can sign a reused request object:%s", err)
}
// mess with the lookup key to make sure Verify fails:
recoveredRequest.Time = 77999 // this will alter the lookup key
if err = recoveredRequest.Verify(); err == nil {
t.Fatalf("Expected Verify to fail since the lookup key has been altered")
}
}
func getTestRequest() *Request {
return &Request{
ResourceUpdate: *getTestResourceUpdate(),
}
}
func TestUpdateChunkSerializationErrorChecking(t *testing.T) {
// Test that parseUpdate fails if the chunk is too small
var r Request
if err := r.fromChunk(storage.ZeroAddr, make([]byte, minimumUpdateDataLength-1+signatureLength)); err == nil {
t.Fatalf("Expected request.fromChunk to fail when chunkData contains less than %d bytes", minimumUpdateDataLength)
}
r = *getTestRequest()
_, err := r.toChunk()
if err == nil {
t.Fatal("Expected request.toChunk to fail when there is no data")
}
r.data = []byte("Al bien hacer jamás le falta premio") // put some arbitrary length data
_, err = r.toChunk()
if err == nil {
t.Fatal("expected request.toChunk to fail when there is no signature")
}
charlie := newCharlieSigner()
if err := r.Sign(charlie); err != nil {
t.Fatalf("error signing:%s", err)
}
chunk, err := r.toChunk()
if err != nil {
t.Fatalf("error creating update chunk:%s", err)
}
compareByteSliceToExpectedHex(t, "chunk", chunk.Data(), "0x0000000000000000776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce803000000000019416c206269656e206861636572206a616dc3a173206c652066616c7461207072656d696f5a0ffe0bc27f207cd5b00944c8b9cee93e08b89b5ada777f123ac535189333f174a6a4ca2f43a92c4a477a49d774813c36ce8288552c58e6205b0ac35d0507eb00")
var recovered Request
recovered.fromChunk(chunk.Address(), chunk.Data())
if !reflect.DeepEqual(recovered, r) {
t.Fatal("Expected recovered SignedResource update to equal the original one")
}
}
// check that signature address matches update signer address
func TestReverse(t *testing.T) {
epoch := lookup.Epoch{
Time: 7888,
Level: 6,
}
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
// set up rpc and create resourcehandler
_, _, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
topic, _ := NewTopic("Cervantes quotes", nil)
view := View{
Topic: topic,
User: signer.Address(),
}
data := []byte("Donde una puerta se cierra, otra se abre")
request := new(Request)
request.View = view
request.Epoch = epoch
request.data = data
// generate a chunk key for this request
key := request.Addr()
if err = request.Sign(signer); err != nil {
t.Fatal(err)
}
chunk, err := request.toChunk()
if err != nil {
t.Fatal(err)
}
// check that we can recover the owner account from the update chunk's signature
var checkUpdate Request
if err := checkUpdate.fromChunk(chunk.Address(), chunk.Data()); err != nil {
t.Fatal(err)
}
checkdigest, err := checkUpdate.GetDigest()
if err != nil {
t.Fatal(err)
}
recoveredaddress, err := getUserAddr(checkdigest, *checkUpdate.Signature)
if err != nil {
t.Fatalf("Retrieve address from signature fail: %v", err)
}
originaladdress := crypto.PubkeyToAddress(signer.PrivKey.PublicKey)
// check that the metadata retrieved from the chunk matches what we gave it
if recoveredaddress != originaladdress {
t.Fatalf("addresses dont match: %x != %x", originaladdress, recoveredaddress)
}
if !bytes.Equal(key[:], chunk.Address()[:]) {
t.Fatalf("Expected chunk key '%x', was '%x'", key, chunk.Address())
}
if epoch != checkUpdate.Epoch {
t.Fatalf("Expected epoch to be '%s', was '%s'", epoch.String(), checkUpdate.Epoch.String())
}
if !bytes.Equal(data, checkUpdate.data) {
t.Fatalf("Expected data '%x', was '%x'", data, checkUpdate.data)
} }
} }

View file

@ -1,76 +0,0 @@
// 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"
"context"
"time"
"github.com/ethereum/go-ethereum/swarm/storage"
)
const (
defaultStoreTimeout = 4000 * time.Millisecond
hasherCount = 8
resourceHashAlgorithm = storage.SHA3Hash
defaultRetrieveTimeout = 100 * time.Millisecond
)
// resource caches resource data and the metadata of its root chunk.
type resource struct {
resourceUpdate
ResourceMetadata
*bytes.Reader
lastKey storage.Address
updated time.Time
}
func (r *resource) Context() context.Context {
return context.TODO()
}
// TODO Expire content after a defined period (to force resync)
func (r *resource) isSynced() bool {
return !r.updated.IsZero()
}
// implements storage.LazySectionReader
func (r *resource) Size(ctx context.Context, _ chan bool) (int64, error) {
if !r.isSynced() {
return 0, NewError(ErrNotSynced, "Not synced")
}
return int64(len(r.resourceUpdate.data)), nil
}
//returns the resource's human-readable name
func (r *resource) Name() string {
return r.ResourceMetadata.Name
}
// Helper function to calculate the next update period number from the current time, start time and frequency
func getNextPeriod(start uint64, current uint64, frequency uint64) (uint32, error) {
if current < start {
return 0, NewErrorf(ErrInvalidValue, "given current time value %d < start time %d", current, start)
}
if frequency == 0 {
return 0, NewError(ErrInvalidValue, "frequency is 0")
}
timeDiff := current - start
period := timeDiff / frequency
return uint32(period + 1), nil
}

View file

@ -60,7 +60,16 @@ func (s *GenericSigner) Sign(data common.Hash) (signature Signature, err error)
return return
} }
// PublicKey returns the public key of the signer's private key // Address returns the public key of the signer's private key
func (s *GenericSigner) Address() common.Address { func (s *GenericSigner) Address() common.Address {
return s.address return s.address
} }
// getUserAddr extracts the address of the resource update signer
func getUserAddr(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
}

View file

@ -1,902 +0,0 @@
// 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"
"context"
"crypto/rand"
"encoding/binary"
"flag"
"io/ioutil"
"os"
"testing"
"time"
"github.com/ethereum/go-ethereum/contracts/ens"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/swarm/chunk"
"github.com/ethereum/go-ethereum/swarm/multihash"
"github.com/ethereum/go-ethereum/swarm/storage"
)
var (
loglevel = flag.Int("loglevel", 3, "loglevel")
testHasher = storage.MakeHashFunc(resourceHashAlgorithm)()
startTime = Timestamp{
Time: uint64(4200),
}
resourceFrequency = uint64(42)
cleanF func()
resourceName = "føø.bar"
hashfunc = storage.MakeHashFunc(storage.DefaultHash)
)
func init() {
flag.Parse()
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
}
// simulated timeProvider
type fakeTimeProvider struct {
currentTime uint64
}
func (f *fakeTimeProvider) Tick() {
f.currentTime++
}
func (f *fakeTimeProvider) Now() Timestamp {
return Timestamp{
Time: f.currentTime,
}
}
func TestUpdateChunkSerializationErrorChecking(t *testing.T) {
// Test that parseUpdate fails if the chunk is too small
var r SignedResourceUpdate
if err := r.fromChunk(storage.ZeroAddr, make([]byte, minimumUpdateDataLength-1)); err == nil {
t.Fatalf("Expected parseUpdate to fail when chunkData contains less than %d bytes", minimumUpdateDataLength)
}
r = SignedResourceUpdate{}
// Test that parseUpdate fails when the length header does not match the data array length
fakeChunk := make([]byte, 150)
binary.LittleEndian.PutUint16(fakeChunk, 44)
if err := r.fromChunk(storage.ZeroAddr, fakeChunk); err == nil {
t.Fatal("Expected parseUpdate to fail when the header length does not match the actual data array passed in")
}
r = SignedResourceUpdate{
resourceUpdate: resourceUpdate{
updateHeader: updateHeader{
UpdateLookup: UpdateLookup{
rootAddr: make([]byte, 79), // put the wrong length, should be storage.AddressLength
},
metaHash: nil,
multihash: false,
},
},
}
_, err := r.toChunk()
if err == nil {
t.Fatal("Expected newUpdateChunk to fail when rootAddr or metaHash have the wrong length")
}
r.rootAddr = make([]byte, storage.AddressLength)
r.metaHash = make([]byte, storage.AddressLength)
_, err = r.toChunk()
if err == nil {
t.Fatal("Expected newUpdateChunk to fail when there is no data")
}
r.data = make([]byte, 79) // put some arbitrary length data
_, err = r.toChunk()
if err == nil {
t.Fatal("expected newUpdateChunk to fail when there is no signature", err)
}
alice := newAliceSigner()
if err := r.Sign(alice); err != nil {
t.Fatalf("error signing:%s", err)
}
_, err = r.toChunk()
if err != nil {
t.Fatalf("error creating update chunk:%s", err)
}
r.multihash = true
r.data[1] = 79 // mess with the multihash, corrupting one byte of it.
if err := r.Sign(alice); err == nil {
t.Fatal("expected Sign() to fail when an invalid multihash is in data and multihash=true", err)
}
}
// check that signature address matches update signer address
func TestReverse(t *testing.T) {
period := uint32(4)
version := uint32(2)
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
// set up rpc and create resourcehandler
_, _, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
metadata := ResourceMetadata{
Name: resourceName,
StartTime: startTime,
Frequency: resourceFrequency,
Owner: signer.Address(),
}
rootAddr, metaHash, _, err := metadata.serializeAndHash()
if err != nil {
t.Fatal(err)
}
// generate some bogus data for the chunk and sign it
data := make([]byte, 8)
_, err = rand.Read(data)
if err != nil {
t.Fatal(err)
}
testHasher.Reset()
testHasher.Write(data)
update := &SignedResourceUpdate{
resourceUpdate: resourceUpdate{
updateHeader: updateHeader{
UpdateLookup: UpdateLookup{
period: period,
version: version,
rootAddr: rootAddr,
},
metaHash: metaHash,
},
data: data,
},
}
// generate a hash for t=4200 version 1
key := update.UpdateAddr()
if err = update.Sign(signer); err != nil {
t.Fatal(err)
}
chunk, err := update.toChunk()
if err != nil {
t.Fatal(err)
}
// check that we can recover the owner account from the update chunk's signature
var checkUpdate SignedResourceUpdate
if err := checkUpdate.fromChunk(chunk.Address(), chunk.Data()); err != nil {
t.Fatal(err)
}
checkdigest, err := checkUpdate.GetDigest()
if err != nil {
t.Fatal(err)
}
recoveredaddress, err := getOwner(checkdigest, *checkUpdate.signature)
if err != nil {
t.Fatalf("Retrieve address from signature fail: %v", err)
}
originaladdress := crypto.PubkeyToAddress(signer.PrivKey.PublicKey)
// check that the metadata retrieved from the chunk matches what we gave it
if recoveredaddress != originaladdress {
t.Fatalf("addresses dont match: %x != %x", originaladdress, recoveredaddress)
}
if !bytes.Equal(key[:], chunk.Address()[:]) {
t.Fatalf("Expected chunk key '%x', was '%x'", key, chunk.Address())
}
if period != checkUpdate.period {
t.Fatalf("Expected period '%d', was '%d'", period, checkUpdate.period)
}
if version != checkUpdate.version {
t.Fatalf("Expected version '%d', was '%d'", version, checkUpdate.version)
}
if !bytes.Equal(data, checkUpdate.data) {
t.Fatalf("Expectedn data '%x', was '%x'", data, checkUpdate.data)
}
}
// make updates and retrieve them based on periods and versions
func TestResourceHandler(t *testing.T) {
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
rh, datadir, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
// create a new resource
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
metadata := &ResourceMetadata{
Name: resourceName,
Frequency: resourceFrequency,
StartTime: Timestamp{Time: timeProvider.Now().Time},
Owner: signer.Address(),
}
request, err := NewCreateUpdateRequest(metadata)
if err != nil {
t.Fatal(err)
}
request.Sign(signer)
if err != nil {
t.Fatal(err)
}
err = rh.New(ctx, request)
if err != nil {
t.Fatal(err)
}
chunk, err := rh.chunkStore.Get(ctx, storage.Address(request.rootAddr))
if err != nil {
t.Fatal(err)
} else if len(chunk.Data()) < 16 {
t.Fatalf("chunk data must be minimum 16 bytes, is %d", len(chunk.Data()))
}
var recoveredMetadata ResourceMetadata
recoveredMetadata.binaryGet(chunk.Data())
if err != nil {
t.Fatal(err)
}
if recoveredMetadata.StartTime.Time != timeProvider.currentTime {
t.Fatalf("stored startTime %d does not match provided startTime %d", recoveredMetadata.StartTime.Time, timeProvider.currentTime)
}
if recoveredMetadata.Frequency != resourceFrequency {
t.Fatalf("stored frequency %d does not match provided frequency %d", recoveredMetadata.Frequency, resourceFrequency)
}
// data for updates:
updates := []string{
"blinky",
"pinky",
"inky",
"clyde",
}
// update halfway to first period. period=1, version=1
resourcekey := make(map[string]storage.Address)
fwdClock(int(resourceFrequency/2), timeProvider)
data := []byte(updates[0])
request.SetData(data, false)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[0]], err = rh.Update(ctx, &request.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
// update on first period with version = 1 to make it fail since there is already one update with version=1
request, err = rh.NewUpdateRequest(ctx, request.rootAddr)
if err != nil {
t.Fatal(err)
}
if request.version != 2 || request.period != 1 {
t.Fatal("Suggested period should be 1 and version should be 2")
}
request.version = 1 // force version 1 instead of 2 to make it fail
data = []byte(updates[1])
request.SetData(data, false)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[1]], err = rh.Update(ctx, &request.SignedResourceUpdate)
if err == nil {
t.Fatal("Expected update to fail since this version already exists")
}
// update on second period with version = 1, correct. period=2, version=1
fwdClock(int(resourceFrequency/2), timeProvider)
request, err = rh.NewUpdateRequest(ctx, request.rootAddr)
if err != nil {
t.Fatal(err)
}
request.SetData(data, false)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[1]], err = rh.Update(ctx, &request.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
fwdClock(int(resourceFrequency), timeProvider)
// Update on third period, with version = 1
request, err = rh.NewUpdateRequest(ctx, request.rootAddr)
if err != nil {
t.Fatal(err)
}
data = []byte(updates[2])
request.SetData(data, false)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[2]], err = rh.Update(ctx, &request.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
// update just after third period
fwdClock(1, timeProvider)
request, err = rh.NewUpdateRequest(ctx, request.rootAddr)
if err != nil {
t.Fatal(err)
}
if request.period != 3 || request.version != 2 {
t.Fatal("Suggested period should be 3 and version should be 2")
}
data = []byte(updates[3])
request.SetData(data, false)
if err := request.Sign(signer); err != nil {
t.Fatal(err)
}
resourcekey[updates[3]], err = rh.Update(ctx, &request.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
rh.Close()
// check we can retrieve the updates after close
// it will match on second iteration startTime + (resourceFrequency * 3)
fwdClock(int(resourceFrequency*2)-1, timeProvider)
rhparams := &HandlerParams{}
rh2, err := NewTestHandler(datadir, rhparams)
if err != nil {
t.Fatal(err)
}
rsrc2, err := rh2.Load(context.TODO(), request.rootAddr)
if err != nil {
t.Fatal(err)
}
_, err = rh2.Lookup(ctx, LookupLatest(request.rootAddr))
if err != nil {
t.Fatal(err)
}
// last update should be "clyde", version two, time= startTime + (resourcefrequency * 3)
if !bytes.Equal(rsrc2.data, []byte(updates[len(updates)-1])) {
t.Fatalf("resource data was %v, expected %v", string(rsrc2.data), updates[len(updates)-1])
}
if rsrc2.version != 2 {
t.Fatalf("resource version was %d, expected 2", rsrc2.version)
}
if rsrc2.period != 3 {
t.Fatalf("resource period was %d, expected 3", rsrc2.period)
}
log.Debug("Latest lookup", "period", rsrc2.period, "version", rsrc2.version, "data", rsrc2.data)
// specific period, latest version
rsrc, err := rh2.Lookup(ctx, LookupLatestVersionInPeriod(request.rootAddr, 3))
if err != nil {
t.Fatal(err)
}
// check data
if !bytes.Equal(rsrc.data, []byte(updates[len(updates)-1])) {
t.Fatalf("resource data (historical) was %v, expected %v", string(rsrc2.data), updates[len(updates)-1])
}
log.Debug("Historical lookup", "period", rsrc2.period, "version", rsrc2.version, "data", rsrc2.data)
// specific period, specific version
lookupParams := LookupVersion(request.rootAddr, 3, 1)
rsrc, err = rh2.Lookup(ctx, lookupParams)
if err != nil {
t.Fatal(err)
}
// check data
if !bytes.Equal(rsrc.data, []byte(updates[2])) {
t.Fatalf("resource data (historical) was %v, expected %v", string(rsrc2.data), updates[2])
}
log.Debug("Specific version lookup", "period", rsrc2.period, "version", rsrc2.version, "data", rsrc2.data)
// we are now at third update
// check backwards stepping to the first
for i := 1; i >= 0; i-- {
rsrc, err := rh2.LookupPrevious(ctx, lookupParams)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rsrc.data, []byte(updates[i])) {
t.Fatalf("resource data (previous) was %v, expected %v", rsrc.data, updates[i])
}
}
// beyond the first should yield an error
rsrc, err = rh2.LookupPrevious(ctx, lookupParams)
if err == nil {
t.Fatalf("expected previous to fail, returned period %d version %d data %v", rsrc.period, rsrc.version, rsrc.data)
}
}
func TestMultihash(t *testing.T) {
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
// set up rpc and create resourcehandler
rh, datadir, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
// create a new resource
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
metadata := &ResourceMetadata{
Name: resourceName,
Frequency: resourceFrequency,
StartTime: Timestamp{Time: timeProvider.Now().Time},
Owner: signer.Address(),
}
mr, err := NewCreateRequest(metadata)
if err != nil {
t.Fatal(err)
}
err = rh.New(ctx, mr)
if err != nil {
t.Fatal(err)
}
// we're naïvely assuming keccak256 for swarm hashes
// if it ever changes this test should also change
multihashbytes := ens.EnsNode("foo")
multihashmulti := multihash.ToMultihash(multihashbytes.Bytes())
if err != nil {
t.Fatal(err)
}
mr.SetData(multihashmulti, true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
multihashkey, err := rh.Update(ctx, &mr.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
sha1bytes := make([]byte, multihash.MultihashLength)
sha1multi := multihash.ToMultihash(sha1bytes)
if err != nil {
t.Fatal(err)
}
mr, err = rh.NewUpdateRequest(ctx, mr.rootAddr)
if err != nil {
t.Fatal(err)
}
mr.SetData(sha1multi, true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
sha1key, err := rh.Update(ctx, &mr.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
// invalid multihashes
mr, err = rh.NewUpdateRequest(ctx, mr.rootAddr)
if err != nil {
t.Fatal(err)
}
mr.SetData(multihashmulti[1:], true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
_, err = rh.Update(ctx, &mr.SignedResourceUpdate)
if err == nil {
t.Fatalf("Expected update to fail with first byte skipped")
}
mr, err = rh.NewUpdateRequest(ctx, mr.rootAddr)
if err != nil {
t.Fatal(err)
}
mr.SetData(multihashmulti[:len(multihashmulti)-2], true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
_, err = rh.Update(ctx, &mr.SignedResourceUpdate)
if err == nil {
t.Fatalf("Expected update to fail with last byte skipped")
}
data, err := getUpdateDirect(rh.Handler, multihashkey)
if err != nil {
t.Fatal(err)
}
multihashdecode, err := multihash.FromMultihash(data)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(multihashdecode, multihashbytes.Bytes()) {
t.Fatalf("Decoded hash '%x' does not match original hash '%x'", multihashdecode, multihashbytes.Bytes())
}
data, err = getUpdateDirect(rh.Handler, sha1key)
if err != nil {
t.Fatal(err)
}
shadecode, err := multihash.FromMultihash(data)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(shadecode, sha1bytes) {
t.Fatalf("Decoded hash '%x' does not match original hash '%x'", shadecode, sha1bytes)
}
rh.Close()
rhparams := &HandlerParams{}
// test with signed data
rh2, err := NewTestHandler(datadir, rhparams)
if err != nil {
t.Fatal(err)
}
mr, err = NewCreateRequest(metadata)
if err != nil {
t.Fatal(err)
}
err = rh2.New(ctx, mr)
if err != nil {
t.Fatal(err)
}
mr.SetData(multihashmulti, true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
multihashsignedkey, err := rh2.Update(ctx, &mr.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
mr, err = rh2.NewUpdateRequest(ctx, mr.rootAddr)
if err != nil {
t.Fatal(err)
}
mr.SetData(sha1multi, true)
mr.Sign(signer)
if err != nil {
t.Fatal(err)
}
sha1signedkey, err := rh2.Update(ctx, &mr.SignedResourceUpdate)
if err != nil {
t.Fatal(err)
}
data, err = getUpdateDirect(rh2.Handler, multihashsignedkey)
if err != nil {
t.Fatal(err)
}
multihashdecode, err = multihash.FromMultihash(data)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(multihashdecode, multihashbytes.Bytes()) {
t.Fatalf("Decoded hash '%x' does not match original hash '%x'", multihashdecode, multihashbytes.Bytes())
}
data, err = getUpdateDirect(rh2.Handler, sha1signedkey)
if err != nil {
t.Fatal(err)
}
shadecode, err = multihash.FromMultihash(data)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(shadecode, sha1bytes) {
t.Fatalf("Decoded hash '%x' does not match original hash '%x'", shadecode, sha1bytes)
}
}
// \TODO verify testing of signature validation and enforcement
func TestValidator(t *testing.T) {
// make fake timeProvider
timeProvider := &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key. Alice will be the good girl
signer := newAliceSigner()
// fake signer for false results. Bob will play the bad guy today.
falseSigner := newBobSigner()
// set up sim timeProvider
rh, _, teardownTest, err := setupTest(timeProvider, signer)
if err != nil {
t.Fatal(err)
}
defer teardownTest()
// create new resource
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
metadata := &ResourceMetadata{
Name: resourceName,
Frequency: resourceFrequency,
StartTime: Timestamp{Time: timeProvider.Now().Time},
Owner: signer.Address(),
}
mr, err := NewCreateRequest(metadata)
if err != nil {
t.Fatal(err)
}
mr.Sign(signer)
err = rh.New(ctx, mr)
if err != nil {
t.Fatalf("Create resource fail: %v", err)
}
// chunk with address
data := []byte("foo")
mr.SetData(data, false)
if err := mr.Sign(signer); err != nil {
t.Fatalf("sign fail: %v", err)
}
chunk, err := mr.SignedResourceUpdate.toChunk()
if err != nil {
t.Fatal(err)
}
if !rh.Validate(chunk.Address(), chunk.Data()) {
t.Fatal("Chunk validator fail on update chunk")
}
// chunk with address made from different publickey
if err := mr.Sign(falseSigner); err == nil {
t.Fatalf("Expected Sign to fail since we are using a different OwnerAddr: %v", err)
}
// chunk with address made from different publickey
mr.metadata.Owner = zeroAddr // set to zero to bypass .Sign() check
if err := mr.Sign(falseSigner); err != nil {
t.Fatalf("sign fail: %v", err)
}
chunk, err = mr.SignedResourceUpdate.toChunk()
if err != nil {
t.Fatal(err)
}
if rh.Validate(chunk.Address(), chunk.Data()) {
t.Fatal("Chunk validator did not fail on update chunk with false address")
}
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
metadata = &ResourceMetadata{
Name: resourceName,
StartTime: TimestampProvider.Now(),
Frequency: resourceFrequency,
Owner: signer.Address(),
}
chunk, _, err = metadata.newChunk()
if err != nil {
t.Fatal(err)
}
if !rh.Validate(chunk.Address(), chunk.Data()) {
t.Fatal("Chunk validator fail on metadata chunk")
}
}
// tests that the content address validator correctly checks the data
// tests that resource update chunks are passed through content address validator
// there is some redundancy in this test as it also tests content addressed chunks,
// which should be evaluated as invalid chunks by this validator
func TestValidatorInStore(t *testing.T) {
// make fake timeProvider
TimestampProvider = &fakeTimeProvider{
currentTime: startTime.Time,
}
// signer containing private key
signer := newAliceSigner()
// set up localstore
datadir, err := ioutil.TempDir("", "storage-testresourcevalidator")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(datadir)
params := storage.NewDefaultLocalStoreParams()
params.Init(datadir)
store, err := storage.NewLocalStore(params, nil)
if err != nil {
t.Fatal(err)
}
// set up resource handler and add is as a validator to the localstore
rhParams := &HandlerParams{}
rh := NewHandler(rhParams)
store.Validators = append(store.Validators, rh)
// create content addressed chunks, one good, one faulty
chunks := storage.GenerateRandomChunks(chunk.DefaultSize, 2)
goodChunk := chunks[0]
badChunk := storage.NewChunk(chunks[1].Address(), goodChunk.Data())
metadata := &ResourceMetadata{
StartTime: startTime,
Name: "xyzzy",
Frequency: resourceFrequency,
Owner: signer.Address(),
}
rootChunk, metaHash, err := metadata.newChunk()
if err != nil {
t.Fatal(err)
}
// create a resource update chunk with correct publickey
updateLookup := UpdateLookup{
period: 42,
version: 1,
rootAddr: rootChunk.Address(),
}
updateAddr := updateLookup.UpdateAddr()
data := []byte("bar")
r := SignedResourceUpdate{
updateAddr: updateAddr,
resourceUpdate: resourceUpdate{
updateHeader: updateHeader{
UpdateLookup: updateLookup,
metaHash: metaHash,
},
data: data,
},
}
r.Sign(signer)
uglyChunk, err := r.toChunk()
if err != nil {
t.Fatal(err)
}
// put the chunks in the store and check their error status
err = store.Put(context.Background(), goodChunk)
if err == nil {
t.Fatal("expected error on good content address chunk with resource validator only, but got nil")
}
err = store.Put(context.Background(), badChunk)
if err == nil {
t.Fatal("expected error on bad content address chunk with resource validator only, but got nil")
}
err = store.Put(context.Background(), uglyChunk)
if err != nil {
t.Fatalf("expected no error on resource update chunk with resource validator only, but got: %s", err)
}
}
// fast-forward clock
func fwdClock(count int, timeProvider *fakeTimeProvider) {
for i := 0; i < count; i++ {
timeProvider.Tick()
}
}
// create rpc and resourcehandler
func setupTest(timeProvider timestampProvider, signer Signer) (rh *TestHandler, datadir string, teardown func(), err error) {
var fsClean func()
var rpcClean func()
cleanF = func() {
if fsClean != nil {
fsClean()
}
if rpcClean != nil {
rpcClean()
}
}
// temp datadir
datadir, err = ioutil.TempDir("", "rh")
if err != nil {
return nil, "", nil, err
}
fsClean = func() {
os.RemoveAll(datadir)
}
TimestampProvider = timeProvider
rhparams := &HandlerParams{}
rh, err = NewTestHandler(datadir, rhparams)
return rh, datadir, cleanF, err
}
func newAliceSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
return NewGenericSigner(privKey)
}
func newBobSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("accedeaccedeaccedeaccedeaccedeaccedeaccedeaccedeaccedeaccedecaca")
return NewGenericSigner(privKey)
}
func newCharlieSigner() *GenericSigner {
privKey, _ := crypto.HexToECDSA("facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca")
return NewGenericSigner(privKey)
}
func getUpdateDirect(rh *Handler, addr storage.Address) ([]byte, error) {
chunk, err := rh.chunkStore.Get(context.TODO(), addr)
if err != nil {
return nil, err
}
var r SignedResourceUpdate
if err := r.fromChunk(addr, chunk.Data()); err != nil {
return nil, err
}
return r.data, nil
}

View file

@ -1,181 +0,0 @@
// 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.")
}
resourceUpdateLength := r.resourceUpdate.binaryLength()
// signature is the last item in the chunk data
copy(r.binaryData[resourceUpdateLength:], r.signature[:])
chunk := storage.NewChunk(r.updateAddr, r.binaryData)
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)
}

View file

@ -40,7 +40,7 @@ func (t *TestHandler) Close() {
type mockNetFetcher struct{} type mockNetFetcher struct{}
func (m *mockNetFetcher) Request(ctx context.Context) { func (m *mockNetFetcher) Request(ctx context.Context, hopCount uint8) {
} }
func (m *mockNetFetcher) Offer(ctx context.Context, source *enode.ID) { func (m *mockNetFetcher) Offer(ctx context.Context, source *enode.ID) {
} }

View file

@ -18,15 +18,16 @@ package mru
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json"
"time" "time"
) )
// TimestampProvider sets the time source of the mru package // TimestampProvider sets the time source of the mru package
var TimestampProvider timestampProvider = NewDefaultTimestampProvider() var TimestampProvider timestampProvider = NewDefaultTimestampProvider()
// Encodes a point in time as a Unix epoch // Timestamp encodes a point in time as a Unix epoch
type Timestamp struct { type Timestamp struct {
Time uint64 // Unix epoch timestamp, in seconds Time uint64 `json:"time"` // Unix epoch timestamp, in seconds
} }
// 8 bytes uint64 Time // 8 bytes uint64 Time
@ -55,6 +56,18 @@ func (t *Timestamp) binaryPut(data []byte) error {
return nil return nil
} }
// UnmarshalJSON implements the json.Unmarshaller interface
func (t *Timestamp) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &t.Time)
}
// MarshalJSON implements the json.Marshaller interface
func (t *Timestamp) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Time)
}
// DefaultTimestampProvider is a TimestampProvider that uses system time
// as time source
type DefaultTimestampProvider struct { type DefaultTimestampProvider struct {
} }

105
swarm/storage/mru/topic.go Normal file
View file

@ -0,0 +1,105 @@
// 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"
"fmt"
"github.com/ethereum/go-ethereum/common/bitutil"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/swarm/storage"
)
// TopicLength establishes the max length of a topic string
const TopicLength = storage.AddressLength
// Topic represents what a resource talks about
type Topic [TopicLength]byte
// ErrTopicTooLong is returned when creating a topic with a name/related content too long
var ErrTopicTooLong = fmt.Errorf("Topic is too long. Max length is %d", TopicLength)
// NewTopic creates a new topic from a provided name and "related content" byte array,
// merging the two together.
// If relatedContent or name are longer than TopicLength, they will be truncated and an error returned
// name can be an empty string
// relatedContent can be nil
func NewTopic(name string, relatedContent []byte) (topic Topic, err error) {
if relatedContent != nil {
contentLength := len(relatedContent)
if contentLength > TopicLength {
contentLength = TopicLength
err = ErrTopicTooLong
}
copy(topic[:], relatedContent[:contentLength])
}
nameBytes := []byte(name)
nameLength := len(nameBytes)
if nameLength > TopicLength {
nameLength = TopicLength
err = ErrTopicTooLong
}
bitutil.XORBytes(topic[:], topic[:], nameBytes[:nameLength])
return topic, err
}
// Hex will return the topic encoded as an hex string
func (t *Topic) Hex() string {
return hexutil.Encode(t[:])
}
// FromHex will parse a hex string into this Topic instance
func (t *Topic) FromHex(hex string) error {
bytes, err := hexutil.Decode(hex)
if err != nil || len(bytes) != len(t) {
return NewErrorf(ErrInvalidValue, "Cannot decode topic")
}
copy(t[:], bytes)
return nil
}
// Name will try to extract the resource name out of the topic
func (t *Topic) Name(relatedContent []byte) string {
nameBytes := *t
if relatedContent != nil {
contentLength := len(relatedContent)
if contentLength > TopicLength {
contentLength = TopicLength
}
bitutil.XORBytes(nameBytes[:], t[:], relatedContent[:contentLength])
}
z := bytes.IndexByte(nameBytes[:], 0)
if z < 0 {
z = TopicLength
}
return string(nameBytes[:z])
}
// UnmarshalJSON implements the json.Unmarshaller interface
func (t *Topic) UnmarshalJSON(data []byte) error {
var hex string
json.Unmarshal(data, &hex)
return t.FromHex(hex)
}
// MarshalJSON implements the json.Marshaller interface
func (t *Topic) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Hex())
}

View file

@ -0,0 +1,50 @@
package mru
import (
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func TestTopic(t *testing.T) {
related, _ := hexutil.Decode("0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
topicName := "test-topic"
topic, _ := NewTopic(topicName, related)
hex := topic.Hex()
expectedHex := "0xdfa89c750e3108f9c2aeef0123456789abcdef0123456789abcdef0123456789"
if hex != expectedHex {
t.Fatalf("Expected %s, got %s", expectedHex, hex)
}
var topic2 Topic
topic2.FromHex(hex)
if topic2 != topic {
t.Fatal("Expected recovered topic to be equal to original one")
}
if topic2.Name(related) != topicName {
t.Fatal("Retrieved name does not match")
}
bytes, err := topic2.MarshalJSON()
if err != nil {
t.Fatal(err)
}
expectedJSON := `"0xdfa89c750e3108f9c2aeef0123456789abcdef0123456789abcdef0123456789"`
equal, err := areEqualJSON(expectedJSON, string(bytes))
if err != nil {
t.Fatal(err)
}
if !equal {
t.Fatalf("Expected JSON to be %s, got %s", expectedJSON, string(bytes))
}
err = topic2.UnmarshalJSON(bytes)
if err != nil {
t.Fatal(err)
}
if topic2 != topic {
t.Fatal("Expected recovered topic to be equal to original one")
}
}

Some files were not shown because too many files have changed in this diff Show more