swarm/storage: Change validator, subclassing -> member

This commit is contained in:
lash 2018-01-17 03:54:14 +01:00
parent c45ce79937
commit 52e529e04b
3 changed files with 112 additions and 119 deletions

View file

@ -91,15 +91,15 @@ 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.
type ResourceHandler interface {
ChunkStore
NewResource(name string, frequency uint64) (*resource, error)
Update(name string, data []byte) (Key, error)
resourceHash(namehash common.Hash, period uint32, version uint32) Key
type ResourceValidator interface {
isOwner(string) (bool, error)
nameHash(string) common.Hash
}
type RawResourceHandler struct {
type ResourceHandler struct {
ChunkStore
validator ResourceValidator
rpcClient *rpc.Client
resources map[string]*resource
hashLock sync.Mutex
@ -107,11 +107,10 @@ type RawResourceHandler struct {
hasher SwarmHash
privKey *ecdsa.PrivateKey
maxChunkData int64
nameHashFunc func(string) common.Hash
}
// Create or open resource update chunk store
func NewRawResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, nameHashFunc func(string) common.Hash) (*RawResourceHandler, error) {
func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) {
path := filepath.Join(datadir, "resource")
dbStore, err := NewDbStore(datadir, nil, singletonSwarmDbCapacity, 0)
if err != nil {
@ -123,7 +122,7 @@ func NewRawResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore
}
hasher := MakeHashFunc("SHA3")
rh := &RawResourceHandler{
rh := &ResourceHandler{
ChunkStore: newResourceChunkStore(path, hasher, localStore, cloudStore),
rpcClient: rpcClient,
resources: make(map[string]*resource),
@ -132,14 +131,16 @@ func NewRawResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore
maxChunkData: DefaultBranches * int64(hasher().Size()),
}
if nameHashFunc == nil {
rh.nameHashFunc = func(name string) common.Hash {
if validator != nil {
rh.validator = validator
} else {
rh.validator = NewGenericValidator(func(name string) common.Hash {
rh.hashLock.Lock()
defer rh.hashLock.Unlock()
rh.hasher.Reset()
rh.hasher.Write([]byte(name))
return common.BytesToHash(rh.hasher.Sum(nil))
}
})
}
return rh, nil
@ -186,14 +187,21 @@ func NewResource(name string, startBlock uint64, frequency uint64, nameHashFunc
// Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`.
//
// The start block of the resource update will be the actual current block height of the connected network.
func (self *RawResourceHandler) NewResource(name string, frequency uint64) (*resource, error) {
func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resource, error) {
ok, err := self.validator.isOwner(name)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("Not owner of '%s'", name)
}
validname, err := validateInput(name, frequency)
if err != nil {
return nil, err
}
nameHash := self.nameHashFunc(validname)
nameHash := self.validator.nameHash(validname)
// get our blockheight at this time
currentblock, err := self.getBlock()
@ -236,7 +244,7 @@ func (self *RawResourceHandler) NewResource(name string, frequency uint64) (*res
//
// Method will fail if resource is already registered in this session, unless
// `allowOverwrite` is set
func (self *RawResourceHandler) SetExternalResource(rsrc *resource, allowOverwrite bool) error {
func (self *ResourceHandler) SetExternalResource(rsrc *resource, allowOverwrite bool) error {
utfname, err := idna.ToUnicode(rsrc.name)
if err != nil {
@ -273,7 +281,7 @@ func (self *RawResourceHandler) SetExternalResource(rsrc *resource, allowOverwri
// root chunk.
// It is the callers responsibility to make sure that this chunk exists (if the resource
// update root data was retrieved externally, it typically doesn't)
func (self *RawResourceHandler) LookupVersion(name string, period uint32, version uint32, refresh bool) (*resource, error) {
func (self *ResourceHandler) LookupVersion(name string, period uint32, version uint32, refresh bool) (*resource, error) {
rsrc, err := self.loadResource(name, refresh)
if err != nil {
return nil, err
@ -289,7 +297,7 @@ func (self *RawResourceHandler) LookupVersion(name string, period uint32, versio
// and returned.
//
// See also (*ResourceHandler).LookupVersion
func (self *RawResourceHandler) LookupHistorical(name string, period uint32, refresh bool) (*resource, error) {
func (self *ResourceHandler) LookupHistorical(name string, period uint32, refresh bool) (*resource, error) {
rsrc, err := self.loadResource(name, refresh)
if err != nil {
return nil, err
@ -307,7 +315,7 @@ func (self *RawResourceHandler) LookupHistorical(name string, period uint32, ref
// Version iteration is done as in (*ResourceHandler).LookupHistorical
//
// See also (*ResourceHandler).LookupHistorical
func (self *RawResourceHandler) LookupLatest(name string, refresh bool) (*resource, error) {
func (self *ResourceHandler) LookupLatest(name string, refresh bool) (*resource, error) {
// get our blockheight at this time and the next block of the update period
rsrc, err := self.loadResource(name, refresh)
@ -323,7 +331,7 @@ func (self *RawResourceHandler) LookupLatest(name string, refresh bool) (*resour
}
// base code for public lookup methods
func (self *RawResourceHandler) lookup(rsrc *resource, name string, period uint32, version uint32, refresh bool) (*resource, error) {
func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, version uint32, refresh bool) (*resource, error) {
if period == 0 {
return nil, fmt.Errorf("period must be >0")
@ -366,7 +374,7 @@ func (self *RawResourceHandler) lookup(rsrc *resource, name string, period uint3
}
// load existing mutable resource into resource struct
func (self *RawResourceHandler) loadResource(name string, refresh bool) (*resource, error) {
func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource, error) {
// if the resource is not known to this session we must load it
// if refresh is set, we force load
@ -379,7 +387,7 @@ func (self *RawResourceHandler) loadResource(name string, refresh bool) (*resour
return nil, err
}
rsrc.name = validname
rsrc.nameHash = self.nameHashFunc(validname)
rsrc.nameHash = self.validator.nameHash(validname)
// get the root info chunk and update the cached value
chunk, err := self.Get(Key(rsrc.nameHash[:]))
@ -409,7 +417,7 @@ func (self *RawResourceHandler) loadResource(name string, refresh bool) (*resour
}
// update mutable resource index map with specified content
func (self *RawResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, indexname *string) (*resource, error) {
func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, indexname *string) (*resource, error) {
// rsrc update data chunks are total hacks
// and have no size prefix :D
@ -455,7 +463,14 @@ func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, da
// It is the caller's responsibility to make sure that this data is not stale.
//
// A resource update cannot span chunks, and thus has max length 4096
func (self *RawResourceHandler) Update(name string, data []byte) (Key, error) {
func (self *ResourceHandler) Update(name string, data []byte) (Key, error) {
ok, err := self.validator.isOwner(name)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("Not owner of '%s'", name)
}
// can be only one chunk long minus 65 byte signature
if int64(len(data)) > self.maxChunkData {
@ -527,11 +542,11 @@ func (self *RawResourceHandler) Update(name string, data []byte) (Key, error) {
// Closes the datastore.
// Always call this at shutdown to avoid data corruption.
func (self *RawResourceHandler) Close() {
func (self *ResourceHandler) Close() {
self.ChunkStore.Close()
}
func (self *RawResourceHandler) getBlock() (uint64, error) {
func (self *ResourceHandler) getBlock() (uint64, error) {
// get the block height and convert to uint64
var currentblock string
err := self.rpcClient.Call(&currentblock, "eth_blockNumber")
@ -544,28 +559,28 @@ func (self *RawResourceHandler) getBlock() (uint64, error) {
return strconv.ParseUint(currentblock, 10, 64)
}
func (self *RawResourceHandler) BlockToPeriod(name string, blocknumber uint64) uint32 {
func (self *ResourceHandler) BlockToPeriod(name string, blocknumber uint64) uint32 {
return getNextPeriod(self.resources[name].startBlock, blocknumber, self.resources[name].frequency)
}
func (self *RawResourceHandler) PeriodToBlock(name string, period uint32) uint64 {
func (self *ResourceHandler) PeriodToBlock(name string, period uint32) uint64 {
return self.resources[name].startBlock + (uint64(period) * self.resources[name].frequency)
}
func (self *RawResourceHandler) getResource(name string) *resource {
func (self *ResourceHandler) getResource(name string) *resource {
self.resourceLock.RLock()
defer self.resourceLock.RUnlock()
rsrc := self.resources[name]
return rsrc
}
func (self *RawResourceHandler) setResource(name string, rsrc *resource) {
func (self *ResourceHandler) setResource(name string, rsrc *resource) {
self.resourceLock.Lock()
defer self.resourceLock.Unlock()
self.resources[name] = rsrc
}
func (self *RawResourceHandler) resourceHash(namehash common.Hash, period uint32, version uint32) Key {
func (self *ResourceHandler) resourceHash(namehash common.Hash, period uint32, version uint32) Key {
// format is: hash(namehash|period|version)
self.hashLock.Lock()
defer self.hashLock.Unlock()
@ -579,7 +594,7 @@ func (self *RawResourceHandler) resourceHash(namehash common.Hash, period uint32
return self.hasher.Sum(nil)
}
func (self *RawResourceHandler) signContent(data []byte) ([]byte, error) {
func (self *ResourceHandler) signContent(data []byte) ([]byte, error) {
self.hashLock.Lock()
self.hasher.Reset()
self.hasher.Write(data)
@ -596,7 +611,7 @@ func (self *RawResourceHandler) signContent(data []byte) ([]byte, error) {
return datawithsign, nil
}
func (self *RawResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) {
func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) {
if len(chunkdata) <= signatureLength {
return common.Address{}, fmt.Errorf("zero-length data")
}
@ -612,7 +627,7 @@ func (self *RawResourceHandler) getContentAccount(chunkdata []byte) (common.Addr
return crypto.PubkeyToAddress(*pub), nil
}
func (self *RawResourceHandler) verifyContent(chunkdata []byte) error {
func (self *ResourceHandler) verifyContent(chunkdata []byte) error {
address, err := self.getContentAccount(chunkdata)
if err != nil {
return err
@ -621,7 +636,7 @@ func (self *RawResourceHandler) verifyContent(chunkdata []byte) error {
return nil
}
func (self *RawResourceHandler) hasUpdate(name string, period uint32) bool {
func (self *ResourceHandler) hasUpdate(name string, period uint32) bool {
if self.resources[name].lastPeriod == period {
return true
}

View file

@ -1,74 +1,54 @@
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/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 {
*RawResourceHandler
addr common.Address
ensapi *ens.ENS
// ENS validation of mutable resource owners
type ENSValidator struct {
owner common.Address
api *ens.ENS
}
func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, backend bind.ContractBackend, ensAddr common.Address) (*ENSResourceHandler, error) {
transactOpts := bind.NewKeyedTransactor(privKey)
ensinstance, err := ens.NewENS(transactOpts, ensAddr, backend)
func NewENSValidator(owneraddress common.Address, contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts) (*ENSValidator, error) {
var err error
validator := &ENSValidator{}
validator.api, err = ens.NewENS(transactOpts, contractaddress, backend)
if err != nil {
return nil, err
}
rh, err := NewRawResourceHandler(privKey, datadir, cloudStore, rpcClient, ens.EnsNode)
if err != nil {
return nil, err
validator.owner = owneraddress
return validator, nil
}
rh.nameHashFunc = func(name string) common.Hash {
func (self *ENSValidator) isOwner(name string) (bool, error) {
owneraddr, err := self.api.Owner(self.nameHash(name))
if err != nil {
return false, err
}
return owneraddr == self.owner, nil
}
func (self *ENSValidator) nameHash(name string) common.Hash {
return ens.EnsNode(name)
}
return &ENSResourceHandler{
RawResourceHandler: rh,
addr: crypto.PubkeyToAddress(privKey.PublicKey),
ensapi: ensinstance,
}, nil
// Default fallthrough validation of mutable resource ownership
type GenericValidator struct {
hashFunc func(string) common.Hash
}
func (self *ENSResourceHandler) NewResource(name string, frequency uint64) (*resource, error) {
ok, err := self.IsOwner(name)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("Not Owner")
func NewGenericValidator(hashFunc func(string) common.Hash) *GenericValidator {
return &GenericValidator{
hashFunc: hashFunc,
}
return self.RawResourceHandler.NewResource(name, frequency)
}
func (self *GenericValidator) isOwner(name string) (bool, error) {
return true, nil
}
func (self *ENSResourceHandler) Update(name string, data []byte) (Key, error) {
ok, err := self.IsOwner(name)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("Not Owner")
}
return self.RawResourceHandler.Update(name, data)
}
func (self *ENSResourceHandler) IsOwner(name string) (bool, error) {
owneraddr, err := self.ensapi.Owner(self.RawResourceHandler.nameHashFunc(name))
if err != nil {
return false, fmt.Errorf("ENS error: %v", err)
}
return owneraddr == self.addr, nil
func (self *GenericValidator) nameHash(name string) common.Hash {
return self.hashFunc(name)
}

View file

@ -74,7 +74,7 @@ func TestResourceSignature(t *testing.T) {
}
// set up rpc and create resourcehandler
rh, _, err, teardownTest := setupTest(privkey, nil, zeroAddr)
rh, _, err, teardownTest := setupTest(privkey, nil, nil)
if err != nil {
teardownTest(t, err)
}
@ -110,8 +110,7 @@ func TestResourceSignature(t *testing.T) {
// check that we can recover the owner account from the update chunk's signature
// TODO: change this to verifyContent on ENS integration
rawrh := rh.(*RawResourceHandler)
recoveredaddress, err := rawrh.getContentAccount(chunk.SData)
recoveredaddress, err := rh.getContentAccount(chunk.SData)
if err != nil {
teardownTest(t, err)
}
@ -136,7 +135,7 @@ func TestResourceReverseLookup(t *testing.T) {
backend := &fakeBackend{
blocknumber: startBlock,
}
rh, _, err, teardownTest := setupTest(privkey, backend, zeroAddr)
rh, _, err, teardownTest := setupTest(privkey, backend, nil)
if err != nil {
teardownTest(t, err)
}
@ -154,8 +153,7 @@ func TestResourceReverseLookup(t *testing.T) {
if err != nil {
teardownTest(t, err)
}
rawrh := rh.(*RawResourceHandler)
chunk, err := rawrh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey))
chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey))
if err != nil {
teardownTest(t, err)
}
@ -197,7 +195,7 @@ func TestResourceHandler(t *testing.T) {
backend := &fakeBackend{
blocknumber: startBlock,
}
rh, datadir, err, teardownTest := setupTest(privkey, backend, zeroAddr)
rh, datadir, err, teardownTest := setupTest(privkey, backend, nil)
if err != nil {
teardownTest(t, err)
}
@ -213,9 +211,8 @@ func TestResourceHandler(t *testing.T) {
}
// check that the new resource is stored correctly
rawrh := rh.(*RawResourceHandler)
namehash := rawrh.nameHashFunc(resourcevalidname)
chunk, err := rawrh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(namehash[:]))
namehash := rh.validator.nameHash(resourcevalidname)
chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(namehash[:]))
if err != nil {
teardownTest(t, err)
} else if len(chunk.SData) < 16 {
@ -265,7 +262,7 @@ func TestResourceHandler(t *testing.T) {
// it will match on second iteration startblocknumber + (resourceFrequency * 3)
fwdBlocks(int(resourceFrequency*2)-1, backend)
rh2, err := NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rawrh.rpcClient, nil)
rh2, err := NewResourceHandler(privkey, datadir, &testCloudStore{}, rh.rpcClient, nil)
_, err = rh2.LookupLatest(domainName, true)
if err != nil {
teardownTest(t, err)
@ -282,7 +279,7 @@ func TestResourceHandler(t *testing.T) {
teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[domainName].lastPeriod))
}
rsrc, err := NewResource(domainName, startblocknumber, resourceFrequency, rh2.nameHashFunc)
rsrc, err := NewResource(domainName, startblocknumber, resourceFrequency, rh2.validator.nameHash)
if err != nil {
teardownTest(t, err)
}
@ -342,15 +339,24 @@ func TestResourceENSOwner(t *testing.T) {
return
}
// ens address and transact options
addr := crypto.PubkeyToAddress(privkey.PublicKey)
transactOpts := bind.NewKeyedTransactor(privkey)
// set up ENS sim
domainparts := strings.Split(domainName, ".")
addr, contractbackend, err := setupENS(privkey, domainparts[0], domainparts[1])
contractAddr, contractbackend, err := setupENS(addr, transactOpts, domainparts[0], domainparts[1])
if err != nil {
t.Fatal(err)
}
validator, err := NewENSValidator(addr, contractAddr, contractbackend, transactOpts)
if err != nil {
t.Fatal(err)
}
// set up rpc and create resourcehandler with ENS sim backend
rh, _, err, teardownTest := setupTest(privkey, contractbackend, addr)
rh, _, err, teardownTest := setupTest(privkey, contractbackend, validator)
if err != nil {
teardownTest(t, err)
}
@ -358,20 +364,20 @@ func TestResourceENSOwner(t *testing.T) {
// create new resource when we are owner = ok
_, err = rh.NewResource(domainName, 42)
if err != nil {
teardownTest(t, err)
teardownTest(t, fmt.Errorf("Create resource fail: %v", err))
}
// update resource when we are owner = ok
_, err = rh.Update(domainName, []byte("foo"))
if err != nil {
teardownTest(t, err)
teardownTest(t, fmt.Errorf("Update resource fail: %v", 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)
addrtwo := crypto.PubkeyToAddress(privkeytwo.PublicKey)
validator.owner = addrtwo
_, err = rh.NewResource(domainName, 42)
if err == nil {
teardownTest(t, fmt.Errorf("Expected resource create fail due to owner mismatch"))
}
@ -392,7 +398,7 @@ func fwdBlocks(count int, backend *fakeBackend) {
}
// create rpc and resourcehandler
func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, ensaddr common.Address) (rh ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) {
func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, validator ResourceValidator) (rh *ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) {
var fsClean func()
var rpcClean func()
@ -443,11 +449,7 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
}
// choose if with ens or not
if ensaddr != zeroAddr {
rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, contractbackend, ensaddr)
} else {
rh, err = NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, nil)
}
rh, err = NewResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, validator)
teardown = func(t *testing.T, err error) {
cleanF()
if err != nil {
@ -459,7 +461,7 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
}
// Set up simulated ENS backend for use with ENSResourceHandler tests
func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address, bind.ContractBackend, error) {
func setupENS(addr common.Address, transactOpts *bind.TransactOpts, 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
@ -472,16 +474,12 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address
hasher.Write([]byte(sub))
copy(subhash[:], hasher.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)}}),
}
ensAddr, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend)
contractAddress, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend)
if err != nil {
return zeroAddr, nil, fmt.Errorf("can't deploy: %v", err)
}
@ -502,7 +500,7 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address
}
contractBackend.Commit()
return ensAddr, contractBackend, nil
return contractAddress, contractBackend, nil
}
type testCloudStore struct {