diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 590f4ba03b..63c497c280 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -40,10 +40,6 @@ type resource struct { // The update scheme is built on swarm chunks with chunk keys following // a predictable, versionable pattern. // -// The data of the chunk contains the content hash of the version in question. -// In order to be valid, the hash is signed by the owner of the ENS record -// of the mutable resource. -// // Updates are defined to be periodic in nature, where periods are // expressed in terms of number of blocks. // @@ -56,7 +52,7 @@ type resource struct { // The root entry tells the requester from when the mutable resource was // first added (block number) and in which block number to look for the -// actual updates. Thus, a resource update for identifier "foo.bar" +// actual updates. Thus, a resource update for identifier "føø.bar" // starting at block 4200 with frequency 42 will have updates on block 4242, // 4284, 4326 and so on. // @@ -66,27 +62,28 @@ type resource struct { // // Note that the root entry is not required for the resource update scheme to // work. A normal chunk of the blocknumber/frequency data can also be created, -// and pointed to by an actual ENS entry (or manifest entry) instead. +// and pointed to by an external resource (ENS or manifest entry) // // 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: // -// sha256(namehash|blocknumber|version) +// sha256(namehash|period|version) // -// The blocknumber here is the next block period after the current block -// calculated from the start block and frequency of the resource update. -// Using our previous example, this means that an update made at block 4285, -// and even 4284, will have 4326 as the block number. +// The period is (currentblock - startblock) / frequency +// +// Using our previous example, this means that a period 3 will have 4326 as +// the block number. // // If more than one update is made to the same block number, incremental // version numbers are used successively. // // A lookup agent need only know the identifier name in order to get the versions // -// the data itself is prefixed with a signed hash of the data. The sigining key -// is used to verify the authenticity of the update, for example by looking -// up the ownership of the namehash in ENS and comparing to the address derived -// from it +// the chunk data is: sign(resourcedata)|resourcedata +// the resourcedata is: headerlength|period|version|name|data +// +// headerlength is a 16 bit value containing the byte length of period|version|name +// period and version are both 32 bit values. name can have arbitrary length // // NOTE: the following is yet to be implemented // The resource update chunks will be stored in the swarm, but receive special @@ -94,8 +91,6 @@ type resource struct { // stored using a separate store, and forwarding/syncing protocols carry per-chunk // flags to tell whether the chunk can be validated or not; if not it is to be // treated as a resource update chunk. -// -// TODO: signature validation type ResourceHandler interface { ChunkStore NewResource(name string, frequency uint64) (*resource, error) diff --git a/swarm/storage/resource_ens.go b/swarm/storage/resource_ens.go index 8b0738b3c3..d13e663e1e 100644 --- a/swarm/storage/resource_ens.go +++ b/swarm/storage/resource_ens.go @@ -2,24 +2,31 @@ package storage import ( "crypto/ecdsa" + "fmt" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/contracts/ens" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rpc" ) +// Implements Mutable Resources as offchain ENS resolvers +// +// The data part of the update is forced to be a valid ENS content hash +// +// Also, the ENSResourceHandler only allows creation and update of +// Resources from the ENS owner's address +// type ENSResourceHandler struct { - ResourceHandler - ethapi *ethclient.Client + *RawResourceHandler + addr common.Address ensapi *ens.ENS } -func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, ensAddr common.Address) (*ENSResourceHandler, error) { +func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, backend bind.ContractBackend, ensAddr common.Address) (*ENSResourceHandler, error) { transactOpts := bind.NewKeyedTransactor(privKey) - ethapi := ethclient.NewClient(rpcClient) - ensinstance, err := ens.NewENS(transactOpts, ensAddr, ethapi) + ensinstance, err := ens.NewENS(transactOpts, ensAddr, backend) if err != nil { return nil, err } @@ -32,8 +39,19 @@ func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore } return &ENSResourceHandler{ - ResourceHandler: rh, - ethapi: ethclient.NewClient(rpcClient), - ensapi: ensinstance, + RawResourceHandler: rh, + addr: crypto.PubkeyToAddress(privKey.PublicKey), + ensapi: ensinstance, }, nil } + +func (self *ENSResourceHandler) NewResource(name string, frequency uint64) (*resource, error) { + owneraddr, err := self.ensapi.Owner(self.RawResourceHandler.nameHashFunc(name)) + if err != nil { + return nil, fmt.Errorf("ENS error: %v", err) + } + if owneraddr != self.addr { + return nil, fmt.Errorf("not owner") + } + return self.RawResourceHandler.NewResource(name, frequency) +} diff --git a/swarm/storage/resource_test.go b/swarm/storage/resource_test.go index 585cd71936..f72188254b 100644 --- a/swarm/storage/resource_test.go +++ b/swarm/storage/resource_test.go @@ -41,6 +41,8 @@ func init() { log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))) } +// simulated backend does not have the blocknumber call +// so we use this wrapper to fake returning the block count type fakeBackend struct { *backends.SimulatedBackend blocknumber uint64 @@ -317,14 +319,21 @@ func TestResourceHandler(t *testing.T) { } -func TestResourceENS(t *testing.T) { +func TestResourceENSNew(t *testing.T) { - // privkey for signing updates + // privkey for ens owner privkey, err := crypto.GenerateKey() if err != nil { return } + // privkey for signing updates + privkeytwo, err := crypto.GenerateKey() + if err != nil { + return + } + + // set up ENS sim domainparts := strings.Split(domainName, ".") addr, contractbackend, err := setupENS(privkey, domainparts[0], domainparts[1]) if err != nil { @@ -336,14 +345,31 @@ func TestResourceENS(t *testing.T) { teardownTest(t, err) } + // create new resource when we are owner = ok _, err = rh.NewResource(domainName, 42) if err != nil { teardownTest(t, err) } + // create new resource when we are NOT owner = !ok + rawrh := rh.(*ENSResourceHandler) + rawrh.privKey = privkeytwo + rawrh.addr = crypto.PubkeyToAddress(privkeytwo.PublicKey) + _, err = rawrh.NewResource(domainName, 42) + if err == nil { + teardownTest(t, fmt.Errorf("Expected resource create fail due to owner mismatch")) + } + teardownTest(t, nil) } +// fast-forward blockheight +func fwdBlocks(count int, backend *fakeBackend) { + for i := 0; i < count; i++ { + backend.Commit() + } +} + func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, ensaddr common.Address) (rh ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) { var fsClean func() @@ -394,8 +420,9 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, return } + // choose if with ens or not if ensaddr != zeroAddr { - rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, ensaddr) + rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, contractbackend, ensaddr) } else { rh, err = NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, nil) } @@ -408,7 +435,11 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, return } + +// Set up simulated ENS backend for use with ENSResourceHandler tests func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address, bind.ContractBackend, error) { + + // create the domain hash values to pass to the ENS contract methods var tophash [32]byte var subhash [32]byte hashfunc.Reset() @@ -417,17 +448,22 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address hashfunc.Reset() hashfunc.Write([]byte(sub)) copy(subhash[:], hashfunc.Sum(nil)) + + // private key -> address is owner of domain addr := crypto.PubkeyToAddress(privkey.PublicKey) + + // initialize contract backend and deploy + transactOpts := bind.NewKeyedTransactor(privkey) contractBackend := &fakeBackend{ SimulatedBackend: backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}}), } - transactOpts := bind.NewKeyedTransactor(privkey) ensAddr, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend) if err != nil { return zeroAddr, nil, fmt.Errorf("can't deploy: %v", err) } + // update the registry for the correct owner address if _, err = ensinstance.SetOwner(transactOpts, [32]byte{}, addr); err != nil { return zeroAddr, nil, fmt.Errorf("can't setowner: %v", err) } @@ -443,13 +479,6 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address } contractBackend.Commit() - nodeowner, err := ensinstance.Owner(&bind.CallOpts{}, ens.EnsNode(strings.Join([]string{sub, top}, "."))) - if err != nil { - return zeroAddr, nil, fmt.Errorf("can't retrieve owner: %v", err) - } else if !bytes.Equal(nodeowner.Bytes(), addr.Bytes()) { - return zeroAddr, nil, fmt.Errorf("retrieved owner doesn't match; expected '%x', got '%x'", addr, nodeowner) - } - return ensAddr, contractBackend, nil }