swarm/storage/mru: Client-side MRU signatures (#784)
* swarm/storage/mru: Add embedded publickey and remove ENS dep This commit breaks swarm, swarm/api... but tests in swarm/storage/mru pass * swarm: Refactor swarm, swarm/api to mru changes, make tests pass * swarm/storage/mru: Remove self from recv, remove test ens vldtr * swarm/storage/mru: Remove redundant test, expose ResourceHash mthd * swarm/storage/mru: Make HeaderGetter mandatory + godoc fixes * swarm/storage: Remove validator prefix for metadata chunk * swarm/storage/mru: Use Address instead of PublicKey * swarm/storage/mru: Change index from name to metadata chunk addr * swarm/storage/mru: Refactor swarm/api/... to MRU index changes * swarm/storage/mru: Refactor cleanup * swarm/storage/mru: Rebase cleanup * swarm: Use constructor for GenericSigner MRU in swarm.go * swarm/storage: Change to BMTHash for MRU hashing * swarm/storage: Reduce loglevel on chunk validator logs * swarm/storage/mru: Delint * swarm: MRU Rebase cleanup * swarm/storage/mru: client-side mru signatures Rebase to PR #668 and fix all conflicts * swarm/storage/mru: refactor and documentation * swarm/resource/mru: error-checking tests for parseUpdate/newUpdateChunk * swarm/storage/mru: Added resourcemetadata tests * swarm/storage/mru: Added tests for UpdateRequest * swarm/storage/mru: more test coverage for UpdateRequest and comments * swarm/storage/mru: Avoid fake chunks in parseUpdate() * swarm/storage/mru: Documented resource.go extensively moved some functions where they make most sense * swarm/storage/mru: increase test coverage for UpdateRequest and variable name changes throughout to increase consistency * swarm/storage/mru: moved default timestamp to NewCreateRequest- * swarm/storage/mru: lookup refactor * swarm/storage/mru: added comments and renamed raw flag to rawmru * swarm/storage/mru: fix receiver typo * swarm/storage/mru: refactored update chunk new/create * swarm/storage/mru: refactored signature digest to avoid malleability * swarm/storage/mru: optimize update data serialization * swarm/storage/mru: refactor and cleanup * swarm/storage/mru: add timestamp struct and serialization * swarm/storage/mru: fix lint error and mark some old code for deletion * swarm/storage/mru: remove unnecessary variable * swarm/storage/mru: Added more comments throughout * swarm/storage/mru: Refactored metadata chunk layout + extensive error... * swarm/storage/mru: refactor cli parser Changed resource info output to JSON * swarm/storage/mru: refactor serialization for extensibility refactored error messages to NewErrorf * swarm/storage/mru: Moved Signature to resource_sign. Check Sign errors in server tests * swarm/storage/mru: Remove isSafeName() checks * swarm/storage/mru: scrubbed off all references to "block" for time * swarm/storage/mru: removed superfluous isSynced() call. * swarm/storage/mru: remove isMultihash() and ToSafeName functions * swarm/storage/mru: various fixes and comments * swarm/storage/mru: decoupled cli for independent create/update * Made resource name optional * Removed unused LookupPrevious * swarm/storage/mru: Decoupled resource create / update & refactor * swarm/storage/mru: Fixed some comments as per issues raised in PR #743 * swarm/storage/mru: Cosmetic changes as per #743 comments * swarm/storage/mru: refct request encoder/decoder > marshal/unmarshal * swarm/storage/mru: Cosmetic changes as per review in #748 * swarm/storage/mru: removed timestamp proof placeholder * swarm/storage/mru: cosmetic/doc/fixes changes as per comments in #704 * swarm/storage/mru: removed unnecessary check in Handler.update * swarm/storage/mru: Implemented Marshaler/Unmarshaler iface in Request * swarm/storage/mru: Fixed linter error * swarm/storage/mru: removed redundant address in signature digest * swarm/storage/mru: fixed bug: LookupLatestVersionInPeriod not working * swarm/storage/mru: Unfold Request creation API for create or update+create set common time source for mru package * swarm/api/http: fix HandleGetResource error variable shadowed when requesting a resource that does not exist * swarm/storage/mru: Add simple check to detect duplicate updates * swarm/storage/mru: moved Multihash() to the right place. * cmd/swarm: remove unneeded clientaccountmanager.go * swarm/storage/mru: Changed some comments as per reviews in #784 * swarm/storage/mru: Made SignedResourceUpdate.GetDigest() public * swarm/storage/mru: cosmetic changes as per comments in #784 * cmd/swarm: Inverted --multihash flag default * swarm/storage/mru: removed Verify from SignedResourceUpdate.fromChunk * swarm/storage/mru: Moved validation code out of serializer Cosmetic / comment changes * swarm/storage/mru: Added unit tests for UpdateLookup * swarm/storage/mru: Increased coverage of metadata serialization * swarm/storage/mru: Increased test coverage of updateHeader serializers * swarm/storage/mru: Add resourceUpdate serializer test
This commit is contained in:
		
				
					committed by
					
						 Anton Evangelatov
						Anton Evangelatov
					
				
			
			
				
	
			
			
			
						parent
						
							0647c4de7b
						
					
				
				
					commit
					427316a707
				
			| @@ -182,6 +182,18 @@ 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{ | ||||||
|  | 		Name:  "multihash", | ||||||
|  | 		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", | ||||||
|  | 	} | ||||||
|  | 	SwarmResourceNameFlag = cli.StringFlag{ | ||||||
|  | 		Name:  "name", | ||||||
|  | 		Usage: "User-defined name for the new resource", | ||||||
|  | 	} | ||||||
|  | 	SwarmResourceDataOnCreateFlag = cli.StringFlag{ | ||||||
|  | 		Name:  "data", | ||||||
|  | 		Usage: "Initializes the resource with the given hex-encoded data. Data must be prefixed by 0x", | ||||||
|  | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //declare a few constant error messages, useful for later error check comparisons in test | //declare a few constant error messages, useful for later error check comparisons in test | ||||||
| @@ -235,6 +247,41 @@ func init() { | |||||||
| 			Flags:              []cli.Flag{SwarmEncryptedFlag}, | 			Flags:              []cli.Flag{SwarmEncryptedFlag}, | ||||||
| 			Description:        "uploads a file or directory to swarm using the HTTP API and prints the root hash", | 			Description:        "uploads a file or directory to swarm using the HTTP API and prints the root hash", | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			CustomHelpTemplate: helpTemplate, | ||||||
|  | 			Name:               "resource", | ||||||
|  | 			Usage:              "(Advanced) Create and update Mutable Resources", | ||||||
|  | 			ArgsUsage:          "<create|update|info>", | ||||||
|  | 			Description:        "Works with Mutable Resource Updates", | ||||||
|  | 			Subcommands: []cli.Command{ | ||||||
|  | 				{ | ||||||
|  | 					Action:             resourceCreate, | ||||||
|  | 					CustomHelpTemplate: helpTemplate, | ||||||
|  | 					Name:               "create", | ||||||
|  | 					Usage:              "creates a new Mutable Resource", | ||||||
|  | 					ArgsUsage:          "<frequency>", | ||||||
|  | 					Description:        "creates a new Mutable Resource", | ||||||
|  | 					Flags:              []cli.Flag{SwarmResourceNameFlag, SwarmResourceDataOnCreateFlag, SwarmResourceMultihashFlag}, | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Action:             resourceUpdate, | ||||||
|  | 					CustomHelpTemplate: helpTemplate, | ||||||
|  | 					Name:               "update", | ||||||
|  | 					Usage:              "updates the content of an existing Mutable Resource", | ||||||
|  | 					ArgsUsage:          "<Manifest Address or ENS domain> <0x Hex data>", | ||||||
|  | 					Description:        "updates the content of an existing Mutable Resource", | ||||||
|  | 					Flags:              []cli.Flag{SwarmResourceMultihashFlag}, | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					Action:             resourceInfo, | ||||||
|  | 					CustomHelpTemplate: helpTemplate, | ||||||
|  | 					Name:               "info", | ||||||
|  | 					Usage:              "obtains information about an existing Mutable Resource", | ||||||
|  | 					ArgsUsage:          "<Manifest Address or ENS domain>", | ||||||
|  | 					Description:        "obtains information about an existing Mutable Resource", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Action:             list, | 			Action:             list, | ||||||
| 			CustomHelpTemplate: helpTemplate, | 			CustomHelpTemplate: helpTemplate, | ||||||
| @@ -563,6 +610,26 @@ func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.Pr | |||||||
| 	return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx)) | 	return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // getPrivKey returns the private key of the specified bzzaccount | ||||||
|  | // Used only by client commands, such as `resource` | ||||||
|  | func getPrivKey(ctx *cli.Context) *ecdsa.PrivateKey { | ||||||
|  | 	// booting up the swarm node just as we do in bzzd action | ||||||
|  | 	bzzconfig, err := buildConfig(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("unable to configure swarm: %v", err) | ||||||
|  | 	} | ||||||
|  | 	cfg := defaultNodeConfig | ||||||
|  | 	if _, err := os.Stat(bzzconfig.Path); err == nil { | ||||||
|  | 		cfg.DataDir = bzzconfig.Path | ||||||
|  | 	} | ||||||
|  | 	utils.SetNodeConfig(ctx, &cfg) | ||||||
|  | 	stack, err := node.New(&cfg) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("can't create node: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return getAccount(bzzconfig.BzzAccount, ctx, stack) | ||||||
|  | } | ||||||
|  |  | ||||||
| func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey { | func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey { | ||||||
| 	var a accounts.Account | 	var a accounts.Account | ||||||
| 	var err error | 	var err error | ||||||
|   | |||||||
							
								
								
									
										169
									
								
								cmd/swarm/mru.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								cmd/swarm/mru.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | |||||||
|  | // Copyright 2016 The go-ethereum Authors | ||||||
|  | // This file is part of go-ethereum. | ||||||
|  | // | ||||||
|  | // go-ethereum is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // go-ethereum is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU General Public License | ||||||
|  | // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | // Command resource allows the user to create and update signed mutable resource updates | ||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/cmd/utils" | ||||||
|  | 	swarm "github.com/ethereum/go-ethereum/swarm/api/client" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage/mru" | ||||||
|  | 	"gopkg.in/urfave/cli.v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func NewGenericSigner(ctx *cli.Context) mru.Signer { | ||||||
|  | 	return mru.NewGenericSigner(getPrivKey(ctx)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // swarm resource create <frequency> [--name <name>] [--data <0x Hexdata> [--multihash=false]] | ||||||
|  | // swarm resource update <Manifest Address or ENS domain> <0x Hexdata> [--multihash=false] | ||||||
|  | // swarm resource info <Manifest Address or ENS domain> | ||||||
|  |  | ||||||
|  | func resourceCreate(ctx *cli.Context) { | ||||||
|  | 	args := ctx.Args() | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		bzzapi      = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") | ||||||
|  | 		client      = swarm.NewClient(bzzapi) | ||||||
|  | 		multihash   = ctx.Bool(SwarmResourceMultihashFlag.Name) | ||||||
|  | 		initialData = ctx.String(SwarmResourceDataOnCreateFlag.Name) | ||||||
|  | 		name        = ctx.String(SwarmResourceNameFlag.Name) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	if len(args) < 1 { | ||||||
|  | 		fmt.Println("Incorrect number of arguments") | ||||||
|  | 		cli.ShowCommandHelpAndExit(ctx, "create", 1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	signer := NewGenericSigner(ctx) | ||||||
|  | 	frequency, err := strconv.ParseUint(args[0], 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("Frequency formatting error: %s\n", err.Error()) | ||||||
|  | 		cli.ShowCommandHelpAndExit(ctx, "create", 1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	metadata := mru.ResourceMetadata{ | ||||||
|  | 		Name:      name, | ||||||
|  | 		Frequency: frequency, | ||||||
|  | 		Owner:     signer.Address(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var newResourceRequest *mru.Request | ||||||
|  | 	if initialData != "" { | ||||||
|  | 		initialDataBytes, err := hexutil.Decode(initialData) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Printf("Error parsing data: %s\n", err.Error()) | ||||||
|  | 			cli.ShowCommandHelpAndExit(ctx, "create", 1) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		newResourceRequest, err = mru.NewCreateUpdateRequest(&metadata) | ||||||
|  | 		if err != nil { | ||||||
|  | 			utils.Fatalf("Error creating new resource request: %s", err) | ||||||
|  | 		} | ||||||
|  | 		newResourceRequest.SetData(initialDataBytes, multihash) | ||||||
|  | 		if err = newResourceRequest.Sign(signer); err != nil { | ||||||
|  | 			utils.Fatalf("Error signing resource update: %s", err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		newResourceRequest, err = mru.NewCreateRequest(&metadata) | ||||||
|  | 		if err != nil { | ||||||
|  | 			utils.Fatalf("Error creating new resource request: %s", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	manifestAddress, err := client.CreateResource(newResourceRequest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error creating resource: %s", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Println(manifestAddress) // output manifest address to the user in a single line (useful for other commands to pick up) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func resourceUpdate(ctx *cli.Context) { | ||||||
|  | 	args := ctx.Args() | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		bzzapi    = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") | ||||||
|  | 		client    = swarm.NewClient(bzzapi) | ||||||
|  | 		multihash = ctx.Bool(SwarmResourceMultihashFlag.Name) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	if len(args) < 2 { | ||||||
|  | 		fmt.Println("Incorrect number of arguments") | ||||||
|  | 		cli.ShowCommandHelpAndExit(ctx, "update", 1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	signer := NewGenericSigner(ctx) | ||||||
|  | 	manifestAddressOrDomain := args[0] | ||||||
|  | 	data, err := hexutil.Decode(args[1]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error parsing data: %s", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Retrieve resource status and metadata out of the manifest | ||||||
|  | 	updateRequest, err := client.GetResourceMetadata(manifestAddressOrDomain) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error retrieving resource status: %s", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set the new data | ||||||
|  | 	updateRequest.SetData(data, multihash) | ||||||
|  |  | ||||||
|  | 	// sign update | ||||||
|  | 	if err = updateRequest.Sign(signer); err != nil { | ||||||
|  | 		utils.Fatalf("Error signing resource update: %s", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// post update | ||||||
|  | 	err = client.UpdateResource(updateRequest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error updating resource: %s", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func resourceInfo(ctx *cli.Context) { | ||||||
|  | 	var ( | ||||||
|  | 		bzzapi = strings.TrimRight(ctx.GlobalString(SwarmApiFlag.Name), "/") | ||||||
|  | 		client = swarm.NewClient(bzzapi) | ||||||
|  | 	) | ||||||
|  | 	args := ctx.Args() | ||||||
|  | 	if len(args) < 1 { | ||||||
|  | 		fmt.Println("Incorrect number of arguments.") | ||||||
|  | 		cli.ShowCommandHelpAndExit(ctx, "info", 1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	manifestAddressOrDomain := args[0] | ||||||
|  | 	metadata, err := client.GetResourceMetadata(manifestAddressOrDomain) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error retrieving resource metadata: %s", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	encodedMetadata, err := metadata.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.Fatalf("Error encoding metadata to JSON for display:%s", err) | ||||||
|  | 	} | ||||||
|  | 	fmt.Println(string(encodedMetadata)) | ||||||
|  | } | ||||||
| @@ -351,11 +351,12 @@ func (a *API) Get(ctx context.Context, manifestAddr storage.Address, path string | |||||||
| 		// 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 { | ||||||
|  |  | ||||||
| 			// get the resource root chunk key | 			// get the resource rootAddr | ||||||
| 			log.Trace("resource type", "key", manifestAddr, "hash", entry.Hash) | 			log.Trace("resource type", "menifestAddr", manifestAddr, "hash", entry.Hash) | ||||||
| 			ctx, cancel := context.WithCancel(context.Background()) | 			ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 			defer cancel() | 			defer cancel() | ||||||
| 			rsrc, err := a.resource.Load(ctx, storage.Address(common.FromHex(entry.Hash))) | 			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 | ||||||
| @@ -364,7 +365,8 @@ func (a *API) Get(ctx context.Context, manifestAddr storage.Address, path string | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// use this key to retrieve the latest update | 			// use this key to retrieve the latest update | ||||||
| 			rsrc, err = a.resource.LookupLatest(ctx, rsrc.NameHash(), true, &mru.LookupParams{}) | 			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 | ||||||
| @@ -374,10 +376,10 @@ func (a *API) Get(ctx context.Context, manifestAddr storage.Address, path string | |||||||
|  |  | ||||||
| 			// if it's multihash, we will transparently serve the content this multihash points to | 			// if it's multihash, we will transparently serve the content this multihash points to | ||||||
| 			// \TODO this resolve is rather expensive all in all, review to see if it can be achieved cheaper | 			// \TODO this resolve is rather expensive all in all, review to see if it can be achieved cheaper | ||||||
| 			if rsrc.Multihash { | 			if rsrc.Multihash() { | ||||||
|  |  | ||||||
| 				// get the data of the update | 				// get the data of the update | ||||||
| 				_, rsrcData, err := a.resource.GetContent(rsrc.NameHash().Hex()) | 				_, rsrcData, err := a.resource.GetContent(rootAddr) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					apiGetNotFound.Inc(1) | 					apiGetNotFound.Inc(1) | ||||||
| 					status = http.StatusNotFound | 					status = http.StatusNotFound | ||||||
| @@ -888,66 +890,39 @@ func (a *API) BuildDirectoryTree(ctx context.Context, mhash string, nameresolver | |||||||
| 	return addr, manifestEntryMap, nil | 	return addr, manifestEntryMap, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // ResourceLookup Looks up 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, addr storage.Address, period uint32, version uint32, maxLookup *mru.LookupParams) (string, []byte, error) { | func (a *API) ResourceLookup(ctx context.Context, params *mru.LookupParams) (string, []byte, error) { | ||||||
| 	var err error | 	var err error | ||||||
| 	rsrc, err := a.resource.Load(ctx, addr) | 	rsrc, err := a.resource.Load(ctx, params.RootAddr()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", nil, err | 		return "", nil, err | ||||||
| 	} | 	} | ||||||
| 	if version != 0 { | 	_, err = a.resource.Lookup(ctx, params) | ||||||
| 		if period == 0 { |  | ||||||
| 			return "", nil, mru.NewError(mru.ErrInvalidValue, "Period can't be 0") |  | ||||||
| 		} |  | ||||||
| 		_, err = a.resource.LookupVersion(ctx, rsrc.NameHash(), period, version, true, maxLookup) |  | ||||||
| 	} else if period != 0 { |  | ||||||
| 		_, err = a.resource.LookupHistorical(ctx, rsrc.NameHash(), period, true, maxLookup) |  | ||||||
| 	} else { |  | ||||||
| 		_, err = a.resource.LookupLatest(ctx, rsrc.NameHash(), true, maxLookup) |  | ||||||
| 	} |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", nil, err | 		return "", nil, err | ||||||
| 	} | 	} | ||||||
| 	var data []byte | 	var data []byte | ||||||
| 	_, data, err = a.resource.GetContent(rsrc.NameHash().Hex()) | 	_, data, err = a.resource.GetContent(params.RootAddr()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", nil, err | 		return "", nil, err | ||||||
| 	} | 	} | ||||||
| 	return rsrc.Name(), data, nil | 	return rsrc.Name(), data, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // ResourceCreate creates Resource and returns its key | // Create Mutable resource | ||||||
| func (a *API) ResourceCreate(ctx context.Context, name string, frequency uint64) (storage.Address, error) { | func (a *API) ResourceCreate(ctx context.Context, request *mru.Request) error { | ||||||
| 	key, _, err := a.resource.New(ctx, name, frequency) | 	return a.resource.New(ctx, request) | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return key, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // ResourceUpdateMultihash updates a Mutable Resource and marks the update's content to be of multihash type, which will be recognized upon retrieval. | // ResourceNewRequest creates a Request object to update a specific mutable resource | ||||||
| // It will fail if the data is not a valid multihash. | func (a *API) ResourceNewRequest(ctx context.Context, rootAddr storage.Address) (*mru.Request, error) { | ||||||
| func (a *API) ResourceUpdateMultihash(ctx context.Context, name string, data []byte) (storage.Address, uint32, uint32, error) { | 	return a.resource.NewUpdateRequest(ctx, rootAddr) | ||||||
| 	return a.resourceUpdate(ctx, name, data, true) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 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, name string, data []byte) (storage.Address, uint32, uint32, error) { | func (a *API) ResourceUpdate(ctx context.Context, request *mru.SignedResourceUpdate) (storage.Address, error) { | ||||||
| 	return a.resourceUpdate(ctx, name, data, false) | 	return a.resource.Update(ctx, request) | ||||||
| } |  | ||||||
|  |  | ||||||
| func (a *API) resourceUpdate(ctx context.Context, name string, data []byte, multihash bool) (storage.Address, uint32, uint32, error) { |  | ||||||
| 	var addr storage.Address |  | ||||||
| 	var err error |  | ||||||
| 	if multihash { |  | ||||||
| 		addr, err = a.resource.UpdateMultihash(ctx, name, data) |  | ||||||
| 	} else { |  | ||||||
| 		addr, err = a.resource.Update(ctx, name, data) |  | ||||||
| 	} |  | ||||||
| 	period, _ := a.resource.GetLastPeriod(name) |  | ||||||
| 	version, _ := a.resource.GetVersion(name) |  | ||||||
| 	return addr, period, version, err |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // ResourceHashSize returned the size of the digest produced by the Mutable Resource hashing function | // ResourceHashSize returned the size of the digest produced by the Mutable Resource hashing function | ||||||
| @@ -955,11 +930,6 @@ func (a *API) ResourceHashSize() int { | |||||||
| 	return a.resource.HashSize | 	return a.resource.HashSize | ||||||
| } | } | ||||||
|  |  | ||||||
| // ResourceIsValidated checks if the Mutable Resource has an active content validator. |  | ||||||
| func (a *API) ResourceIsValidated() bool { |  | ||||||
| 	return a.resource.IsValidated() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ResolveResourceManifest retrieves the Mutable Resource manifest for the given address, and returns the address of the metadata chunk. | // ResolveResourceManifest retrieves the Mutable Resource manifest for the given address, and returns the address of the metadata chunk. | ||||||
| func (a *API) ResolveResourceManifest(ctx context.Context, addr storage.Address) (storage.Address, error) { | func (a *API) ResolveResourceManifest(ctx context.Context, addr storage.Address) (storage.Address, error) { | ||||||
| 	trie, err := loadManifest(ctx, a.fileStore, addr, nil) | 	trie, err := loadManifest(ctx, a.fileStore, addr, nil) | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/api" | 	"github.com/ethereum/go-ethereum/swarm/api" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage/mru" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -562,3 +563,89 @@ func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) | |||||||
| 	} | 	} | ||||||
| 	return string(data), nil | 	return string(data), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 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. | ||||||
|  | // startTime=0 means "now" | ||||||
|  | // Returns the resulting Mutable Resource manifest address that you can use to include in an ENS Resolver (setContent) | ||||||
|  | // or reference future updates (Client.UpdateResource) | ||||||
|  | func (c *Client) CreateResource(request *mru.Request) (string, error) { | ||||||
|  | 	responseStream, err := c.updateResource(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	defer responseStream.Close() | ||||||
|  |  | ||||||
|  | 	body, err := ioutil.ReadAll(responseStream) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var manifestAddress string | ||||||
|  | 	if err = json.Unmarshal(body, &manifestAddress); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return manifestAddress, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UpdateResource allows you to set a new version of your content | ||||||
|  | func (c *Client) UpdateResource(request *mru.Request) error { | ||||||
|  | 	_, err := c.updateResource(request) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Client) updateResource(request *mru.Request) (io.ReadCloser, error) { | ||||||
|  | 	body, err := request.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req, err := http.NewRequest("POST", c.Gateway+"/bzz-resource:/", bytes.NewBuffer(body)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res, err := http.DefaultClient.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res.Body, nil | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  | func (c *Client) GetResource(manifestAddressOrDomain string) (io.ReadCloser, error) { | ||||||
|  |  | ||||||
|  | 	res, err := http.Get(c.Gateway + "/bzz-resource:/" + manifestAddressOrDomain) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return res.Body, nil | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetResourceMetadata returns a structure that describes the Mutable Resource | ||||||
|  | // manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver | ||||||
|  | // points to that address | ||||||
|  | func (c *Client) GetResourceMetadata(manifestAddressOrDomain string) (*mru.Request, error) { | ||||||
|  |  | ||||||
|  | 	responseStream, err := c.GetResource(manifestAddressOrDomain + "/meta") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer responseStream.Close() | ||||||
|  |  | ||||||
|  | 	body, err := ioutil.ReadAll(responseStream) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var metadata mru.Request | ||||||
|  | 	if err := metadata.UnmarshalJSON(body); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &metadata, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -25,8 +25,12 @@ import ( | |||||||
| 	"sort" | 	"sort" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common" | ||||||
|  | 	"github.com/ethereum/go-ethereum/crypto" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/api" | 	"github.com/ethereum/go-ethereum/swarm/api" | ||||||
| 	swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http" | 	swarmhttp "github.com/ethereum/go-ethereum/swarm/api/http" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/multihash" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage/mru" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/testutil" | 	"github.com/ethereum/go-ethereum/swarm/testutil" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -354,3 +358,159 @@ func TestClientMultipartUpload(t *testing.T) { | |||||||
| 		checkDownloadFile(file) | 		checkDownloadFile(file) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func newTestSigner() (*mru.GenericSigner, error) { | ||||||
|  | 	privKey, err := crypto.HexToECDSA("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return mru.NewGenericSigner(privKey), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // test the transparent resolving of multihash resource types with bzz:// scheme | ||||||
|  | // | ||||||
|  | // first upload data, and store the multihash to the resulting manifest in a resource update | ||||||
|  | // retrieving the update with the multihash should return the manifest pointing directly to the data | ||||||
|  | // and raw retrieve of that hash should return the data | ||||||
|  | func TestClientCreateResourceMultihash(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	signer, _ := newTestSigner() | ||||||
|  |  | ||||||
|  | 	srv := testutil.NewTestSwarmServer(t, serverFunc) | ||||||
|  | 	client := NewClient(srv.URL) | ||||||
|  | 	defer srv.Close() | ||||||
|  |  | ||||||
|  | 	// add the data our multihash aliased manifest will point to | ||||||
|  | 	databytes := []byte("bar") | ||||||
|  |  | ||||||
|  | 	swarmHash, err := client.UploadRaw(bytes.NewReader(databytes), int64(len(databytes)), false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error uploading raw test data: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s := common.FromHex(swarmHash) | ||||||
|  | 	mh := multihash.ToMultihash(s) | ||||||
|  |  | ||||||
|  | 	// our mutable resource "name" | ||||||
|  | 	resourceName := "foo.eth" | ||||||
|  |  | ||||||
|  | 	createRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ | ||||||
|  | 		Name:      resourceName, | ||||||
|  | 		Frequency: 13, | ||||||
|  | 		StartTime: srv.GetCurrentTime(), | ||||||
|  | 		Owner:     signer.Address(), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	createRequest.SetData(mh, true) | ||||||
|  | 	if err := createRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatalf("Error signing update: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resourceManifestHash, err := client.CreateResource(createRequest) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error creating resource: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" | ||||||
|  | 	if resourceManifestHash != correctManifestAddrHex { | ||||||
|  | 		t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reader, err := client.GetResource(correctManifestAddrHex) | ||||||
|  | 	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(mh, gotData) { | ||||||
|  | 		t.Fatalf("Expected: %v, got %v", mh, gotData) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestClientCreateUpdateResource will check that mutable resources can be created and updated via the HTTP client. | ||||||
|  | func TestClientCreateUpdateResource(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	signer, _ := newTestSigner() | ||||||
|  |  | ||||||
|  | 	srv := testutil.NewTestSwarmServer(t, serverFunc) | ||||||
|  | 	client := NewClient(srv.URL) | ||||||
|  | 	defer srv.Close() | ||||||
|  |  | ||||||
|  | 	// set raw data for the resource | ||||||
|  | 	databytes := []byte("En un lugar de La Mancha, de cuyo nombre no quiero acordarme...") | ||||||
|  |  | ||||||
|  | 	// our mutable resource name | ||||||
|  | 	resourceName := "El Quijote" | ||||||
|  |  | ||||||
|  | 	createRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ | ||||||
|  | 		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 { | ||||||
|  | 		t.Fatalf("Error signing update: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resourceManifestHash, err := client.CreateResource(createRequest) | ||||||
|  |  | ||||||
|  | 	correctManifestAddrHex := "cc7904c17b49f9679e2d8006fe25e87e3f5c2072c2b49cab50f15e544471b30a" | ||||||
|  | 	if resourceManifestHash != correctManifestAddrHex { | ||||||
|  | 		t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, resourceManifestHash) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reader, err := client.GetResource(correctManifestAddrHex) | ||||||
|  | 	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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// define different data | ||||||
|  | 	databytes = []byte("... no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero ...") | ||||||
|  |  | ||||||
|  | 	updateRequest, err := client.GetResourceMetadata(correctManifestAddrHex) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error retrieving update request template: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	updateRequest.SetData(databytes, false) | ||||||
|  | 	if err := updateRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatalf("Error signing update: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = client.UpdateResource(updateRequest); err != nil { | ||||||
|  | 		t.Fatalf("Error updating resource: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reader, err = client.GetResource(correctManifestAddrHex) | ||||||
|  | 	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) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|   | |||||||
| @@ -38,7 +38,6 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/ethereum/go-ethereum/common" | 	"github.com/ethereum/go-ethereum/common" | ||||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" |  | ||||||
| 	"github.com/ethereum/go-ethereum/metrics" | 	"github.com/ethereum/go-ethereum/metrics" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/api" | 	"github.com/ethereum/go-ethereum/swarm/api" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/log" | 	"github.com/ethereum/go-ethereum/swarm/log" | ||||||
| @@ -518,9 +517,8 @@ func resourcePostMode(path string) (isRaw bool, frequency uint64, err error) { | |||||||
| // If the latter is used, a subsequent bzz:// GET call to the manifest of the resource will return | // If the latter is used, a subsequent bzz:// GET call to the manifest of the resource will return | ||||||
| // the page that the multihash is pointing to, as if it held a normal swarm content manifest | // the page that the multihash is pointing to, as if it held a normal swarm content manifest | ||||||
| // | // | ||||||
| // The resource name will be verbatim what is passed as the address part of the url. | // The POST request admits a JSON structure as defined in the mru package: `mru.updateRequestJSON` | ||||||
| // For example, if a POST is made to /bzz-resource:/foo.eth/raw/13 a new resource with frequency 13 | // 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 | ||||||
| // and name "foo.eth" will be created |  | ||||||
| func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { | func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { | ||||||
| 	log.Debug("handle.post.resource", "ruid", r.ruid) | 	log.Debug("handle.post.resource", "ruid", r.ruid) | ||||||
|  |  | ||||||
| @@ -532,33 +530,54 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { | |||||||
| 	defer sp.Finish() | 	defer sp.Finish() | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	var addr storage.Address |  | ||||||
| 	var name string | 	// Creation and update must send mru.updateRequestJSON JSON structure | ||||||
| 	var outdata []byte | 	body, err := ioutil.ReadAll(r.Body) | ||||||
| 	isRaw, frequency, err := resourcePostMode(r.uri.Path) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		Respond(w, r, err.Error(), http.StatusBadRequest) | 		Respond(w, r, err.Error(), http.StatusInternalServerError) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	var updateRequest mru.Request | ||||||
|  | 	if err := updateRequest.UnmarshalJSON(body); err != nil { // decodes request JSON | ||||||
|  | 		Respond(w, r, err.Error(), http.StatusBadRequest) //TODO: send different status response depending on error | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// new mutable resource creation will always have a frequency field larger than 0 | 	if updateRequest.IsUpdate() { | ||||||
| 	if frequency > 0 { | 		// Verify that the signature is intact and that the signer is authorized | ||||||
|  | 		// to update this resource | ||||||
|  | 		// Check this early, to avoid creating a resource and then not being able to set its first update. | ||||||
|  | 		if err = updateRequest.Verify(); err != nil { | ||||||
|  | 			Respond(w, r, err.Error(), http.StatusForbidden) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		name = r.uri.Addr | 	if updateRequest.IsNew() { | ||||||
|  | 		err = s.api.ResourceCreate(r.Context(), &updateRequest) | ||||||
| 		// the key is the content addressed root chunk holding mutable resource metadata information |  | ||||||
| 		addr, err = s.api.ResourceCreate(ctx, name, frequency) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			code, err2 := s.translateResourceError(w, r, "resource creation fail", err) | 			code, err2 := s.translateResourceError(w, r, "resource creation fail", err) | ||||||
|  |  | ||||||
| 			Respond(w, r, err2.Error(), code) | 			Respond(w, r, err2.Error(), code) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if updateRequest.IsUpdate() { | ||||||
|  | 		_, err = s.api.ResourceUpdate(r.Context(), &updateRequest.SignedResourceUpdate) | ||||||
|  | 		if err != nil { | ||||||
|  | 			Respond(w, r, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// at this point both possible operations (create, update or both) were successful | ||||||
|  | 	// 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 its hash is the key of the mutable resource | ||||||
| 		// root chunk | 		// metadata chunk (rootAddr) | ||||||
| 		m, err := s.api.NewResourceManifest(r.Context(), addr.Hex()) | 		m, err := s.api.NewResourceManifest(r.Context(), updateRequest.RootAddr().Hex()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			Respond(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError) | 			Respond(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| @@ -568,85 +587,21 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { | |||||||
| 		// the client can access the root chunk key directly through its Hash member | 		// the client can access the root chunk key directly through its Hash member | ||||||
| 		// the manifest key should be set as content in the resolver of the ENS name | 		// the manifest key should be set as content in the resolver of the ENS name | ||||||
| 		// \TODO update manifest key automatically in ENS | 		// \TODO update manifest key automatically in ENS | ||||||
| 		outdata, err = json.Marshal(m) | 		outdata, err := json.Marshal(m) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			Respond(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) | 			Respond(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} else { |  | ||||||
| 		// to update the resource through http we need to retrieve the key for the mutable resource root chunk |  | ||||||
| 		// that means that we retrieve the manifest and inspect its Hash member. |  | ||||||
| 		manifestAddr := r.uri.Address() |  | ||||||
| 		if manifestAddr == nil { |  | ||||||
| 			manifestAddr, err = s.api.Resolve(r.Context(), r.uri) |  | ||||||
| 			if err != nil { |  | ||||||
| 				getFail.Inc(1) |  | ||||||
| 				Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusNotFound) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			w.Header().Set("Cache-Control", "max-age=2147483648") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// get the root chunk key from the manifest |  | ||||||
| 		addr, err = s.api.ResolveResourceManifest(r.Context(), manifestAddr) |  | ||||||
| 		if err != nil { |  | ||||||
| 			getFail.Inc(1) |  | ||||||
| 			Respond(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", r.uri.Addr, err), http.StatusNotFound) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		log.Debug("handle.post.resource: resolved", "ruid", r.ruid, "manifestkey", manifestAddr, "rootchunkkey", addr) |  | ||||||
|  |  | ||||||
| 		name, _, err = s.api.ResourceLookup(ctx, addr, 0, 0, &mru.LookupParams{}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			Respond(w, r, err.Error(), http.StatusNotFound) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Creation and update must send data aswell. This data constitutes the update data itself. |  | ||||||
| 	data, err := ioutil.ReadAll(r.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		Respond(w, r, err.Error(), http.StatusInternalServerError) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Multihash will be passed as hex-encoded data, so we need to parse this to bytes |  | ||||||
| 	if isRaw { |  | ||||||
| 		_, _, _, err = s.api.ResourceUpdate(ctx, name, data) |  | ||||||
| 		if err != nil { |  | ||||||
| 			Respond(w, r, err.Error(), http.StatusBadRequest) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		bytesdata, err := hexutil.Decode(string(data)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			Respond(w, r, err.Error(), http.StatusBadRequest) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		_, _, _, err = s.api.ResourceUpdateMultihash(ctx, name, bytesdata) |  | ||||||
| 		if err != nil { |  | ||||||
| 			Respond(w, r, err.Error(), http.StatusBadRequest) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If we have data to return, write this now |  | ||||||
| 	// \TODO there should always be data to return here |  | ||||||
| 	if len(outdata) > 0 { |  | ||||||
| 		w.Header().Add("Content-type", "text/plain") |  | ||||||
| 		w.WriteHeader(http.StatusOK) |  | ||||||
| 		fmt.Fprint(w, string(outdata)) | 		fmt.Fprint(w, string(outdata)) | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
| 	w.WriteHeader(http.StatusOK) | 	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>/<n> - get latest update on period n | ||||||
| // bzz-resource://<id>/<n>/<m> - get update version m of period n | // bzz-resource://<id>/<n>/<m> - get update version m of period n | ||||||
|  | // 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 | ||||||
| func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) { | func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) { | ||||||
| @@ -666,31 +621,51 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) { | |||||||
| 		w.Header().Set("Cache-Control", "max-age=2147483648") | 		w.Header().Set("Cache-Control", "max-age=2147483648") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// get the root chunk key from the manifest | 	// get the root chunk rootAddr from the manifest | ||||||
| 	key, err := s.api.ResolveResourceManifest(r.Context(), manifestAddr) | 	rootAddr, err := s.api.ResolveResourceManifest(r.Context(), manifestAddr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		getFail.Inc(1) | 		getFail.Inc(1) | ||||||
| 		Respond(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", r.uri.Addr, err), http.StatusNotFound) | 		Respond(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", r.uri.Addr, err), http.StatusNotFound) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug("handle.get.resource: resolved", "ruid", r.ruid, "manifestkey", manifestAddr, "rootchunk key", key) | 	log.Debug("handle.get.resource: resolved", "ruid", r.ruid, "manifestkey", manifestAddr, "rootchunk addr", rootAddr) | ||||||
|  |  | ||||||
| 	// determine if the query specifies period and version | 	// determine if the query specifies period and version or it is a metadata query | ||||||
| 	var params []string | 	var params []string | ||||||
| 	if len(r.uri.Path) > 0 { | 	if len(r.uri.Path) > 0 { | ||||||
|  | 		if r.uri.Path == "meta" { | ||||||
|  | 			unsignedUpdateRequest, err := s.api.ResourceNewRequest(r.Context(), rootAddr) | ||||||
|  | 			if err != nil { | ||||||
|  | 				getFail.Inc(1) | ||||||
|  | 				Respond(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 { | ||||||
|  | 				Respond(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 | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		params = strings.Split(r.uri.Path, "/") | 		params = strings.Split(r.uri.Path, "/") | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
| 	var name string | 	var name string | ||||||
| 	var period uint64 |  | ||||||
| 	var version uint64 |  | ||||||
| 	var data []byte | 	var data []byte | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
|  |  | ||||||
| 	switch len(params) { | 	switch len(params) { | ||||||
| 	case 0: // latest only | 	case 0: // latest only | ||||||
| 		name, data, err = s.api.ResourceLookup(r.Context(), key, 0, 0, nil) | 		name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupLatest(rootAddr)) | ||||||
| 	case 2: // specific period and version | 	case 2: // specific period and version | ||||||
|  | 		var version uint64 | ||||||
|  | 		var period uint64 | ||||||
| 		version, err = strconv.ParseUint(params[1], 10, 32) | 		version, err = strconv.ParseUint(params[1], 10, 32) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			break | 			break | ||||||
| @@ -699,13 +674,14 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		name, data, err = s.api.ResourceLookup(r.Context(), key, uint32(period), uint32(version), nil) | 		name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupVersion(rootAddr, uint32(period), uint32(version))) | ||||||
| 	case 1: // last version of specific period | 	case 1: // last version of specific period | ||||||
|  | 		var period uint64 | ||||||
| 		period, err = strconv.ParseUint(params[0], 10, 32) | 		period, err = strconv.ParseUint(params[0], 10, 32) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		name, data, err = s.api.ResourceLookup(r.Context(), key, uint32(period), uint32(version), nil) | 		name, data, err = s.api.ResourceLookup(r.Context(), mru.LookupLatestVersionInPeriod(rootAddr, uint32(period))) | ||||||
| 	default: // bogus | 	default: // bogus | ||||||
| 		err = mru.NewError(storage.ErrInvalidValue, "invalid mutable resource request") | 		err = mru.NewError(storage.ErrInvalidValue, "invalid mutable resource request") | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -34,12 +34,13 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/ethereum/go-ethereum/common" | 	"github.com/ethereum/go-ethereum/common" | ||||||
| 	"github.com/ethereum/go-ethereum/common/hexutil" | 	"github.com/ethereum/go-ethereum/crypto" | ||||||
| 	"github.com/ethereum/go-ethereum/log" | 	"github.com/ethereum/go-ethereum/log" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/api" | 	"github.com/ethereum/go-ethereum/swarm/api" | ||||||
| 	swarm "github.com/ethereum/go-ethereum/swarm/api/client" | 	swarm "github.com/ethereum/go-ethereum/swarm/api/client" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/multihash" | 	"github.com/ethereum/go-ethereum/swarm/multihash" | ||||||
| 	"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/testutil" | 	"github.com/ethereum/go-ethereum/swarm/testutil" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -94,6 +95,14 @@ func serverFunc(api *api.API) testutil.TestServer { | |||||||
| 	return NewServer(api, "") | 	return NewServer(api, "") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func newTestSigner() (*mru.GenericSigner, error) { | ||||||
|  | 	privKey, err := crypto.HexToECDSA("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return mru.NewGenericSigner(privKey), nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // test the transparent resolving of multihash resource types with bzz:// scheme | // test the transparent resolving of multihash resource types with bzz:// scheme | ||||||
| // | // | ||||||
| // first upload data, and store the multihash to the resulting manifest in a resource update | // first upload data, and store the multihash to the resulting manifest in a resource update | ||||||
| @@ -101,6 +110,8 @@ func serverFunc(api *api.API) testutil.TestServer { | |||||||
| // and raw retrieve of that hash should return the data | // and raw retrieve of that hash should return the data | ||||||
| func TestBzzResourceMultihash(t *testing.T) { | func TestBzzResourceMultihash(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	signer, _ := newTestSigner() | ||||||
|  |  | ||||||
| 	srv := testutil.NewTestSwarmServer(t, serverFunc) | 	srv := testutil.NewTestSwarmServer(t, serverFunc) | ||||||
| 	defer srv.Close() | 	defer srv.Close() | ||||||
|  |  | ||||||
| @@ -123,15 +134,35 @@ func TestBzzResourceMultihash(t *testing.T) { | |||||||
| 	s := common.FromHex(string(b)) | 	s := common.FromHex(string(b)) | ||||||
| 	mh := multihash.ToMultihash(s) | 	mh := multihash.ToMultihash(s) | ||||||
|  |  | ||||||
| 	mhHex := hexutil.Encode(mh) |  | ||||||
| 	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" | 	// our mutable resource "name" | ||||||
| 	keybytes := "foo.eth" | 	keybytes := "foo.eth" | ||||||
|  |  | ||||||
|  | 	updateRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ | ||||||
|  | 		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 { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	log.Info("added data", "manifest", string(b), "data", common.ToHex(mh)) | ||||||
|  |  | ||||||
|  | 	body, err := updateRequest.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// create the multihash update | 	// create the multihash update | ||||||
| 	url = fmt.Sprintf("%s/bzz-resource:/%s/13", srv.URL, keybytes) | 	url = fmt.Sprintf("%s/bzz-resource:/", srv.URL) | ||||||
| 	resp, err = http.Post(url, "application/octet-stream", bytes.NewReader([]byte(mhHex))) | 	resp, err = http.Post(url, "application/json", bytes.NewReader(body)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @@ -149,9 +180,9 @@ 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 := "d689648fb9e00ddc7ebcf474112d5881c5bf7dbc6e394681b1d224b11b59b5e0" | 	correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" | ||||||
| 	if rsrcResp.Hex() != correctManifestAddrHex { | 	if rsrcResp.Hex() != correctManifestAddrHex { | ||||||
| 		t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp) | 		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 | ||||||
| @@ -176,6 +207,8 @@ func TestBzzResourceMultihash(t *testing.T) { | |||||||
| // Test resource updates using the raw update methods | // Test resource updates using the raw update methods | ||||||
| func TestBzzResource(t *testing.T) { | func TestBzzResource(t *testing.T) { | ||||||
| 	srv := testutil.NewTestSwarmServer(t, serverFunc) | 	srv := testutil.NewTestSwarmServer(t, serverFunc) | ||||||
|  | 	signer, _ := newTestSigner() | ||||||
|  |  | ||||||
| 	defer srv.Close() | 	defer srv.Close() | ||||||
|  |  | ||||||
| 	// our mutable resource "name" | 	// our mutable resource "name" | ||||||
| @@ -188,9 +221,29 @@ func TestBzzResource(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	updateRequest, err := mru.NewCreateUpdateRequest(&mru.ResourceMetadata{ | ||||||
|  | 		Name:      keybytes, | ||||||
|  | 		Frequency: 13, | ||||||
|  | 		StartTime: srv.GetCurrentTime(), | ||||||
|  | 		Owner:     signer.Address(), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	updateRequest.SetData(databytes, false) | ||||||
|  |  | ||||||
|  | 	if err := updateRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	body, err := updateRequest.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// creates resource and sets update 1 | 	// creates resource and sets update 1 | ||||||
| 	url := fmt.Sprintf("%s/bzz-resource:/%s/raw/13", srv.URL, []byte(keybytes)) | 	url := fmt.Sprintf("%s/bzz-resource:/", srv.URL) | ||||||
| 	resp, err := http.Post(url, "application/octet-stream", bytes.NewReader(databytes)) | 	resp, err := http.Post(url, "application/json", bytes.NewReader(body)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @@ -208,7 +261,7 @@ 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 := "d689648fb9e00ddc7ebcf474112d5881c5bf7dbc6e394681b1d224b11b59b5e0" | 	correctManifestAddrHex := "6d3bc4664c97d8b821cb74bcae43f592494fb46d2d9cd31e69f3c7c802bbbd8e" | ||||||
| 	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()) | ||||||
| 	} | 	} | ||||||
| @@ -235,8 +288,7 @@ 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" | ||||||
| 	correctRootKeyHex := "f667277e004e8486c7a3631fd226802430e84e9a81b6085d31f512a591ae0065" |  | ||||||
| 	if manifest.Entries[0].Hash != correctRootKeyHex { | 	if manifest.Entries[0].Hash != correctRootKeyHex { | ||||||
| 		t.Fatalf("Expected manifest path '%s', got '%s'", correctRootKeyHex, manifest.Entries[0].Hash) | 		t.Fatalf("Expected manifest path '%s', got '%s'", correctRootKeyHex, manifest.Entries[0].Hash) | ||||||
| 	} | 	} | ||||||
| @@ -262,6 +314,11 @@ func TestBzzResource(t *testing.T) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != http.StatusNotFound { | ||||||
|  | 		t.Fatalf("Expected get non-existent resource to fail with StatusNotFound (404), got %d", resp.StatusCode) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	resp.Body.Close() | 	resp.Body.Close() | ||||||
|  |  | ||||||
| 	// get latest update (1.1) through resource directly | 	// get latest update (1.1) through resource directly | ||||||
| @@ -285,9 +342,36 @@ func TestBzzResource(t *testing.T) { | |||||||
|  |  | ||||||
| 	// update 2 | 	// update 2 | ||||||
| 	log.Info("update 2") | 	log.Info("update 2") | ||||||
| 	url = fmt.Sprintf("%s/bzz-resource:/%s/raw", srv.URL, correctManifestAddrHex) |  | ||||||
|  | 	// 1.- get metadata about this resource | ||||||
|  | 	url = fmt.Sprintf("%s/bzz-resource:/%s/", srv.URL, correctManifestAddrHex) | ||||||
|  | 	resp, err = http.Get(url + "meta") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 	if resp.StatusCode != http.StatusOK { | ||||||
|  | 		t.Fatalf("Get resource metadata returned %s", resp.Status) | ||||||
|  | 	} | ||||||
|  | 	b, err = ioutil.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	updateRequest = &mru.Request{} | ||||||
|  | 	if err = updateRequest.UnmarshalJSON(b); err != nil { | ||||||
|  | 		t.Fatalf("Error decoding resource metadata: %s", err) | ||||||
|  | 	} | ||||||
| 	data := []byte("foo") | 	data := []byte("foo") | ||||||
| 	resp, err = http.Post(url, "application/octet-stream", bytes.NewReader(data)) | 	updateRequest.SetData(data, false) | ||||||
|  | 	if err = updateRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	body, err = updateRequest.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp, err = http.Post(url, "application/json", bytes.NewReader(body)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								swarm/storage/mru/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								swarm/storage/mru/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | // Package mru defines Mutable resource updates. | ||||||
|  | // A Mutable Resource is an entity which allows updates to a resource | ||||||
|  | // without resorting to ENS on each update. | ||||||
|  | // The update scheme is built on swarm chunks with chunk keys following | ||||||
|  | // a predictable, versionable pattern. | ||||||
|  | // | ||||||
|  | // Updates are defined to be periodic in nature, where the update frequency | ||||||
|  | // is expressed in seconds. | ||||||
|  | // | ||||||
|  | // The root entry of a mutable resource is tied to a unique identifier that | ||||||
|  | // is deterministically generated out of the metadata content that describes | ||||||
|  | // the resource. This metadata includes a user-defined resource name, a resource | ||||||
|  | // start time that indicates when the resource becomes valid, | ||||||
|  | // the frequency in seconds with which the resource is expected to be updated, both of | ||||||
|  | // which are stored as little-endian uint64 values in the database (for a | ||||||
|  | // total of 16 bytes). It also contains the owner's address (ownerAddr) | ||||||
|  | // This MRU info is stored in a separate content-addressed chunk | ||||||
|  | // (call it the metadata chunk), with the following layout: | ||||||
|  | // | ||||||
|  | // (00|length|startTime|frequency|name|ownerAddr) | ||||||
|  | // | ||||||
|  | // (The two first zero-value bytes are used for disambiguation by the chunk validator, | ||||||
|  | // and update chunk will always have a value > 0 there.) | ||||||
|  | // | ||||||
|  | // Each metadata chunk is identified by its rootAddr, calculated as follows: | ||||||
|  | // metaHash=H(len(metadata), startTime, frequency,name) | ||||||
|  | // rootAddr = H(metaHash, ownerAddr). | ||||||
|  | // where H is the SHA3 hash function | ||||||
|  | // This scheme effectively locks the root chunk so that only the owner of the private key | ||||||
|  | // that ownerAddr was derived from can sign updates. | ||||||
|  | // | ||||||
|  | // The root entry tells the requester from when the mutable resource was | ||||||
|  | // first added (Unix time in seconds) and in which moments to look for the | ||||||
|  | // actual updates. Thus, a resource update for identifier "føø.bar" | ||||||
|  | // starting at unix time 1528800000 with frequency 300 (every 5 mins) will have updates on 1528800300, | ||||||
|  | // 1528800600, 1528800900 and so on. | ||||||
|  | // | ||||||
|  | // Actual data updates are also made in the form of swarm chunks. The keys | ||||||
|  | // of the updates are the hash of a concatenation of properties as follows: | ||||||
|  | // | ||||||
|  | // updateAddr = H(period, version, rootAddr) | ||||||
|  | // where H is the SHA3 hash function | ||||||
|  | // The period is (currentTime - startTime) / frequency | ||||||
|  | // | ||||||
|  | // Using our previous example, this means that a period 3 will happen when the | ||||||
|  | // clock hits 1528800900 | ||||||
|  | // | ||||||
|  | // If more than one update is made in the same period, incremental | ||||||
|  | // version numbers are used successively. | ||||||
|  | // | ||||||
|  | // A user looking up a resource would only need to know the rootAddr in order to get the versions | ||||||
|  | // | ||||||
|  | // the resource update data is: | ||||||
|  | // resourcedata = headerlength|period|version|rootAddr|flags|metaHash | ||||||
|  | // where flags is a 1-byte flags field. Flag 0 is set to 1 to indicate multihash | ||||||
|  | // | ||||||
|  | // the full update data that goes in the chunk payload is: | ||||||
|  | // resourcedata|sign(resourcedata) | ||||||
|  | // | ||||||
|  | // headerlength is a 16 bit value containing the byte length of period|version|rootAddr|flags|metaHash | ||||||
|  | package mru | ||||||
| @@ -16,6 +16,10 @@ | |||||||
|  |  | ||||||
| package mru | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	ErrInit = iota | 	ErrInit = iota | ||||||
| 	ErrNotFound | 	ErrNotFound | ||||||
| @@ -30,3 +34,40 @@ const ( | |||||||
| 	ErrPeriodDepth | 	ErrPeriodDepth | ||||||
| 	ErrCnt | 	ErrCnt | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // Error is a the typed error object used for Mutable Resources | ||||||
|  | type Error struct { | ||||||
|  | 	code int | ||||||
|  | 	err  string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Error implements the error interface | ||||||
|  | func (e *Error) Error() string { | ||||||
|  | 	return e.err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Code returns the error code | ||||||
|  | // Error codes are enumerated in the error.go file within the mru package | ||||||
|  | func (e *Error) Code() int { | ||||||
|  | 	return e.code | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewError creates a new Mutable Resource Error object with the specified code and custom error message | ||||||
|  | func NewError(code int, s string) error { | ||||||
|  | 	if code < 0 || code >= ErrCnt { | ||||||
|  | 		panic("no such error code!") | ||||||
|  | 	} | ||||||
|  | 	r := &Error{ | ||||||
|  | 		err: s, | ||||||
|  | 	} | ||||||
|  | 	switch code { | ||||||
|  | 	case ErrNotFound, ErrIO, ErrUnauthorized, ErrInvalidValue, ErrDataOverflow, ErrNothingToReturn, ErrInvalidSignature, ErrNotSynced, ErrPeriodDepth, ErrCorruptData: | ||||||
|  | 		r.code = code | ||||||
|  | 	} | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewErrorf is a convenience version of NewError that incorporates printf-style formatting | ||||||
|  | func NewErrorf(code int, format string, args ...interface{}) error { | ||||||
|  | 	return NewError(code, fmt.Sprintf(format, args...)) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										514
									
								
								swarm/storage/mru/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								swarm/storage/mru/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,514 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | // Handler is the API for Mutable Resources | ||||||
|  | // It enables creating, updating, syncing and retrieving resources and their update data | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 	"unsafe" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/log" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const chunkSize = 4096 // temporary until we implement FileStore in the resourcehandler | ||||||
|  |  | ||||||
|  | type Handler struct { | ||||||
|  | 	chunkStore      *storage.NetStore | ||||||
|  | 	HashSize        int | ||||||
|  | 	resources       map[uint64]*resource | ||||||
|  | 	resourceLock    sync.RWMutex | ||||||
|  | 	storeTimeout    time.Duration | ||||||
|  | 	queryMaxPeriods uint32 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HandlerParams pass parameters to the Handler constructor NewHandler | ||||||
|  | // Signer and TimestampProvider are mandatory parameters | ||||||
|  | type HandlerParams struct { | ||||||
|  | 	QueryMaxPeriods uint32 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // hashPool contains a pool of ready hashers | ||||||
|  | var hashPool sync.Pool | ||||||
|  | var minimumChunkLength int | ||||||
|  |  | ||||||
|  | // init initializes the package and hashPool | ||||||
|  | func init() { | ||||||
|  | 	hashPool = sync.Pool{ | ||||||
|  | 		New: func() interface{} { | ||||||
|  | 			return storage.MakeHashFunc(resourceHashAlgorithm)() | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	if minimumMetadataLength < minimumUpdateDataLength { | ||||||
|  | 		minimumChunkLength = minimumMetadataLength | ||||||
|  | 	} else { | ||||||
|  | 		minimumChunkLength = minimumUpdateDataLength | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewHandler creates a new Mutable Resource API | ||||||
|  | func NewHandler(params *HandlerParams) (*Handler, error) { | ||||||
|  |  | ||||||
|  | 	rh := &Handler{ | ||||||
|  | 		resources:       make(map[uint64]*resource), | ||||||
|  | 		storeTimeout:    defaultStoreTimeout, | ||||||
|  | 		queryMaxPeriods: params.QueryMaxPeriods, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := 0; i < hasherCount; i++ { | ||||||
|  | 		hashfunc := storage.MakeHashFunc(resourceHashAlgorithm)() | ||||||
|  | 		if rh.HashSize == 0 { | ||||||
|  | 			rh.HashSize = hashfunc.Size() | ||||||
|  | 		} | ||||||
|  | 		hashPool.Put(hashfunc) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return rh, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetStore sets the store backend for the Mutable Resource API | ||||||
|  | func (h *Handler) SetStore(store *storage.NetStore) { | ||||||
|  | 	h.chunkStore = store | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Validate is a chunk validation method | ||||||
|  | // If it looks like a resource update, the chunk address is checked against the ownerAddr of the update's signature | ||||||
|  | // It implements the storage.ChunkValidator interface | ||||||
|  | func (h *Handler) Validate(chunkAddr storage.Address, data []byte) bool { | ||||||
|  |  | ||||||
|  | 	dataLength := len(data) | ||||||
|  | 	if dataLength < minimumChunkLength { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//metadata chunks have the first two bytes set to zero | ||||||
|  | 	if data[0] == 0 && data[1] == 0 && dataLength >= minimumMetadataLength { | ||||||
|  | 		//metadata chunk | ||||||
|  | 		rootAddr, _ := metadataHash(data) | ||||||
|  | 		valid := bytes.Equal(chunkAddr, rootAddr) | ||||||
|  | 		if !valid { | ||||||
|  | 			log.Debug(fmt.Sprintf("Invalid root metadata chunk with address: %s", chunkAddr.Hex())) | ||||||
|  | 		} | ||||||
|  | 		return valid | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// if it is not a metadata chunk, check if it is a properly formatted update chunk with | ||||||
|  | 	// valid signature and proof of ownership of the resource it is trying | ||||||
|  | 	// to update | ||||||
|  |  | ||||||
|  | 	// First, deserialize the chunk | ||||||
|  | 	var r SignedResourceUpdate | ||||||
|  | 	if err := r.fromChunk(chunkAddr, data); err != nil { | ||||||
|  | 		log.Debug("Invalid resource chunk with address %s: %s ", chunkAddr.Hex(), err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check that the lookup information contained in the chunk matches the updateAddr (chunk search key) | ||||||
|  | 	// that was used to retrieve this chunk | ||||||
|  | 	// if this validation fails, someone forged a chunk. | ||||||
|  | 	if !bytes.Equal(chunkAddr, r.updateHeader.UpdateAddr()) { | ||||||
|  | 		log.Debug("period,version,rootAddr contained in update chunk do not match updateAddr %s", chunkAddr.Hex()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Verify signatures and that the signer actually owns the resource | ||||||
|  | 	// If it fails, it means either the signature is not valid, data is corrupted | ||||||
|  | 	// or someone is trying to update someone else's resource. | ||||||
|  | 	if err := r.Verify(); err != nil { | ||||||
|  | 		log.Debug("Invalid signature: %v", err) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetContent retrieves the data payload of the last synced update of the Mutable Resource | ||||||
|  | func (h *Handler) GetContent(rootAddr storage.Address) (storage.Address, []byte, error) { | ||||||
|  | 	rsrc := h.get(rootAddr) | ||||||
|  | 	if rsrc == nil || !rsrc.isSynced() { | ||||||
|  | 		return nil, nil, NewError(ErrNotFound, " does not exist or is not synced") | ||||||
|  | 	} | ||||||
|  | 	return rsrc.lastKey, rsrc.data, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetLastPeriod retrieves the period of the last synced update of the Mutable Resource | ||||||
|  | func (h *Handler) GetLastPeriod(rootAddr storage.Address) (uint32, error) { | ||||||
|  | 	rsrc := h.get(rootAddr) | ||||||
|  | 	if rsrc == nil { | ||||||
|  | 		return 0, NewError(ErrNotFound, " does not exist") | ||||||
|  | 	} else if !rsrc.isSynced() { | ||||||
|  | 		return 0, NewError(ErrNotSynced, " is not synced") | ||||||
|  | 	} | ||||||
|  | 	return rsrc.period, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetVersion retrieves the period of the last synced update of the Mutable Resource | ||||||
|  | func (h *Handler) GetVersion(rootAddr storage.Address) (uint32, error) { | ||||||
|  | 	rsrc := h.get(rootAddr) | ||||||
|  | 	if rsrc == nil { | ||||||
|  | 		return 0, NewError(ErrNotFound, " does not exist") | ||||||
|  | 	} else if !rsrc.isSynced() { | ||||||
|  | 		return 0, NewError(ErrNotSynced, " is not synced") | ||||||
|  | 	} | ||||||
|  | 	return rsrc.version, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // \TODO should be hashsize * branches from the chosen chunker, implement with FileStore | ||||||
|  | func (h *Handler) chunkSize() int64 { | ||||||
|  | 	return chunkSize | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // New creates a new metadata chunk out of the request passed in. | ||||||
|  | func (h *Handler) New(ctx context.Context, request *Request) error { | ||||||
|  |  | ||||||
|  | 	// frequency 0 is invalid | ||||||
|  | 	if request.metadata.Frequency == 0 { | ||||||
|  | 		return NewError(ErrInvalidValue, "frequency cannot be 0 when creating a resource") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// make sure owner is set to something | ||||||
|  | 	if request.metadata.Owner == zeroAddr { | ||||||
|  | 		return NewError(ErrInvalidValue, "ownerAddr must be set to create a new metadata chunk") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// create the meta chunk and store it in swarm | ||||||
|  | 	chunk, metaHash, err := request.metadata.newChunk() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if request.metaHash != nil && !bytes.Equal(request.metaHash, metaHash) || | ||||||
|  | 		request.rootAddr != nil && !bytes.Equal(request.rootAddr, chunk.Addr) { | ||||||
|  | 		return NewError(ErrInvalidValue, "metaHash in UpdateRequest does not match actual metadata") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	request.metaHash = metaHash | ||||||
|  | 	request.rootAddr = chunk.Addr | ||||||
|  |  | ||||||
|  | 	h.chunkStore.Put(ctx, chunk) | ||||||
|  | 	log.Debug("new resource", "name", request.metadata.Name, "startTime", request.metadata.StartTime, "frequency", request.metadata.Frequency, "owner", request.metadata.Owner) | ||||||
|  |  | ||||||
|  | 	// create the internal index for the resource and populate it with its metadata | ||||||
|  | 	rsrc := &resource{ | ||||||
|  | 		resourceUpdate: resourceUpdate{ | ||||||
|  | 			updateHeader: updateHeader{ | ||||||
|  | 				UpdateLookup: UpdateLookup{ | ||||||
|  | 					rootAddr: chunk.Addr, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		ResourceMetadata: request.metadata, | ||||||
|  | 		updated:          time.Now(), | ||||||
|  | 	} | ||||||
|  | 	h.set(chunk.Addr, rsrc) | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewUpdateRequest prepares an UpdateRequest structure with all the necessary information to | ||||||
|  | // just add the desired data and sign it. | ||||||
|  | // The resulting structure can then be signed and passed to Handler.Update to be verified and sent | ||||||
|  | func (h *Handler) NewUpdateRequest(ctx context.Context, rootAddr storage.Address) (updateRequest *Request, err error) { | ||||||
|  |  | ||||||
|  | 	if rootAddr == nil { | ||||||
|  | 		return nil, NewError(ErrInvalidValue, "rootAddr cannot be nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Make sure we have a cache of the metadata chunk | ||||||
|  | 	rsrc, err := h.Load(ctx, rootAddr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	now := TimestampProvider.Now() | ||||||
|  |  | ||||||
|  | 	updateRequest = new(Request) | ||||||
|  | 	updateRequest.period, err = getNextPeriod(rsrc.StartTime.Time, now.Time, rsrc.Frequency) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if _, err = h.lookup(rsrc, LookupLatestVersionInPeriod(rsrc.rootAddr, updateRequest.period)); err != nil { | ||||||
|  | 		if err.(*Error).code != ErrNotFound { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		// not finding updates means that there is a network error | ||||||
|  | 		// or that the resource really does not have updates in this period. | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	updateRequest.multihash = rsrc.multihash | ||||||
|  | 	updateRequest.rootAddr = rsrc.rootAddr | ||||||
|  | 	updateRequest.metaHash = rsrc.metaHash | ||||||
|  | 	updateRequest.metadata = rsrc.ResourceMetadata | ||||||
|  |  | ||||||
|  | 	// if we already have an update for this period then increment version | ||||||
|  | 	// resource object MUST be in sync for version to be correct, but we checked this earlier in the method already | ||||||
|  | 	if h.hasUpdate(rootAddr, updateRequest.period) { | ||||||
|  | 		updateRequest.version = rsrc.version + 1 | ||||||
|  | 	} else { | ||||||
|  | 		updateRequest.version = 1 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return updateRequest, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Lookup retrieves a specific or latest version of the resource update with metadata chunk at params.Root | ||||||
|  | // Lookup works differently depending on the configuration of `LookupParams` | ||||||
|  | // See the `LookupParams` documentation and helper functions: | ||||||
|  | // `LookupLatest`, `LookupLatestVersionInPeriod` and `LookupVersion` | ||||||
|  | // When looking for the latest update, it starts at the next period after the current time. | ||||||
|  | // upon failure tries the corresponding keys of each previous period until one is found | ||||||
|  | // (or startTime is reached, in which case there are no updates). | ||||||
|  | func (h *Handler) Lookup(ctx context.Context, params *LookupParams) (*resource, error) { | ||||||
|  |  | ||||||
|  | 	rsrc := h.get(params.rootAddr) | ||||||
|  | 	if rsrc == nil { | ||||||
|  | 		return nil, NewError(ErrNothingToReturn, "resource not loaded") | ||||||
|  | 	} | ||||||
|  | 	return h.lookup(rsrc, params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LookupPrevious returns the resource before the one currently loaded in the resource cache | ||||||
|  | // This is useful where resource updates are used incrementally in contrast to | ||||||
|  | // merely replacing content. | ||||||
|  | // Requires a cached resource object to determine the current state of the resource. | ||||||
|  | func (h *Handler) LookupPrevious(ctx context.Context, params *LookupParams) (*resource, error) { | ||||||
|  | 	rsrc := h.get(params.rootAddr) | ||||||
|  | 	if rsrc == nil { | ||||||
|  | 		return nil, NewError(ErrNothingToReturn, "resource not loaded") | ||||||
|  | 	} | ||||||
|  | 	if !rsrc.isSynced() { | ||||||
|  | 		return nil, NewError(ErrNotSynced, "LookupPrevious requires synced resource.") | ||||||
|  | 	} else if rsrc.period == 0 { | ||||||
|  | 		return nil, NewError(ErrNothingToReturn, " not found") | ||||||
|  | 	} | ||||||
|  | 	var version, period uint32 | ||||||
|  | 	if rsrc.version > 1 { | ||||||
|  | 		version = rsrc.version - 1 | ||||||
|  | 		period = rsrc.period | ||||||
|  | 	} else if rsrc.period == 1 { | ||||||
|  | 		return nil, NewError(ErrNothingToReturn, "Current update is the oldest") | ||||||
|  | 	} else { | ||||||
|  | 		version = 0 | ||||||
|  | 		period = rsrc.period - 1 | ||||||
|  | 	} | ||||||
|  | 	return h.lookup(rsrc, NewLookupParams(rsrc.rootAddr, period, version, params.Limit)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // base code for public lookup methods | ||||||
|  | func (h *Handler) lookup(rsrc *resource, params *LookupParams) (*resource, error) { | ||||||
|  |  | ||||||
|  | 	lp := *params | ||||||
|  | 	// we can't look for anything without a store | ||||||
|  | 	if h.chunkStore == nil { | ||||||
|  | 		return nil, NewError(ErrInit, "Call Handler.SetStore() before performing lookups") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var specificperiod bool | ||||||
|  | 	if lp.period > 0 { | ||||||
|  | 		specificperiod = true | ||||||
|  | 	} else { | ||||||
|  | 		// get the current time and the next period | ||||||
|  | 		now := TimestampProvider.Now() | ||||||
|  |  | ||||||
|  | 		var period uint32 | ||||||
|  | 		period, err := getNextPeriod(rsrc.StartTime.Time, now.Time, rsrc.Frequency) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		lp.period = period | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// start from the last possible period, and iterate previous ones | ||||||
|  | 	// (unless we want a specific period only) until we find a match. | ||||||
|  | 	// If we hit startTime we're out of options | ||||||
|  | 	var specificversion bool | ||||||
|  | 	if lp.version > 0 { | ||||||
|  | 		specificversion = true | ||||||
|  | 	} else { | ||||||
|  | 		lp.version = 1 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var hops uint32 | ||||||
|  | 	if lp.Limit == 0 { | ||||||
|  | 		lp.Limit = h.queryMaxPeriods | ||||||
|  | 	} | ||||||
|  | 	log.Trace("resource lookup", "period", lp.period, "version", lp.version, "limit", lp.Limit) | ||||||
|  | 	for lp.period > 0 { | ||||||
|  | 		if lp.Limit != 0 && hops > lp.Limit { | ||||||
|  | 			return nil, NewErrorf(ErrPeriodDepth, "Lookup exceeded max period hops (%d)", lp.Limit) | ||||||
|  | 		} | ||||||
|  | 		updateAddr := lp.UpdateAddr() | ||||||
|  | 		chunk, err := h.chunkStore.GetWithTimeout(context.TODO(), updateAddr, defaultRetrieveTimeout) | ||||||
|  | 		if err == nil { | ||||||
|  | 			if specificversion { | ||||||
|  | 				return h.updateIndex(rsrc, chunk) | ||||||
|  | 			} | ||||||
|  | 			// check if we have versions > 1. If a version fails, the previous version is used and returned. | ||||||
|  | 			log.Trace("rsrc update version 1 found, checking for version updates", "period", lp.period, "updateAddr", updateAddr) | ||||||
|  | 			for { | ||||||
|  | 				newversion := lp.version + 1 | ||||||
|  | 				updateAddr := lp.UpdateAddr() | ||||||
|  | 				newchunk, err := h.chunkStore.GetWithTimeout(context.TODO(), updateAddr, defaultRetrieveTimeout) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return h.updateIndex(rsrc, chunk) | ||||||
|  | 				} | ||||||
|  | 				chunk = newchunk | ||||||
|  | 				lp.version = newversion | ||||||
|  | 				log.Trace("version update found, checking next", "version", lp.version, "period", lp.period, "updateAddr", updateAddr) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if specificperiod { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		log.Trace("rsrc update not found, checking previous period", "period", lp.period, "updateAddr", updateAddr) | ||||||
|  | 		lp.period-- | ||||||
|  | 		hops++ | ||||||
|  | 	} | ||||||
|  | 	return nil, NewError(ErrNotFound, "no updates found") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Load retrieves the Mutable Resource metadata chunk stored at rootAddr | ||||||
|  | // Upon retrieval it creates/updates the index entry for it with metadata corresponding to the chunk contents | ||||||
|  | func (h *Handler) Load(ctx context.Context, rootAddr storage.Address) (*resource, error) { | ||||||
|  | 	chunk, err := h.chunkStore.GetWithTimeout(ctx, rootAddr, defaultRetrieveTimeout) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewError(ErrNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// create the index entry | ||||||
|  | 	rsrc := &resource{} | ||||||
|  |  | ||||||
|  | 	if err := rsrc.ResourceMetadata.binaryGet(chunk.SData); err != nil { // Will fail if this is not really a metadata chunk | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rsrc.rootAddr, rsrc.metaHash = metadataHash(chunk.SData) | ||||||
|  | 	if !bytes.Equal(rsrc.rootAddr, rootAddr) { | ||||||
|  | 		return nil, NewError(ErrCorruptData, "Corrupt metadata chunk") | ||||||
|  | 	} | ||||||
|  | 	h.set(rootAddr, rsrc) | ||||||
|  | 	log.Trace("resource index load", "rootkey", rootAddr, "name", rsrc.ResourceMetadata.Name, "starttime", rsrc.ResourceMetadata.StartTime, "frequency", rsrc.ResourceMetadata.Frequency) | ||||||
|  | 	return rsrc, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // update mutable resource index map with specified content | ||||||
|  | func (h *Handler) updateIndex(rsrc *resource, chunk *storage.Chunk) (*resource, error) { | ||||||
|  |  | ||||||
|  | 	// retrieve metadata from chunk data and check that it matches this mutable resource | ||||||
|  | 	var r SignedResourceUpdate | ||||||
|  | 	if err := r.fromChunk(chunk.Addr, chunk.SData); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	log.Trace("resource index update", "name", rsrc.ResourceMetadata.Name, "updatekey", chunk.Addr, "period", r.period, "version", r.version) | ||||||
|  |  | ||||||
|  | 	// update our rsrcs entry map | ||||||
|  | 	rsrc.lastKey = chunk.Addr | ||||||
|  | 	rsrc.period = r.period | ||||||
|  | 	rsrc.version = r.version | ||||||
|  | 	rsrc.updated = time.Now() | ||||||
|  | 	rsrc.data = make([]byte, len(r.data)) | ||||||
|  | 	rsrc.multihash = r.multihash | ||||||
|  | 	copy(rsrc.data, r.data) | ||||||
|  | 	rsrc.Reader = bytes.NewReader(rsrc.data) | ||||||
|  | 	log.Debug("resource synced", "name", rsrc.ResourceMetadata.Name, "updateAddr", chunk.Addr, "period", rsrc.period, "version", rsrc.version) | ||||||
|  | 	h.set(chunk.Addr, rsrc) | ||||||
|  | 	return rsrc, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Update adds an actual data update | ||||||
|  | // Uses the Mutable Resource metadata currently loaded in the resources map entry. | ||||||
|  | // It is the caller's responsibility to make sure that this data is not stale. | ||||||
|  | // Note that a Mutable Resource update cannot span chunks, and thus has a MAX NET LENGTH 4096, INCLUDING update header data and signature. An error will be returned if the total length of the chunk payload will exceed this limit. | ||||||
|  | // Update can only check if the caller is trying to overwrite the very last known version, otherwise it just puts the update | ||||||
|  | // on the network. | ||||||
|  | func (h *Handler) Update(ctx context.Context, r *SignedResourceUpdate) (storage.Address, error) { | ||||||
|  | 	return h.update(ctx, r) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // create and commit an update | ||||||
|  | func (h *Handler) update(ctx context.Context, r *SignedResourceUpdate) (updateAddr storage.Address, err error) { | ||||||
|  |  | ||||||
|  | 	// we can't update anything without a store | ||||||
|  | 	if h.chunkStore == nil { | ||||||
|  | 		return nil, NewError(ErrInit, "Call Handler.SetStore() before updating") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rsrc := h.get(r.rootAddr) | ||||||
|  | 	if rsrc != nil && rsrc.period != 0 && rsrc.version != 0 && // This is the only cheap check we can do for sure | ||||||
|  | 		rsrc.period == r.period && rsrc.version >= r.version { // without having to lookup update chunks | ||||||
|  |  | ||||||
|  | 		return nil, NewError(ErrInvalidValue, "A former update in this period is already known to exist") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	chunk, err := r.toChunk() // Serialize the update into a chunk. Fails if data is too big | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// send the chunk | ||||||
|  | 	h.chunkStore.Put(ctx, chunk) | ||||||
|  | 	log.Trace("resource update", "updateAddr", r.updateAddr, "lastperiod", r.period, "version", r.version, "data", chunk.SData, "multihash", r.multihash) | ||||||
|  |  | ||||||
|  | 	// update our resources map entry if the new update is older than the one we have, if we have it. | ||||||
|  | 	if rsrc != nil && r.period > rsrc.period || (rsrc.period == r.period && r.version > rsrc.version) { | ||||||
|  | 		rsrc.period = r.period | ||||||
|  | 		rsrc.version = r.version | ||||||
|  | 		rsrc.data = make([]byte, len(r.data)) | ||||||
|  | 		rsrc.updated = time.Now() | ||||||
|  | 		rsrc.lastKey = r.updateAddr | ||||||
|  | 		rsrc.multihash = r.multihash | ||||||
|  | 		copy(rsrc.data, r.data) | ||||||
|  | 		rsrc.Reader = bytes.NewReader(rsrc.data) | ||||||
|  | 	} | ||||||
|  | 	return r.updateAddr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Retrieves the resource index value for the given nameHash | ||||||
|  | func (h *Handler) get(rootAddr storage.Address) *resource { | ||||||
|  | 	if len(rootAddr) < storage.KeyLength { | ||||||
|  | 		log.Warn("Handler.get with invalid rootAddr") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0])) | ||||||
|  | 	h.resourceLock.RLock() | ||||||
|  | 	defer h.resourceLock.RUnlock() | ||||||
|  | 	rsrc := h.resources[hashKey] | ||||||
|  | 	return rsrc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sets the resource index value for the given nameHash | ||||||
|  | func (h *Handler) set(rootAddr storage.Address, rsrc *resource) { | ||||||
|  | 	if len(rootAddr) < storage.KeyLength { | ||||||
|  | 		log.Warn("Handler.set with invalid rootAddr") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	hashKey := *(*uint64)(unsafe.Pointer(&rootAddr[0])) | ||||||
|  | 	h.resourceLock.Lock() | ||||||
|  | 	defer h.resourceLock.Unlock() | ||||||
|  | 	h.resources[hashKey] = rsrc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Checks if we already have an update on this resource, according to the value in the current state of the resource index | ||||||
|  | func (h *Handler) hasUpdate(rootAddr storage.Address, period uint32) bool { | ||||||
|  | 	rsrc := h.get(rootAddr) | ||||||
|  | 	return rsrc != nil && rsrc.period == period | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								swarm/storage/mru/lookup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								swarm/storage/mru/lookup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"hash" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // LookupParams is used to specify constraints when performing an update lookup | ||||||
|  | // Limit defines whether or not the lookup should be limited | ||||||
|  | // If Limit is set to true then Max defines the amount of hops that can be performed | ||||||
|  | type LookupParams struct { | ||||||
|  | 	UpdateLookup | ||||||
|  | 	Limit uint32 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RootAddr returns the metadata chunk address | ||||||
|  | func (r *LookupParams) RootAddr() storage.Address { | ||||||
|  | 	return r.rootAddr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewLookupParams(rootAddr storage.Address, period, version uint32, limit uint32) *LookupParams { | ||||||
|  | 	return &LookupParams{ | ||||||
|  | 		UpdateLookup: UpdateLookup{ | ||||||
|  | 			period:   period, | ||||||
|  | 			version:  version, | ||||||
|  | 			rootAddr: rootAddr, | ||||||
|  | 		}, | ||||||
|  | 		Limit: limit, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LookupLatest generates lookup parameters that look for the latest version of a resource | ||||||
|  | func LookupLatest(rootAddr storage.Address) *LookupParams { | ||||||
|  | 	return NewLookupParams(rootAddr, 0, 0, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LookupLatestVersionInPeriod generates lookup parameters that look for the latest version of a resource in a given period | ||||||
|  | func LookupLatestVersionInPeriod(rootAddr storage.Address, period uint32) *LookupParams { | ||||||
|  | 	return NewLookupParams(rootAddr, period, 0, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LookupVersion generates lookup parameters that look for a specific version of a resource | ||||||
|  | func LookupVersion(rootAddr storage.Address, period, version uint32) *LookupParams { | ||||||
|  | 	return NewLookupParams(rootAddr, period, version, 0) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UpdateLookup represents the components of a resource update search key | ||||||
|  | type UpdateLookup struct { | ||||||
|  | 	period   uint32 | ||||||
|  | 	version  uint32 | ||||||
|  | 	rootAddr storage.Address | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 4 bytes period | ||||||
|  | // 4 bytes version | ||||||
|  | // storage.Keylength for rootAddr | ||||||
|  | const updateLookupLength = 4 + 4 + storage.KeyLength | ||||||
|  |  | ||||||
|  | // UpdateAddr calculates the resource update chunk address corresponding to this lookup key | ||||||
|  | func (u *UpdateLookup) UpdateAddr() (updateAddr storage.Address) { | ||||||
|  | 	serializedData := make([]byte, updateLookupLength) | ||||||
|  | 	u.binaryPut(serializedData) | ||||||
|  | 	hasher := hashPool.Get().(hash.Hash) | ||||||
|  | 	defer hashPool.Put(hasher) | ||||||
|  | 	hasher.Reset() | ||||||
|  | 	hasher.Write(serializedData) | ||||||
|  | 	return hasher.Sum(nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryPut serializes this UpdateLookup instance into the provided slice | ||||||
|  | func (u *UpdateLookup) binaryPut(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) != updateLookupLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Incorrect slice size to serialize UpdateLookup. Expected %d, got %d", updateLookupLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  | 	if len(u.rootAddr) != storage.KeyLength { | ||||||
|  | 		return NewError(ErrInvalidValue, "UpdateLookup.binaryPut called without rootAddr set") | ||||||
|  | 	} | ||||||
|  | 	binary.LittleEndian.PutUint32(serializedData[:4], u.period) | ||||||
|  | 	binary.LittleEndian.PutUint32(serializedData[4:8], u.version) | ||||||
|  | 	copy(serializedData[8:], u.rootAddr[:]) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryLength returns the expected size of this structure when serialized | ||||||
|  | func (u *UpdateLookup) binaryLength() int { | ||||||
|  | 	return updateLookupLength | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryGet restores the current instance from the information contained in the passed slice | ||||||
|  | func (u *UpdateLookup) binaryGet(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) != updateLookupLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Incorrect slice size to read UpdateLookup. Expected %d, got %d", updateLookupLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  | 	u.period = binary.LittleEndian.Uint32(serializedData[:4]) | ||||||
|  | 	u.version = binary.LittleEndian.Uint32(serializedData[4:8]) | ||||||
|  | 	u.rootAddr = storage.Address(make([]byte, storage.KeyLength)) | ||||||
|  | 	copy(u.rootAddr[:], serializedData[8:]) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										85
									
								
								swarm/storage/mru/lookup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								swarm/storage/mru/lookup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func getTestUpdateLookup() *UpdateLookup { | ||||||
|  | 	metadata := *getTestMetadata() | ||||||
|  | 	rootAddr, _, _, _ := metadata.serializeAndHash() | ||||||
|  | 	return &UpdateLookup{ | ||||||
|  | 		period:   79, | ||||||
|  | 		version:  2010, | ||||||
|  | 		rootAddr: rootAddr, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func compareUpdateLookup(a, b *UpdateLookup) bool { | ||||||
|  | 	return a.version == b.version && | ||||||
|  | 		a.period == b.period && | ||||||
|  | 		bytes.Equal(a.rootAddr, b.rootAddr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateLookupUpdateAddr(t *testing.T) { | ||||||
|  | 	ul := getTestUpdateLookup() | ||||||
|  | 	updateAddr := ul.UpdateAddr() | ||||||
|  | 	compareByteSliceToExpectedHex(t, "updateAddr", updateAddr, "0x8fbc8d4777ef6da790257eda80ab4321fabd08cbdbe67e4e3da6caca386d64e0") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateLookupSerializer(t *testing.T) { | ||||||
|  | 	serializedUpdateLookup := make([]byte, updateLookupLength) | ||||||
|  | 	ul := getTestUpdateLookup() | ||||||
|  | 	if err := ul.binaryPut(serializedUpdateLookup); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	compareByteSliceToExpectedHex(t, "serializedUpdateLookup", serializedUpdateLookup, "0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb") | ||||||
|  |  | ||||||
|  | 	// set receiving slice to the wrong size | ||||||
|  | 	serializedUpdateLookup = make([]byte, updateLookupLength+7) | ||||||
|  | 	if err := ul.binaryPut(serializedUpdateLookup); err == nil { | ||||||
|  | 		t.Fatalf("Expected UpdateLookup.binaryPut to fail when receiving slice has a length != %d", updateLookupLength) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set rootAddr to an invalid length | ||||||
|  | 	ul.rootAddr = []byte{1, 2, 3, 4} | ||||||
|  | 	serializedUpdateLookup = make([]byte, updateLookupLength) | ||||||
|  | 	if err := ul.binaryPut(serializedUpdateLookup); err == nil { | ||||||
|  | 		t.Fatal("Expected UpdateLookup.binaryPut to fail when rootAddr is not of the correct size") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateLookupDeserializer(t *testing.T) { | ||||||
|  | 	serializedUpdateLookup, _ := hexutil.Decode("0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb") | ||||||
|  | 	var recoveredUpdateLookup UpdateLookup | ||||||
|  | 	if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	originalUpdateLookup := *getTestUpdateLookup() | ||||||
|  | 	if !compareUpdateLookup(&originalUpdateLookup, &recoveredUpdateLookup) { | ||||||
|  | 		t.Fatalf("Expected recovered UpdateLookup to match") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set source slice to the wrong size | ||||||
|  | 	serializedUpdateLookup = make([]byte, updateLookupLength+4) | ||||||
|  | 	if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err == nil { | ||||||
|  | 		t.Fatalf("Expected UpdateLookup.binaryGet to fail when source slice has a length != %d", updateLookupLength) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateLookupSerializeDeserialize(t *testing.T) { | ||||||
|  | 	serializedUpdateLookup := make([]byte, updateLookupLength) | ||||||
|  | 	originalUpdateLookup := getTestUpdateLookup() | ||||||
|  | 	if err := originalUpdateLookup.binaryPut(serializedUpdateLookup); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	var recoveredUpdateLookup UpdateLookup | ||||||
|  | 	if err := recoveredUpdateLookup.binaryGet(serializedUpdateLookup); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if !compareUpdateLookup(originalUpdateLookup, &recoveredUpdateLookup) { | ||||||
|  | 		t.Fatalf("Expected recovered UpdateLookup to match") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										189
									
								
								swarm/storage/mru/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								swarm/storage/mru/metadata.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"hash" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ResourceMetadata encapsulates the immutable information about a mutable resource :) | ||||||
|  | // once serialized into a chunk, the resource can be retrieved by knowing its content-addressed rootAddr | ||||||
|  | type ResourceMetadata struct { | ||||||
|  | 	StartTime Timestamp      // time at which the resource starts to be valid | ||||||
|  | 	Frequency uint64         // expected update frequency for the resource | ||||||
|  | 	Name      string         // name of the resource, for the reference of the user or to disambiguate resources with same starttime, frequency, owneraddr | ||||||
|  | 	Owner     common.Address // public address of the resource owner | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const frequencyLength = 8 // sizeof(uint64) | ||||||
|  | const nameLengthLength = 1 | ||||||
|  |  | ||||||
|  | // Resource metadata chunk layout: | ||||||
|  | // 4 prefix bytes (chunkPrefixLength). The first two set to zero. The second two indicate the length | ||||||
|  | // Timestamp: timestampLength bytes | ||||||
|  | // frequency: frequencyLength bytes | ||||||
|  | // name length: nameLengthLength bytes | ||||||
|  | // name (variable length, can be empty, up to 255 bytes) | ||||||
|  | // ownerAddr: common.AddressLength | ||||||
|  | const minimumMetadataLength = chunkPrefixLength + timestampLength + frequencyLength + nameLengthLength + common.AddressLength | ||||||
|  |  | ||||||
|  | // binaryGet populates the resource metadata from a byte array | ||||||
|  | func (r *ResourceMetadata) binaryGet(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) < minimumMetadataLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Metadata chunk to deserialize is too short. Expected at least %d. Got %d.", minimumMetadataLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// first two bytes must be set to zero to indicate metadata chunks, so enforce this. | ||||||
|  | 	if serializedData[0] != 0 || serializedData[1] != 0 { | ||||||
|  | 		return NewError(ErrCorruptData, "Invalid metadata chunk") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cursor := 2 | ||||||
|  | 	metadataLength := int(binary.LittleEndian.Uint16(serializedData[cursor : cursor+2])) // metadataLength does not include the 4 prefix bytes | ||||||
|  | 	if metadataLength+chunkPrefixLength != len(serializedData) { | ||||||
|  | 		return NewErrorf(ErrCorruptData, "Incorrect declared metadata length. Expected %d, got %d.", metadataLength+chunkPrefixLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cursor += 2 | ||||||
|  |  | ||||||
|  | 	if err := r.StartTime.binaryGet(serializedData[cursor : cursor+timestampLength]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cursor += timestampLength | ||||||
|  |  | ||||||
|  | 	r.Frequency = binary.LittleEndian.Uint64(serializedData[cursor : cursor+frequencyLength]) | ||||||
|  | 	cursor += frequencyLength | ||||||
|  |  | ||||||
|  | 	nameLength := int(serializedData[cursor]) | ||||||
|  | 	if nameLength+minimumMetadataLength > len(serializedData) { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Metadata chunk to deserialize is too short when decoding resource name. Expected at least %d. Got %d.", nameLength+minimumMetadataLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  | 	cursor++ | ||||||
|  | 	r.Name = string(serializedData[cursor : cursor+nameLength]) | ||||||
|  | 	cursor += nameLength | ||||||
|  |  | ||||||
|  | 	copy(r.Owner[:], serializedData[cursor:]) | ||||||
|  | 	cursor += common.AddressLength | ||||||
|  | 	if cursor != len(serializedData) { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Metadata chunk has leftover data after deserialization. %d left to read", len(serializedData)-cursor) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryPut encodes the metadata into a byte array | ||||||
|  | func (r *ResourceMetadata) binaryPut(serializedData []byte) error { | ||||||
|  | 	metadataChunkLength := r.binaryLength() | ||||||
|  | 	if len(serializedData) != metadataChunkLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Need a slice of exactly %d bytes to serialize this metadata, but got a slice of size %d.", metadataChunkLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// root chunk has first two bytes both set to 0, which distinguishes from update bytes | ||||||
|  | 	// therefore, skip the first two bytes of a zero-initialized array. | ||||||
|  | 	cursor := 2 | ||||||
|  | 	binary.LittleEndian.PutUint16(serializedData[cursor:cursor+2], uint16(metadataChunkLength-chunkPrefixLength)) // metadataLength does not include the 4 prefix bytes | ||||||
|  | 	cursor += 2 | ||||||
|  |  | ||||||
|  | 	r.StartTime.binaryPut(serializedData[cursor : cursor+timestampLength]) | ||||||
|  | 	cursor += timestampLength | ||||||
|  |  | ||||||
|  | 	binary.LittleEndian.PutUint64(serializedData[cursor:cursor+frequencyLength], r.Frequency) | ||||||
|  | 	cursor += frequencyLength | ||||||
|  |  | ||||||
|  | 	// Encode the name string as a 1 byte length followed by the encoded string. | ||||||
|  | 	// Longer strings will be truncated. | ||||||
|  | 	nameLength := len(r.Name) | ||||||
|  | 	if nameLength > 255 { | ||||||
|  | 		nameLength = 255 | ||||||
|  | 	} | ||||||
|  | 	serializedData[cursor] = uint8(nameLength) | ||||||
|  | 	cursor++ | ||||||
|  | 	copy(serializedData[cursor:cursor+nameLength], []byte(r.Name[:nameLength])) | ||||||
|  | 	cursor += nameLength | ||||||
|  |  | ||||||
|  | 	copy(serializedData[cursor:cursor+common.AddressLength], r.Owner[:]) | ||||||
|  | 	cursor += common.AddressLength | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *ResourceMetadata) binaryLength() int { | ||||||
|  | 	return minimumMetadataLength + len(r.Name) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // serializeAndHash returns the root chunk addr and metadata hash that help identify and ascertain ownership of this resource | ||||||
|  | // returns the serialized metadata as a byproduct of having to hash it. | ||||||
|  | func (r *ResourceMetadata) serializeAndHash() (rootAddr, metaHash []byte, chunkData []byte, err error) { | ||||||
|  |  | ||||||
|  | 	chunkData = make([]byte, r.binaryLength()) | ||||||
|  | 	if err := r.binaryPut(chunkData); err != nil { | ||||||
|  | 		return nil, nil, nil, err | ||||||
|  | 	} | ||||||
|  | 	rootAddr, metaHash = metadataHash(chunkData) | ||||||
|  | 	return rootAddr, metaHash, chunkData, nil | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // creates a metadata chunk out of a resourceMetadata structure | ||||||
|  | func (metadata *ResourceMetadata) newChunk() (chunk *storage.Chunk, metaHash []byte, err error) { | ||||||
|  | 	// the metadata chunk contains a timestamp of when the resource starts to be valid | ||||||
|  | 	// and also how frequently it is expected to be updated | ||||||
|  | 	// from this we know at what time we should look for updates, and how often | ||||||
|  | 	// it also contains the name of the resource, so we know what resource we are working with | ||||||
|  |  | ||||||
|  | 	// the key (rootAddr) of the metadata chunk is content-addressed | ||||||
|  | 	// if it wasn't we couldn't replace it later | ||||||
|  | 	// resolving this relationship is left up to external agents (for example ENS) | ||||||
|  | 	rootAddr, metaHash, chunkData, err := metadata.serializeAndHash() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// make the chunk and send it to swarm | ||||||
|  | 	chunk = storage.NewChunk(rootAddr, nil) | ||||||
|  | 	chunk.SData = chunkData | ||||||
|  | 	chunk.Size = int64(len(chunkData)) | ||||||
|  |  | ||||||
|  | 	return chunk, metaHash, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // metadataHash returns the metadata chunk root address and metadata hash | ||||||
|  | // that help identify and ascertain ownership of this resource | ||||||
|  | // We compute it as rootAddr = H(ownerAddr, H(metadata)) | ||||||
|  | // Where H() is SHA3 | ||||||
|  | // metadata are all the metadata fields, except ownerAddr | ||||||
|  | // ownerAddr is the public address of the resource owner | ||||||
|  | // Update chunks must carry a rootAddr reference and metaHash in order to be verified | ||||||
|  | // This way, a node that receives an update can check the signature, recover the public address | ||||||
|  | // and check the ownership by computing H(ownerAddr, metaHash) and comparing it to the rootAddr | ||||||
|  | // the resource is claiming to update without having to lookup the metadata chunk. | ||||||
|  | // see verifyResourceOwnerhsip in signedupdate.go | ||||||
|  | func metadataHash(chunkData []byte) (rootAddr, metaHash []byte) { | ||||||
|  | 	hasher := hashPool.Get().(hash.Hash) | ||||||
|  | 	defer hashPool.Put(hasher) | ||||||
|  | 	hasher.Reset() | ||||||
|  | 	hasher.Write(chunkData[:len(chunkData)-common.AddressLength]) | ||||||
|  | 	metaHash = hasher.Sum(nil) | ||||||
|  | 	hasher.Reset() | ||||||
|  | 	hasher.Write(metaHash) | ||||||
|  | 	hasher.Write(chunkData[len(chunkData)-common.AddressLength:]) | ||||||
|  | 	rootAddr = hasher.Sum(nil) | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										126
									
								
								swarm/storage/mru/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								swarm/storage/mru/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func compareByteSliceToExpectedHex(t *testing.T, variableName string, actualValue []byte, expectedHex string) { | ||||||
|  | 	if hexutil.Encode(actualValue) != expectedHex { | ||||||
|  | 		t.Fatalf("%s: Expected %s to be %s, got %s", t.Name(), variableName, expectedHex, hexutil.Encode(actualValue)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getTestMetadata() *ResourceMetadata { | ||||||
|  | 	return &ResourceMetadata{ | ||||||
|  | 		Name: "world news report, every hour, on the hour", | ||||||
|  | 		StartTime: Timestamp{ | ||||||
|  | 			Time: 1528880400, | ||||||
|  | 		}, | ||||||
|  | 		Frequency: 3600, | ||||||
|  | 		Owner:     newCharlieSigner().Address(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMetadataSerializerDeserializer(t *testing.T) { | ||||||
|  | 	metadata := *getTestMetadata() | ||||||
|  |  | ||||||
|  | 	rootAddr, metaHash, chunkData, err := metadata.serializeAndHash() // creates hashes and marshals, in one go | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	const expectedRootAddr = "0xfb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fb" | ||||||
|  | 	const expectedMetaHash = "0xf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf0" | ||||||
|  | 	const expectedChunkData = "0x00004f0010dd205b00000000100e0000000000002a776f726c64206e657773207265706f72742c20657665727920686f75722c206f6e2074686520686f7572876a8936a7cd0b79ef0735ad0896c1afe278781c" | ||||||
|  |  | ||||||
|  | 	compareByteSliceToExpectedHex(t, "rootAddr", rootAddr, expectedRootAddr) | ||||||
|  | 	compareByteSliceToExpectedHex(t, "metaHash", metaHash, expectedMetaHash) | ||||||
|  | 	compareByteSliceToExpectedHex(t, "chunkData", chunkData, expectedChunkData) | ||||||
|  |  | ||||||
|  | 	recoveredMetadata := ResourceMetadata{} | ||||||
|  | 	recoveredMetadata.binaryGet(chunkData) | ||||||
|  |  | ||||||
|  | 	if recoveredMetadata != metadata { | ||||||
|  | 		t.Fatalf("Expected that the recovered metadata equals the marshalled metadata") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// we are going to mess with the data, so create a backup to go back to it for the next test | ||||||
|  | 	backup := make([]byte, len(chunkData)) | ||||||
|  | 	copy(backup, chunkData) | ||||||
|  |  | ||||||
|  | 	chunkData = []byte{1, 2, 3} | ||||||
|  | 	if err := recoveredMetadata.binaryGet(chunkData); err == nil { | ||||||
|  | 		t.Fatal("Expected binaryGet to fail since chunk is too small") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore backup | ||||||
|  | 	chunkData = make([]byte, len(backup)) | ||||||
|  | 	copy(chunkData, backup) | ||||||
|  |  | ||||||
|  | 	// mess with the prefix so it is not zero | ||||||
|  | 	chunkData[0] = 7 | ||||||
|  | 	chunkData[1] = 9 | ||||||
|  |  | ||||||
|  | 	if err := recoveredMetadata.binaryGet(chunkData); err == nil { | ||||||
|  | 		t.Fatal("Expected binaryGet to fail since prefix bytes are not zero") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore backup | ||||||
|  | 	chunkData = make([]byte, len(backup)) | ||||||
|  | 	copy(chunkData, backup) | ||||||
|  |  | ||||||
|  | 	// mess with the length header to trigger an error | ||||||
|  | 	chunkData[2] = 255 | ||||||
|  | 	chunkData[3] = 44 | ||||||
|  | 	if err := recoveredMetadata.binaryGet(chunkData); err == nil { | ||||||
|  | 		t.Fatal("Expected binaryGet to fail since header length does not match") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore backup | ||||||
|  | 	chunkData = make([]byte, len(backup)) | ||||||
|  | 	copy(chunkData, backup) | ||||||
|  |  | ||||||
|  | 	// mess with name length header to trigger a chunk too short error | ||||||
|  | 	chunkData[20] = 255 | ||||||
|  | 	if err := recoveredMetadata.binaryGet(chunkData); err == nil { | ||||||
|  | 		t.Fatal("Expected binaryGet to fail since name length is incorrect") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore backup | ||||||
|  | 	chunkData = make([]byte, len(backup)) | ||||||
|  | 	copy(chunkData, backup) | ||||||
|  |  | ||||||
|  | 	// mess with name length header to trigger an leftover bytes to read error | ||||||
|  | 	chunkData[20] = 3 | ||||||
|  | 	if err := recoveredMetadata.binaryGet(chunkData); err == nil { | ||||||
|  | 		t.Fatal("Expected binaryGet to fail since name length is too small") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMetadataSerializerLengthCheck(t *testing.T) { | ||||||
|  | 	metadata := *getTestMetadata() | ||||||
|  |  | ||||||
|  | 	// make a slice that is too small to contain the metadata | ||||||
|  | 	serializedMetadata := make([]byte, 4) | ||||||
|  |  | ||||||
|  | 	if err := metadata.binaryPut(serializedMetadata); err == nil { | ||||||
|  | 		t.Fatal("Expected metadata.binaryPut to fail, since target slice is too small") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										297
									
								
								swarm/storage/mru/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								swarm/storage/mru/request.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,297 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common" | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // updateRequestJSON represents a JSON-serialized UpdateRequest | ||||||
|  | type updateRequestJSON struct { | ||||||
|  | 	Name      string `json:"name,omitempty"` | ||||||
|  | 	Frequency uint64 `json:"frequency,omitempty"` | ||||||
|  | 	StartTime uint64 `json:"startTime,omitempty"` | ||||||
|  | 	Owner     string `json:"ownerAddr,omitempty"` | ||||||
|  | 	RootAddr  string `json:"rootAddr,omitempty"` | ||||||
|  | 	MetaHash  string `json:"metaHash,omitempty"` | ||||||
|  | 	Version   uint32 `json:"version,omitempty"` | ||||||
|  | 	Period    uint32 `json:"period,omitempty"` | ||||||
|  | 	Data      string `json:"data,omitempty"` | ||||||
|  | 	Multihash bool   `json:"multiHash"` | ||||||
|  | 	Signature string `json:"signature,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Request represents an update and/or resource create message | ||||||
|  | type Request struct { | ||||||
|  | 	SignedResourceUpdate | ||||||
|  | 	metadata ResourceMetadata | ||||||
|  | 	isNew    bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var zeroAddr = common.Address{} | ||||||
|  |  | ||||||
|  | // NewCreateUpdateRequest returns a ready to sign request to create and initialize a resource with data | ||||||
|  | func NewCreateUpdateRequest(metadata *ResourceMetadata) (*Request, error) { | ||||||
|  |  | ||||||
|  | 	request, err := NewCreateRequest(metadata) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// get the current time | ||||||
|  | 	now := TimestampProvider.Now().Time | ||||||
|  |  | ||||||
|  | 	request.version = 1 | ||||||
|  | 	request.period, err = getNextPeriod(metadata.StartTime.Time, now, metadata.Frequency) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewCreateRequest returns a request to create a new resource | ||||||
|  | func NewCreateRequest(metadata *ResourceMetadata) (request *Request, err error) { | ||||||
|  | 	if metadata.StartTime.Time == 0 { // get the current time | ||||||
|  | 		metadata.StartTime = TimestampProvider.Now() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if metadata.Owner == zeroAddr { | ||||||
|  | 		return nil, NewError(ErrInvalidValue, "OwnerAddr is not set") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	request = &Request{ | ||||||
|  | 		metadata: *metadata, | ||||||
|  | 	} | ||||||
|  | 	request.rootAddr, request.metaHash, _, err = request.metadata.serializeAndHash() | ||||||
|  | 	request.isNew = true | ||||||
|  | 	return request, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Frequency returns the resource's expected update frequency | ||||||
|  | func (r *Request) Frequency() uint64 { | ||||||
|  | 	return r.metadata.Frequency | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Name returns the resource human-readable name | ||||||
|  | func (r *Request) Name() string { | ||||||
|  | 	return r.metadata.Name | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Multihash returns true if the resource data should be interpreted as a multihash | ||||||
|  | func (r *Request) Multihash() bool { | ||||||
|  | 	return r.multihash | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Period returns in which period the resource will be published | ||||||
|  | func (r *Request) Period() uint32 { | ||||||
|  | 	return r.period | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Version returns the resource version to publish | ||||||
|  | func (r *Request) Version() uint32 { | ||||||
|  | 	return r.version | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RootAddr returns the metadata chunk address | ||||||
|  | func (r *Request) RootAddr() storage.Address { | ||||||
|  | 	return r.rootAddr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StartTime returns the time that the resource was/will be created at | ||||||
|  | func (r *Request) StartTime() Timestamp { | ||||||
|  | 	return r.metadata.StartTime | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Owner returns the resource owner's address | ||||||
|  | func (r *Request) Owner() common.Address { | ||||||
|  | 	return r.metadata.Owner | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sign executes the signature to validate the resource and sets the owner address field | ||||||
|  | func (r *Request) Sign(signer Signer) error { | ||||||
|  | 	if r.metadata.Owner != zeroAddr && r.metadata.Owner != signer.Address() { | ||||||
|  | 		return NewError(ErrInvalidSignature, "Signer does not match current owner of the resource") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := r.SignedResourceUpdate.Sign(signer); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	r.metadata.Owner = signer.Address() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetData stores the payload data the resource will be updated with | ||||||
|  | func (r *Request) SetData(data []byte, multihash bool) { | ||||||
|  | 	r.data = data | ||||||
|  | 	r.multihash = multihash | ||||||
|  | 	r.signature = nil | ||||||
|  | 	if !r.isNew { | ||||||
|  | 		r.metadata.Frequency = 0 // mark as update | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *Request) IsNew() bool { | ||||||
|  | 	return r.metadata.Frequency > 0 && (r.period <= 1 || r.version <= 1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *Request) IsUpdate() bool { | ||||||
|  | 	return r.signature != nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // fromJSON takes an update request JSON and populates an UpdateRequest | ||||||
|  | func (r *Request) fromJSON(j *updateRequestJSON) error { | ||||||
|  |  | ||||||
|  | 	r.version = j.Version | ||||||
|  | 	r.period = j.Period | ||||||
|  | 	r.multihash = j.Multihash | ||||||
|  | 	r.metadata.Name = j.Name | ||||||
|  | 	r.metadata.Frequency = j.Frequency | ||||||
|  | 	r.metadata.StartTime.Time = j.StartTime | ||||||
|  |  | ||||||
|  | 	if err := decodeHexArray(r.metadata.Owner[:], j.Owner, "ownerAddr"); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	if j.Data != "" { | ||||||
|  | 		r.data, err = hexutil.Decode(j.Data) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return NewError(ErrInvalidValue, "Cannot decode data") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var declaredRootAddr storage.Address | ||||||
|  | 	var declaredMetaHash []byte | ||||||
|  |  | ||||||
|  | 	declaredRootAddr, err = decodeHexSlice(j.RootAddr, storage.KeyLength, "rootAddr") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	declaredMetaHash, err = decodeHexSlice(j.MetaHash, 32, "metaHash") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if r.IsNew() { | ||||||
|  | 		// for new resource creation, rootAddr and metaHash are optional because | ||||||
|  | 		// we can derive them from the content itself. | ||||||
|  | 		// however, if the user sent them, we check them for consistency. | ||||||
|  |  | ||||||
|  | 		r.rootAddr, r.metaHash, _, err = r.metadata.serializeAndHash() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if j.RootAddr != "" && !bytes.Equal(declaredRootAddr, r.rootAddr) { | ||||||
|  | 			return NewError(ErrInvalidValue, "rootAddr does not match resource metadata") | ||||||
|  | 		} | ||||||
|  | 		if j.MetaHash != "" && !bytes.Equal(declaredMetaHash, r.metaHash) { | ||||||
|  | 			return NewError(ErrInvalidValue, "metaHash does not match resource metadata") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  | 		//Update message | ||||||
|  | 		r.rootAddr = declaredRootAddr | ||||||
|  | 		r.metaHash = declaredMetaHash | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if j.Signature != "" { | ||||||
|  | 		sigBytes, err := hexutil.Decode(j.Signature) | ||||||
|  | 		if err != nil || len(sigBytes) != signatureLength { | ||||||
|  | 			return NewError(ErrInvalidSignature, "Cannot decode signature") | ||||||
|  | 		} | ||||||
|  | 		r.signature = new(Signature) | ||||||
|  | 		r.updateAddr = r.UpdateAddr() | ||||||
|  | 		copy(r.signature[:], sigBytes) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeHexArray(dst []byte, src, name string) error { | ||||||
|  | 	bytes, err := decodeHexSlice(src, len(dst), name) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if bytes != nil { | ||||||
|  | 		copy(dst, bytes) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeHexSlice(src string, expectedLength int, name string) (bytes []byte, err error) { | ||||||
|  | 	if src != "" { | ||||||
|  | 		bytes, err = hexutil.Decode(src) | ||||||
|  | 		if err != nil || len(bytes) != expectedLength { | ||||||
|  | 			return nil, NewErrorf(ErrInvalidValue, "Cannot decode %s", name) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return bytes, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnmarshalJSON takes a JSON structure stored in a byte array and populates the Request object | ||||||
|  | // Implements json.Unmarshaler interface | ||||||
|  | func (r *Request) UnmarshalJSON(rawData []byte) error { | ||||||
|  | 	var requestJSON updateRequestJSON | ||||||
|  | 	if err := json.Unmarshal(rawData, &requestJSON); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return r.fromJSON(&requestJSON) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MarshalJSON takes an update request and encodes it as a JSON structure into a byte array | ||||||
|  | // Implements json.Marshaler interface | ||||||
|  | func (r *Request) MarshalJSON() (rawData []byte, err error) { | ||||||
|  | 	var signatureString, dataHashString, rootAddrString, metaHashString string | ||||||
|  | 	if r.signature != nil { | ||||||
|  | 		signatureString = hexutil.Encode(r.signature[:]) | ||||||
|  | 	} | ||||||
|  | 	if r.data != nil { | ||||||
|  | 		dataHashString = hexutil.Encode(r.data) | ||||||
|  | 	} | ||||||
|  | 	if r.rootAddr != nil { | ||||||
|  | 		rootAddrString = hexutil.Encode(r.rootAddr) | ||||||
|  | 	} | ||||||
|  | 	if r.metaHash != nil { | ||||||
|  | 		metaHashString = hexutil.Encode(r.metaHash) | ||||||
|  | 	} | ||||||
|  | 	var ownerAddrString string | ||||||
|  | 	if r.metadata.Frequency == 0 { | ||||||
|  | 		ownerAddrString = "" | ||||||
|  | 	} else { | ||||||
|  | 		ownerAddrString = hexutil.Encode(r.metadata.Owner[:]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	requestJSON := &updateRequestJSON{ | ||||||
|  | 		Name:      r.metadata.Name, | ||||||
|  | 		Frequency: r.metadata.Frequency, | ||||||
|  | 		StartTime: r.metadata.StartTime.Time, | ||||||
|  | 		Version:   r.version, | ||||||
|  | 		Period:    r.period, | ||||||
|  | 		Owner:     ownerAddrString, | ||||||
|  | 		Data:      dataHashString, | ||||||
|  | 		Multihash: r.multihash, | ||||||
|  | 		Signature: signatureString, | ||||||
|  | 		RootAddr:  rootAddrString, | ||||||
|  | 		MetaHash:  metaHashString, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return json.Marshal(requestJSON) | ||||||
|  | } | ||||||
							
								
								
									
										175
									
								
								swarm/storage/mru/request_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								swarm/storage/mru/request_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | |||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func areEqualJSON(s1, s2 string) (bool, error) { | ||||||
|  | 	//credit for the trick: turtlemonvh https://gist.github.com/turtlemonvh/e4f7404e28387fadb8ad275a99596f67 | ||||||
|  | 	var o1 interface{} | ||||||
|  | 	var o2 interface{} | ||||||
|  |  | ||||||
|  | 	err := json.Unmarshal([]byte(s1), &o1) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, fmt.Errorf("Error mashalling string 1 :: %s", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	err = json.Unmarshal([]byte(s2), &o2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, fmt.Errorf("Error mashalling string 2 :: %s", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return reflect.DeepEqual(o1, o2), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestEncodingDecodingUpdateRequests ensures that requests are serialized properly | ||||||
|  | // while also checking cryptographically that only the owner of a resource can update it. | ||||||
|  | func TestEncodingDecodingUpdateRequests(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	signer := newCharlieSigner()  //Charlie, our good guy | ||||||
|  | 	falseSigner := newBobSigner() //Bob will play the bad guy again | ||||||
|  |  | ||||||
|  | 	// Create a resource to our good guy Charlie's name | ||||||
|  | 	createRequest, err := NewCreateRequest(&ResourceMetadata{ | ||||||
|  | 		Name:      "a good resource name", | ||||||
|  | 		Frequency: 300, | ||||||
|  | 		StartTime: Timestamp{Time: 1528900000}, | ||||||
|  | 		Owner:     signer.Address()}) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error creating resource name: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We now encode the create message to simulate we send it over the wire | ||||||
|  | 	messageRawData, err := createRequest.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error encoding create resource request: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// ... the message arrives and is decoded... | ||||||
|  | 	var recoveredCreateRequest Request | ||||||
|  | 	if err := recoveredCreateRequest.UnmarshalJSON(messageRawData); err != nil { | ||||||
|  | 		t.Fatalf("Error decoding create resource request: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// ... but verification should fail because it is not signed! | ||||||
|  | 	if err := recoveredCreateRequest.Verify(); err == nil { | ||||||
|  | 		t.Fatal("Expected Verify to fail since the message is not signed") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We now assume that the resource was created and propagated. With rootAddr we can retrieve the resource metadata | ||||||
|  | 	// and recover the information above. To sign an update, we need the rootAddr and the metaHash to construct | ||||||
|  | 	// proof of ownership | ||||||
|  |  | ||||||
|  | 	metaHash := createRequest.metaHash | ||||||
|  | 	rootAddr := createRequest.rootAddr | ||||||
|  | 	const expectedSignature = "0x1c2bab66dc4ed63783d62934e3a628e517888d6949aef0349f3bd677121db9aa09bbfb865904e6c50360e209e0fe6fe757f8a2474cf1b34169c99b95e3fd5a5101" | ||||||
|  | 	const expectedJSON = `{"rootAddr":"0x6e744a730f7ea0881528576f0354b6268b98e35a6981ef703153ff1b8d32bbef","metaHash":"0x0c0d5c18b89da503af92302a1a64fab6acb60f78e288eb9c3d541655cd359b60","version":1,"period":7,"data":"0x5468697320686f75722773207570646174653a20537761726d2039392e3020686173206265656e2072656c656173656421","multiHash":false}` | ||||||
|  |  | ||||||
|  | 	//Put together an unsigned update request that we will serialize to send it to the signer. | ||||||
|  | 	data := []byte("This hour's update: Swarm 99.0 has been released!") | ||||||
|  | 	request := &Request{ | ||||||
|  | 		SignedResourceUpdate: SignedResourceUpdate{ | ||||||
|  | 			resourceUpdate: resourceUpdate{ | ||||||
|  | 				updateHeader: updateHeader{ | ||||||
|  | 					UpdateLookup: UpdateLookup{ | ||||||
|  | 						period:   7, | ||||||
|  | 						version:  1, | ||||||
|  | 						rootAddr: rootAddr, | ||||||
|  | 					}, | ||||||
|  | 					multihash: false, | ||||||
|  | 					metaHash:  metaHash, | ||||||
|  | 				}, | ||||||
|  | 				data: data, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	messageRawData, err = request.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error encoding update request: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	equalJSON, err := areEqualJSON(string(messageRawData), expectedJSON) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error decoding update request JSON: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if !equalJSON { | ||||||
|  | 		t.Fatalf("Received a different JSON message. Expected %s, got %s", expectedJSON, string(messageRawData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// now the encoded message messageRawData is sent over the wire and arrives to the signer | ||||||
|  |  | ||||||
|  | 	//Attempt to extract an UpdateRequest out of the encoded message | ||||||
|  | 	var recoveredRequest Request | ||||||
|  | 	if err := recoveredRequest.UnmarshalJSON(messageRawData); err != nil { | ||||||
|  | 		t.Fatalf("Error decoding update request: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//sign the request and see if it matches our predefined signature above. | ||||||
|  | 	if err := recoveredRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatalf("Error signing request: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	compareByteSliceToExpectedHex(t, "signature", recoveredRequest.signature[:], expectedSignature) | ||||||
|  |  | ||||||
|  | 	// mess with the signature and see what happens. To alter the signature, we briefly decode it as JSON | ||||||
|  | 	// to alter the signature field. | ||||||
|  | 	var j updateRequestJSON | ||||||
|  | 	if err := json.Unmarshal([]byte(expectedJSON), &j); err != nil { | ||||||
|  | 		t.Fatal("Error unmarshalling test json, check expectedJSON constant") | ||||||
|  | 	} | ||||||
|  | 	j.Signature = "Certainly not a signature" | ||||||
|  | 	corruptMessage, _ := json.Marshal(j) // encode the message with the bad signature | ||||||
|  | 	var corruptRequest Request | ||||||
|  | 	if err = corruptRequest.UnmarshalJSON(corruptMessage); err == nil { | ||||||
|  | 		t.Fatal("Expected DecodeUpdateRequest to fail when trying to interpret a corrupt message with an invalid signature") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Now imagine Evil Bob (why always Bob, poor Bob) attempts to update Charlie's resource, | ||||||
|  | 	// signing a message with his private key | ||||||
|  | 	if err := request.Sign(falseSigner); err != nil { | ||||||
|  | 		t.Fatalf("Error signing: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Now Bob encodes the message to send it over the wire... | ||||||
|  | 	messageRawData, err = request.MarshalJSON() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error encoding message:%s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// ... the message arrives to our Swarm node and it is decoded. | ||||||
|  | 	recoveredRequest = Request{} | ||||||
|  | 	if err := recoveredRequest.UnmarshalJSON(messageRawData); err != nil { | ||||||
|  | 		t.Fatalf("Error decoding message:%s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Before discovering Bob's misdemeanor, let's see what would happen if we mess | ||||||
|  | 	// with the signature big time to see if Verify catches it | ||||||
|  | 	savedSignature := *recoveredRequest.signature                               // save the signature for later | ||||||
|  | 	binary.LittleEndian.PutUint64(recoveredRequest.signature[5:], 556845463424) // write some random data to break the signature | ||||||
|  | 	if err = recoveredRequest.Verify(); err == nil { | ||||||
|  | 		t.Fatal("Expected Verify to fail on corrupt signature") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// restore the Evil Bob's signature from corruption | ||||||
|  | 	*recoveredRequest.signature = savedSignature | ||||||
|  |  | ||||||
|  | 	// Now the signature is not corrupt, however Verify should now fail because Bob doesn't own the resource | ||||||
|  | 	if err = recoveredRequest.Verify(); err == nil { | ||||||
|  | 		t.Fatalf("Expected Verify to fail because this resource belongs to Charlie, not Bob the attacker:%s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Sign with our friend Charlie's private key | ||||||
|  | 	if err := recoveredRequest.Sign(signer); err != nil { | ||||||
|  | 		t.Fatalf("Error signing with the correct private key: %s", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// And now, Verify should work since this resource belongs to Charlie | ||||||
|  | 	if err = recoveredRequest.Verify(); err != nil { | ||||||
|  | 		t.Fatalf("Error verifying that Charlie, the good guy, can sign his resource:%s", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -23,20 +23,44 @@ import ( | |||||||
| 	"github.com/ethereum/go-ethereum/crypto" | 	"github.com/ethereum/go-ethereum/crypto" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Signs resource updates | const signatureLength = 65 | ||||||
|  |  | ||||||
|  | // Signature is an alias for a static byte array with the size of a signature | ||||||
|  | type Signature [signatureLength]byte | ||||||
|  |  | ||||||
|  | // Signer signs Mutable Resource update payloads | ||||||
| type Signer interface { | type Signer interface { | ||||||
| 	Sign(common.Hash) (Signature, error) | 	Sign(common.Hash) (Signature, error) | ||||||
|  | 	Address() common.Address | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GenericSigner implements the Signer interface | ||||||
|  | // It is the vanilla signer that probably should be used in most cases | ||||||
| type GenericSigner struct { | type GenericSigner struct { | ||||||
| 	PrivKey *ecdsa.PrivateKey | 	PrivKey *ecdsa.PrivateKey | ||||||
|  | 	address common.Address | ||||||
| } | } | ||||||
|  |  | ||||||
| func (self *GenericSigner) Sign(data common.Hash) (signature Signature, err error) { | // NewGenericSigner builds a signer that will sign everything with the provided private key | ||||||
| 	signaturebytes, err := crypto.Sign(data.Bytes(), self.PrivKey) | func NewGenericSigner(privKey *ecdsa.PrivateKey) *GenericSigner { | ||||||
|  | 	return &GenericSigner{ | ||||||
|  | 		PrivKey: privKey, | ||||||
|  | 		address: crypto.PubkeyToAddress(privKey.PublicKey), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sign signs the supplied data | ||||||
|  | // It wraps the ethereum crypto.Sign() method | ||||||
|  | func (s *GenericSigner) Sign(data common.Hash) (signature Signature, err error) { | ||||||
|  | 	signaturebytes, err := crypto.Sign(data.Bytes(), s.PrivKey) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	copy(signature[:], signaturebytes) | 	copy(signature[:], signaturebytes) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // PublicKey returns the public key of the signer's private key | ||||||
|  | func (s *GenericSigner) Address() common.Address { | ||||||
|  | 	return s.address | ||||||
|  | } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										184
									
								
								swarm/storage/mru/signedupdate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								swarm/storage/mru/signedupdate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"hash" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common" | ||||||
|  | 	"github.com/ethereum/go-ethereum/crypto" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // SignedResourceUpdate represents a resource update with all the necessary information to prove ownership of the resource | ||||||
|  | type SignedResourceUpdate struct { | ||||||
|  | 	resourceUpdate // actual content that will be put on the chunk, less signature | ||||||
|  | 	signature      *Signature | ||||||
|  | 	updateAddr     storage.Address // resulting chunk address for the update (not serialized, for internal use) | ||||||
|  | 	binaryData     []byte          // resulting serialized data (not serialized, for efficiency/internal use) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Verify checks that signatures are valid and that the signer owns the resource to be updated | ||||||
|  | func (r *SignedResourceUpdate) Verify() (err error) { | ||||||
|  | 	if len(r.data) == 0 { | ||||||
|  | 		return NewError(ErrInvalidValue, "Update does not contain data") | ||||||
|  | 	} | ||||||
|  | 	if r.signature == nil { | ||||||
|  | 		return NewError(ErrInvalidSignature, "Missing signature field") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	digest, err := r.GetDigest() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// get the address of the signer (which also checks that it's a valid signature) | ||||||
|  | 	ownerAddr, err := getOwner(digest, *r.signature) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !bytes.Equal(r.updateAddr, r.UpdateAddr()) { | ||||||
|  | 		return NewError(ErrInvalidSignature, "Signature address does not match with ownerAddr") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if who signed the resource update really owns the resource | ||||||
|  | 	if !verifyOwner(ownerAddr, r.metaHash, r.rootAddr) { | ||||||
|  | 		return NewErrorf(ErrUnauthorized, "signature is valid but signer does not own the resource: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sign executes the signature to validate the resource | ||||||
|  | func (r *SignedResourceUpdate) Sign(signer Signer) error { | ||||||
|  |  | ||||||
|  | 	r.binaryData = nil           //invalidate serialized data | ||||||
|  | 	digest, err := r.GetDigest() // computes digest and serializes into .binaryData | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	signature, err := signer.Sign(digest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Although the Signer interface returns the public address of the signer, | ||||||
|  | 	// recover it from the signature to see if they match | ||||||
|  | 	ownerAddress, err := getOwner(digest, signature) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return NewError(ErrInvalidSignature, "Error verifying signature") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ownerAddress != signer.Address() { // sanity check to make sure the Signer is declaring the same address used to sign! | ||||||
|  | 		return NewError(ErrInvalidSignature, "Signer address does not match ownerAddr") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.signature = &signature | ||||||
|  | 	r.updateAddr = r.UpdateAddr() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // create an update chunk. | ||||||
|  | func (r *SignedResourceUpdate) toChunk() (*storage.Chunk, error) { | ||||||
|  |  | ||||||
|  | 	// Check that the update is signed and serialized | ||||||
|  | 	// For efficiency, data is serialized during signature and cached in | ||||||
|  | 	// the binaryData field when computing the signature digest in .getDigest() | ||||||
|  | 	if r.signature == nil || r.binaryData == nil { | ||||||
|  | 		return nil, NewError(ErrInvalidSignature, "newUpdateChunk called without a valid signature or payload data. Call .Sign() first.") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	chunk := storage.NewChunk(r.updateAddr, nil) | ||||||
|  | 	resourceUpdateLength := r.resourceUpdate.binaryLength() | ||||||
|  | 	chunk.SData = r.binaryData | ||||||
|  |  | ||||||
|  | 	// signature is the last item in the chunk data | ||||||
|  | 	copy(chunk.SData[resourceUpdateLength:], r.signature[:]) | ||||||
|  |  | ||||||
|  | 	chunk.Size = int64(len(chunk.SData)) | ||||||
|  | 	return chunk, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // fromChunk populates this structure from chunk data. It does not verify the signature is valid. | ||||||
|  | func (r *SignedResourceUpdate) fromChunk(updateAddr storage.Address, chunkdata []byte) error { | ||||||
|  | 	// for update chunk layout see SignedResourceUpdate definition | ||||||
|  |  | ||||||
|  | 	//deserialize the resource update portion | ||||||
|  | 	if err := r.resourceUpdate.binaryGet(chunkdata); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Extract the signature | ||||||
|  | 	var signature *Signature | ||||||
|  | 	cursor := r.resourceUpdate.binaryLength() | ||||||
|  | 	sigdata := chunkdata[cursor : cursor+signatureLength] | ||||||
|  | 	if len(sigdata) > 0 { | ||||||
|  | 		signature = &Signature{} | ||||||
|  | 		copy(signature[:], sigdata) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	r.signature = signature | ||||||
|  | 	r.updateAddr = updateAddr | ||||||
|  | 	r.binaryData = chunkdata | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetDigest creates the resource update digest used in signatures (formerly known as keyDataHash) | ||||||
|  | // the serialized payload is cached in .binaryData | ||||||
|  | func (r *SignedResourceUpdate) GetDigest() (result common.Hash, err error) { | ||||||
|  | 	hasher := hashPool.Get().(hash.Hash) | ||||||
|  | 	defer hashPool.Put(hasher) | ||||||
|  | 	hasher.Reset() | ||||||
|  | 	dataLength := r.resourceUpdate.binaryLength() | ||||||
|  | 	if r.binaryData == nil { | ||||||
|  | 		r.binaryData = make([]byte, dataLength+signatureLength) | ||||||
|  | 		if err := r.resourceUpdate.binaryPut(r.binaryData[:dataLength]); err != nil { | ||||||
|  | 			return result, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	hasher.Write(r.binaryData[:dataLength]) //everything except the signature. | ||||||
|  |  | ||||||
|  | 	return common.BytesToHash(hasher.Sum(nil)), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getOwner extracts the address of the resource update signer | ||||||
|  | func getOwner(digest common.Hash, signature Signature) (common.Address, error) { | ||||||
|  | 	pub, err := crypto.SigToPub(digest.Bytes(), signature[:]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return common.Address{}, err | ||||||
|  | 	} | ||||||
|  | 	return crypto.PubkeyToAddress(*pub), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // verifyResourceOwnerhsip checks that the signer of the update actually owns the resource | ||||||
|  | // H(ownerAddr, metaHash) is computed. If it matches the rootAddr the update chunk is claiming | ||||||
|  | // to update, it is proven that signer of the resource update owns the resource. | ||||||
|  | // See metadataHash in metadata.go for a more detailed explanation | ||||||
|  | func verifyOwner(ownerAddr common.Address, metaHash []byte, rootAddr storage.Address) bool { | ||||||
|  | 	hasher := hashPool.Get().(hash.Hash) | ||||||
|  | 	defer hashPool.Put(hasher) | ||||||
|  | 	hasher.Reset() | ||||||
|  | 	hasher.Write(metaHash) | ||||||
|  | 	hasher.Write(ownerAddr.Bytes()) | ||||||
|  | 	rootAddr2 := hasher.Sum(nil) | ||||||
|  | 	return bytes.Equal(rootAddr2, rootAddr) | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								swarm/storage/mru/testutil.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								swarm/storage/mru/testutil.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"path/filepath" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	testDbDirName = "mru" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type TestHandler struct { | ||||||
|  | 	*Handler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *TestHandler) Close() { | ||||||
|  | 	t.chunkStore.Close() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewTestHandler creates Handler object to be used for testing purposes. | ||||||
|  | func NewTestHandler(datadir string, params *HandlerParams) (*TestHandler, error) { | ||||||
|  | 	path := filepath.Join(datadir, testDbDirName) | ||||||
|  | 	rh, err := NewHandler(params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("resource handler create fail: %v", err) | ||||||
|  | 	} | ||||||
|  | 	localstoreparams := storage.NewDefaultLocalStoreParams() | ||||||
|  | 	localstoreparams.Init(path) | ||||||
|  | 	localStore, err := storage.NewLocalStore(localstoreparams, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("localstore create fail, path %s: %v", path, err) | ||||||
|  | 	} | ||||||
|  | 	localStore.Validators = append(localStore.Validators, storage.NewContentAddressValidator(storage.MakeHashFunc(resourceHashAlgorithm))) | ||||||
|  | 	localStore.Validators = append(localStore.Validators, rh) | ||||||
|  | 	netStore := storage.NewNetStore(localStore, nil) | ||||||
|  | 	rh.SetStore(netStore) | ||||||
|  | 	return &TestHandler{rh}, nil | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								swarm/storage/mru/timestampprovider.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								swarm/storage/mru/timestampprovider.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // TimestampProvider sets the time source of the mru package | ||||||
|  | var TimestampProvider timestampProvider = NewDefaultTimestampProvider() | ||||||
|  |  | ||||||
|  | // Encodes a point in time as a Unix epoch | ||||||
|  | type Timestamp struct { | ||||||
|  | 	Time uint64 // Unix epoch timestamp, in seconds | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 8 bytes uint64 Time | ||||||
|  | const timestampLength = 8 | ||||||
|  |  | ||||||
|  | // timestampProvider interface describes a source of timestamp information | ||||||
|  | type timestampProvider interface { | ||||||
|  | 	Now() Timestamp // returns the current timestamp information | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryGet populates the timestamp structure from the given byte slice | ||||||
|  | func (t *Timestamp) binaryGet(data []byte) error { | ||||||
|  | 	if len(data) != timestampLength { | ||||||
|  | 		return NewError(ErrCorruptData, "timestamp data has the wrong size") | ||||||
|  | 	} | ||||||
|  | 	t.Time = binary.LittleEndian.Uint64(data[:8]) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryPut Serializes a Timestamp to a byte slice | ||||||
|  | func (t *Timestamp) binaryPut(data []byte) error { | ||||||
|  | 	if len(data) != timestampLength { | ||||||
|  | 		return NewError(ErrCorruptData, "timestamp data has the wrong size") | ||||||
|  | 	} | ||||||
|  | 	binary.LittleEndian.PutUint64(data, t.Time) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DefaultTimestampProvider struct { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewDefaultTimestampProvider creates a system clock based timestamp provider | ||||||
|  | func NewDefaultTimestampProvider() *DefaultTimestampProvider { | ||||||
|  | 	return &DefaultTimestampProvider{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Now returns the current time according to this provider | ||||||
|  | func (dtp *DefaultTimestampProvider) Now() Timestamp { | ||||||
|  | 	return Timestamp{ | ||||||
|  | 		Time: uint64(time.Now().Unix()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										147
									
								
								swarm/storage/mru/update.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								swarm/storage/mru/update.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"errors" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/log" | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/multihash" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // resourceUpdate encapsulates the information sent as part of a resource update | ||||||
|  | type resourceUpdate struct { | ||||||
|  | 	updateHeader        // metainformationa about this resource update | ||||||
|  | 	data         []byte // actual data payload | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Update chunk layout | ||||||
|  | // Prefix: | ||||||
|  | // 2 bytes updateHeaderLength | ||||||
|  | // 2 bytes data length | ||||||
|  | const chunkPrefixLength = 2 + 2 | ||||||
|  |  | ||||||
|  | // Header: (see updateHeader) | ||||||
|  | // Data: | ||||||
|  | // data (datalength bytes) | ||||||
|  | // | ||||||
|  | // Minimum size is Header + 1 (minimum data length, enforced) | ||||||
|  | const minimumUpdateDataLength = updateHeaderLength + 1 | ||||||
|  | const maxUpdateDataLength = chunkSize - signatureLength - updateHeaderLength - chunkPrefixLength | ||||||
|  |  | ||||||
|  | // binaryPut serializes the resource update information into the given slice | ||||||
|  | func (r *resourceUpdate) binaryPut(serializedData []byte) error { | ||||||
|  | 	datalength := len(r.data) | ||||||
|  | 	if datalength == 0 { | ||||||
|  | 		return NewError(ErrInvalidValue, "cannot update a resource with no data") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if datalength > maxUpdateDataLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "data is too big (length=%d). Max length=%d", datalength, maxUpdateDataLength) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(serializedData) != r.binaryLength() { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "slice passed to putBinary must be of exact size. Expected %d bytes", r.binaryLength()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if r.multihash { | ||||||
|  | 		if _, _, err := multihash.GetMultihashLength(r.data); err != nil { | ||||||
|  | 			return NewError(ErrInvalidValue, "Invalid multihash") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add prefix: updateHeaderLength and actual data length | ||||||
|  | 	cursor := 0 | ||||||
|  | 	binary.LittleEndian.PutUint16(serializedData[cursor:], uint16(updateHeaderLength)) | ||||||
|  | 	cursor += 2 | ||||||
|  |  | ||||||
|  | 	// data length | ||||||
|  | 	binary.LittleEndian.PutUint16(serializedData[cursor:], uint16(datalength)) | ||||||
|  | 	cursor += 2 | ||||||
|  |  | ||||||
|  | 	// serialize header (see updateHeader) | ||||||
|  | 	if err := r.updateHeader.binaryPut(serializedData[cursor : cursor+updateHeaderLength]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cursor += updateHeaderLength | ||||||
|  |  | ||||||
|  | 	// add the data | ||||||
|  | 	copy(serializedData[cursor:], r.data) | ||||||
|  | 	cursor += datalength | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryLength returns the expected number of bytes this structure will take to encode | ||||||
|  | func (r *resourceUpdate) binaryLength() int { | ||||||
|  | 	return chunkPrefixLength + updateHeaderLength + len(r.data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryGet populates this instance from the information contained in the passed byte slice | ||||||
|  | func (r *resourceUpdate) binaryGet(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) < minimumUpdateDataLength { | ||||||
|  | 		return NewErrorf(ErrNothingToReturn, "chunk less than %d bytes cannot be a resource update chunk", minimumUpdateDataLength) | ||||||
|  | 	} | ||||||
|  | 	cursor := 0 | ||||||
|  | 	declaredHeaderlength := binary.LittleEndian.Uint16(serializedData[cursor : cursor+2]) | ||||||
|  | 	if declaredHeaderlength != updateHeaderLength { | ||||||
|  | 		return NewErrorf(ErrCorruptData, "Invalid header length. Expected %d, got %d", updateHeaderLength, declaredHeaderlength) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cursor += 2 | ||||||
|  | 	datalength := int(binary.LittleEndian.Uint16(serializedData[cursor : cursor+2])) | ||||||
|  | 	cursor += 2 | ||||||
|  |  | ||||||
|  | 	if chunkPrefixLength+updateHeaderLength+datalength+signatureLength != len(serializedData) { | ||||||
|  | 		return NewError(ErrNothingToReturn, "length specified in header is different than actual chunk size") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// at this point we can be satisfied that we have the correct data length to read | ||||||
|  | 	if err := r.updateHeader.binaryGet(serializedData[cursor : cursor+updateHeaderLength]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cursor += updateHeaderLength | ||||||
|  |  | ||||||
|  | 	data := serializedData[cursor : cursor+datalength] | ||||||
|  | 	cursor += datalength | ||||||
|  |  | ||||||
|  | 	// if multihash content is indicated we check the validity of the multihash | ||||||
|  | 	if r.updateHeader.multihash { | ||||||
|  | 		mhLength, mhHeaderLength, err := multihash.GetMultihashLength(data) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("multihash parse error", "err", err) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if datalength != mhLength+mhHeaderLength { | ||||||
|  | 			log.Debug("multihash error", "datalength", datalength, "mhLength", mhLength, "mhHeaderLength", mhHeaderLength) | ||||||
|  | 			return errors.New("Corrupt multihash data") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// now that all checks have passed, copy data into structure | ||||||
|  | 	r.data = make([]byte, datalength) | ||||||
|  | 	copy(r.data, data) | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Multihash specifies whether the resource data should be interpreted as multihash | ||||||
|  | func (r *resourceUpdate) Multihash() bool { | ||||||
|  | 	return r.multihash | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								swarm/storage/mru/update_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								swarm/storage/mru/update_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const serializedUpdateHex = "0x490034004f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf000456c20717565206c6565206d7563686f207920616e6461206d7563686f2c207665206d7563686f20792073616265206d7563686f" | ||||||
|  | const serializedUpdateMultihashHex = "0x490022004f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf0011b200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1c1e1f20" | ||||||
|  |  | ||||||
|  | func getTestResourceUpdate() *resourceUpdate { | ||||||
|  | 	return &resourceUpdate{ | ||||||
|  | 		updateHeader: *getTestUpdateHeader(false), | ||||||
|  | 		data:         []byte("El que lee mucho y anda mucho, ve mucho y sabe mucho"), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getTestResourceUpdateMultihash() *resourceUpdate { | ||||||
|  | 	return &resourceUpdate{ | ||||||
|  | 		updateHeader: *getTestUpdateHeader(true), | ||||||
|  | 		data:         []byte{0x1b, 0x20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 28, 30, 31, 32}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func compareResourceUpdate(a, b *resourceUpdate) bool { | ||||||
|  | 	return compareUpdateHeader(&a.updateHeader, &b.updateHeader) && | ||||||
|  | 		bytes.Equal(a.data, b.data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestResourceUpdateSerializer(t *testing.T) { | ||||||
|  | 	var serializedUpdateLength = len(serializedUpdateHex)/2 - 1 // hack to calculate the byte length out of the hex representation | ||||||
|  | 	update := getTestResourceUpdate() | ||||||
|  | 	serializedUpdate := make([]byte, serializedUpdateLength) | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	compareByteSliceToExpectedHex(t, "serializedUpdate", serializedUpdate, serializedUpdateHex) | ||||||
|  |  | ||||||
|  | 	// Test fail if update does not contain data | ||||||
|  | 	update.data = nil | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err == nil { | ||||||
|  | 		t.Fatal("Expected resourceUpdate.binaryPut to fail since update does not contain data") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test fail if update is too big | ||||||
|  | 	update.data = make([]byte, 10000) | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err == nil { | ||||||
|  | 		t.Fatal("Expected resourceUpdate.binaryPut to fail since update is too big") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test fail if passed slice is not of the exact size required for this update | ||||||
|  | 	update.data = make([]byte, 1) | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err == nil { | ||||||
|  | 		t.Fatal("Expected resourceUpdate.binaryPut to fail since passed slice is not of the appropriate size") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test serializing a multihash update | ||||||
|  | 	var serializedUpdateMultihashLength = len(serializedUpdateMultihashHex)/2 - 1 // hack to calculate the byte length out of the hex representation | ||||||
|  | 	update = getTestResourceUpdateMultihash() | ||||||
|  | 	serializedUpdate = make([]byte, serializedUpdateMultihashLength) | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	compareByteSliceToExpectedHex(t, "serializedUpdate", serializedUpdate, serializedUpdateMultihashHex) | ||||||
|  |  | ||||||
|  | 	// mess with the multihash to test it fails with a wrong multihash error | ||||||
|  | 	update.data[1] = 79 | ||||||
|  | 	if err := update.binaryPut(serializedUpdate); err == nil { | ||||||
|  | 		t.Fatal("Expected resourceUpdate.binaryPut to fail since data contains an invalid multihash") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										88
									
								
								swarm/storage/mru/updateheader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								swarm/storage/mru/updateheader.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | // Copyright 2018 The go-ethereum Authors | ||||||
|  | // This file is part of the go-ethereum library. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Lesser General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // The go-ethereum library is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
|  | // GNU Lesser General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Lesser General Public License | ||||||
|  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/ethereum/go-ethereum/swarm/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // updateHeader models the non-payload components of a Resource Update | ||||||
|  | type updateHeader struct { | ||||||
|  | 	UpdateLookup        // UpdateLookup contains the information required to locate this resource (components of the search key used to find it) | ||||||
|  | 	multihash    bool   // Whether the data in this Resource Update should be interpreted as multihash | ||||||
|  | 	metaHash     []byte // SHA3 hash of the metadata chunk (less ownerAddr). Used to prove ownerhsip of the resource. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const metaHashLength = storage.KeyLength | ||||||
|  |  | ||||||
|  | // updateLookupLength bytes | ||||||
|  | // 1 byte flags (multihash bool for now) | ||||||
|  | // 32 bytes metaHash | ||||||
|  | const updateHeaderLength = updateLookupLength + 1 + metaHashLength | ||||||
|  |  | ||||||
|  | // binaryPut serializes the resource header information into the given slice | ||||||
|  | func (h *updateHeader) binaryPut(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) != updateHeaderLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Incorrect slice size to serialize updateHeaderLength. Expected %d, got %d", updateHeaderLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  | 	if len(h.metaHash) != metaHashLength { | ||||||
|  | 		return NewError(ErrInvalidValue, "updateHeader.binaryPut called without metaHash set") | ||||||
|  | 	} | ||||||
|  | 	if err := h.UpdateLookup.binaryPut(serializedData[:updateLookupLength]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cursor := updateLookupLength | ||||||
|  | 	copy(serializedData[cursor:], h.metaHash[:metaHashLength]) | ||||||
|  | 	cursor += metaHashLength | ||||||
|  |  | ||||||
|  | 	var flags byte | ||||||
|  | 	if h.multihash { | ||||||
|  | 		flags |= 0x01 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	serializedData[cursor] = flags | ||||||
|  | 	cursor++ | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryLength returns the expected size of this structure when serialized | ||||||
|  | func (h *updateHeader) binaryLength() int { | ||||||
|  | 	return updateHeaderLength | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // binaryGet restores the current updateHeader instance from the information contained in the passed slice | ||||||
|  | func (h *updateHeader) binaryGet(serializedData []byte) error { | ||||||
|  | 	if len(serializedData) != updateHeaderLength { | ||||||
|  | 		return NewErrorf(ErrInvalidValue, "Incorrect slice size to read updateHeaderLength. Expected %d, got %d", updateHeaderLength, len(serializedData)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := h.UpdateLookup.binaryGet(serializedData[:updateLookupLength]); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	cursor := updateLookupLength | ||||||
|  | 	h.metaHash = make([]byte, metaHashLength) | ||||||
|  | 	copy(h.metaHash[:storage.KeyLength], serializedData[cursor:cursor+storage.KeyLength]) | ||||||
|  | 	cursor += metaHashLength | ||||||
|  |  | ||||||
|  | 	flags := serializedData[cursor] | ||||||
|  | 	cursor++ | ||||||
|  |  | ||||||
|  | 	h.multihash = flags&0x01 != 0 | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								swarm/storage/mru/updateheader_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								swarm/storage/mru/updateheader_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | package mru | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/ethereum/go-ethereum/common/hexutil" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const serializedUpdateHeaderMultihashHex = "0x4f000000da070000fb0ed7efa696bdb0b54cd75554cc3117ffc891454317df7dd6fefad978e2f2fbf74a10ce8f26ffc8bfaa07c3031a34b2c61f517955e7deb1592daccf96c69cf001" | ||||||
|  |  | ||||||
|  | func getTestUpdateHeader(multihash bool) (header *updateHeader) { | ||||||
|  | 	_, metaHash, _, _ := getTestMetadata().serializeAndHash() | ||||||
|  | 	return &updateHeader{ | ||||||
|  | 		UpdateLookup: *getTestUpdateLookup(), | ||||||
|  | 		multihash:    multihash, | ||||||
|  | 		metaHash:     metaHash, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func compareUpdateHeader(a, b *updateHeader) bool { | ||||||
|  | 	return compareUpdateLookup(&a.UpdateLookup, &b.UpdateLookup) && | ||||||
|  | 		a.multihash == b.multihash && | ||||||
|  | 		bytes.Equal(a.metaHash, b.metaHash) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateHeaderSerializer(t *testing.T) { | ||||||
|  | 	header := getTestUpdateHeader(true) | ||||||
|  | 	serializedHeader := make([]byte, updateHeaderLength) | ||||||
|  | 	if err := header.binaryPut(serializedHeader); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	compareByteSliceToExpectedHex(t, "serializedHeader", serializedHeader, serializedUpdateHeaderMultihashHex) | ||||||
|  |  | ||||||
|  | 	// trigger incorrect slice length error passing a slice that is 1 byte too big | ||||||
|  | 	if err := header.binaryPut(make([]byte, updateHeaderLength+1)); err == nil { | ||||||
|  | 		t.Fatal("Expected updateHeader.binaryPut to fail since supplied slice is of incorrect length") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// trigger invalid metaHash error | ||||||
|  | 	header.metaHash = nil | ||||||
|  | 	if err := header.binaryPut(serializedHeader); err == nil { | ||||||
|  | 		t.Fatal("Expected updateHeader.binaryPut to fail metaHash is of incorrect length") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateHeaderDeserializer(t *testing.T) { | ||||||
|  | 	originalUpdate := getTestUpdateHeader(true) | ||||||
|  | 	serializedData, _ := hexutil.Decode(serializedUpdateHeaderMultihashHex) | ||||||
|  | 	var retrievedUpdate updateHeader | ||||||
|  | 	if err := retrievedUpdate.binaryGet(serializedData); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if !compareUpdateHeader(originalUpdate, &retrievedUpdate) { | ||||||
|  | 		t.Fatalf("Expected deserialized structure to equal the original") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// mess with source slice to test length checks | ||||||
|  | 	serializedData = []byte{1, 2, 3} | ||||||
|  | 	if err := retrievedUpdate.binaryGet(serializedData); err == nil { | ||||||
|  | 		t.Fatal("Expected retrievedUpdate.binaryGet, since passed slice is too small") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -192,25 +192,8 @@ func NewSwarm(config *api.Config, mockStore *mock.NodeStore) (self *Swarm, err e | |||||||
| 	self.fileStore = storage.NewFileStore(netStore, self.config.FileStoreParams) | 	self.fileStore = storage.NewFileStore(netStore, self.config.FileStoreParams) | ||||||
|  |  | ||||||
| 	var resourceHandler *mru.Handler | 	var resourceHandler *mru.Handler | ||||||
| 	rhparams := &mru.HandlerParams{ | 	rhparams := &mru.HandlerParams{} | ||||||
| 		// TODO: config parameter to set limits |  | ||||||
| 		QueryMaxPeriods: &mru.LookupParams{ |  | ||||||
| 			Limit: false, |  | ||||||
| 		}, |  | ||||||
| 		Signer: &mru.GenericSigner{ |  | ||||||
| 			PrivKey: self.privateKey, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	if resolver != nil { |  | ||||||
| 		resolver.SetNameHash(ens.EnsNode) |  | ||||||
| 		// Set HeaderGetter and OwnerValidator interfaces to resolver only if it is not nil. |  | ||||||
| 		rhparams.HeaderGetter = resolver |  | ||||||
| 		rhparams.OwnerValidator = resolver |  | ||||||
| 	} else { |  | ||||||
| 		log.Warn("No ETH API specified, resource updates will use block height approximation") |  | ||||||
| 		// TODO: blockestimator should use saved values derived from last time ethclient was connected |  | ||||||
| 		rhparams.HeaderGetter = mru.NewBlockEstimator() |  | ||||||
| 	} |  | ||||||
| 	resourceHandler, err = mru.NewHandler(rhparams) | 	resourceHandler, err = mru.NewHandler(rhparams) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|   | |||||||
| @@ -17,15 +17,15 @@ | |||||||
| package testutil | package testutil | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"math/big" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"os" | 	"os" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/ethereum/go-ethereum/core/types" | 	"github.com/ethereum/go-ethereum/metrics" | ||||||
|  | 	"github.com/ethereum/go-ethereum/metrics/influxdb" | ||||||
| 	"github.com/ethereum/go-ethereum/swarm/api" | 	"github.com/ethereum/go-ethereum/swarm/api" | ||||||
| 	"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" | ||||||
| @@ -35,16 +35,17 @@ type TestServer interface { | |||||||
| 	ServeHTTP(http.ResponseWriter, *http.Request) | 	ServeHTTP(http.ResponseWriter, *http.Request) | ||||||
| } | } | ||||||
|  |  | ||||||
| type fakeBackend struct { | // simulated timeProvider | ||||||
| 	blocknumber int64 | type fakeTimeProvider struct { | ||||||
|  | 	currentTime uint64 | ||||||
| } | } | ||||||
|  |  | ||||||
| func (f *fakeBackend) HeaderByNumber(context context.Context, _ string, bigblock *big.Int) (*types.Header, error) { | func (f *fakeTimeProvider) Tick() { | ||||||
| 	f.blocknumber++ | 	f.currentTime++ | ||||||
| 	biggie := big.NewInt(f.blocknumber) | } | ||||||
| 	return &types.Header{ |  | ||||||
| 		Number: biggie, | func (f *fakeTimeProvider) Now() mru.Timestamp { | ||||||
| 	}, nil | 	return mru.Timestamp{Time: f.currentTime} | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewTestSwarmServer(t *testing.T, serverFunc func(*api.API) TestServer) *TestSwarmServer { | func NewTestSwarmServer(t *testing.T, serverFunc func(*api.API) TestServer) *TestSwarmServer { | ||||||
| @@ -68,24 +69,25 @@ func NewTestSwarmServer(t *testing.T, serverFunc func(*api.API) TestServer) *Tes | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	rhparams := &mru.HandlerParams{ |  | ||||||
| 		QueryMaxPeriods: &mru.LookupParams{}, | 	fakeTimeProvider := &fakeTimeProvider{ | ||||||
| 		HeaderGetter: &fakeBackend{ | 		currentTime: 42, | ||||||
| 			blocknumber: 42, |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
|  | 	mru.TimestampProvider = fakeTimeProvider | ||||||
|  | 	rhparams := &mru.HandlerParams{} | ||||||
| 	rh, err := mru.NewTestHandler(resourceDir, rhparams) | 	rh, err := mru.NewTestHandler(resourceDir, rhparams) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	a := api.NewAPI(fileStore, nil, rh) | 	a := api.NewAPI(fileStore, nil, rh.Handler) | ||||||
| 	srv := httptest.NewServer(serverFunc(a)) | 	srv := httptest.NewServer(serverFunc(a)) | ||||||
| 	return &TestSwarmServer{ | 	return &TestSwarmServer{ | ||||||
| 		Server:            srv, | 		Server:            srv, | ||||||
| 		FileStore:         fileStore, | 		FileStore:         fileStore, | ||||||
| 		dir:               dir, | 		dir:               dir, | ||||||
| 		Hasher:            storage.MakeHashFunc(storage.DefaultHash)(), | 		Hasher:            storage.MakeHashFunc(storage.DefaultHash)(), | ||||||
|  | 		timestampProvider: fakeTimeProvider, | ||||||
| 		cleanup: func() { | 		cleanup: func() { | ||||||
| 			srv.Close() | 			srv.Close() | ||||||
| 			rh.Close() | 			rh.Close() | ||||||
| @@ -101,8 +103,21 @@ type TestSwarmServer struct { | |||||||
| 	FileStore         *storage.FileStore | 	FileStore         *storage.FileStore | ||||||
| 	dir               string | 	dir               string | ||||||
| 	cleanup           func() | 	cleanup           func() | ||||||
|  | 	timestampProvider *fakeTimeProvider | ||||||
| } | } | ||||||
|  |  | ||||||
| func (t *TestSwarmServer) Close() { | func (t *TestSwarmServer) Close() { | ||||||
| 	t.cleanup() | 	t.cleanup() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (t *TestSwarmServer) GetCurrentTime() mru.Timestamp { | ||||||
|  | 	return t.timestampProvider.Now() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // EnableMetrics is starting InfluxDB reporter so that we collect stats when running tests locally | ||||||
|  | func EnableMetrics() { | ||||||
|  | 	metrics.Enabled = true | ||||||
|  | 	go influxdb.InfluxDBWithTags(metrics.DefaultRegistry, 1*time.Second, "http://localhost:8086", "metrics", "admin", "admin", "swarm.", map[string]string{ | ||||||
|  | 		"host": "test", | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user