Merge pull request #200 from ethersphere/swarm-mutableresources-extsign

swarm/storage: Externalize signatures of Resource updates
This commit is contained in:
lash 2018-01-23 15:20:36 +01:00 committed by GitHub
commit f68e83683d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 471 additions and 459 deletions

View file

@ -1,7 +1,6 @@
package storage package storage
import ( import (
"crypto/ecdsa"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"path/filepath" "path/filepath"
@ -18,14 +17,23 @@ import (
) )
const ( const (
signatureLength = 65 signatureLength = 65
indexSize = 24 indexSize = 16
dbDirName = "resource"
chunkSize = 4096 // temporary until we implement DPA in the resourcehandler
defaultStoreTimeout = 4000 * time.Millisecond
) )
// Encapsulates an actual resource update. When synced it contains the most recent type Signature [signatureLength]byte
type SignFunc func(common.Hash) (Signature, error)
type nameHashFunc func(string) common.Hash
// Encapsulates an specific resource update. When synced it contains the most recent
// version of the resource update data. // version of the resource update data.
type resource struct { type resource struct {
name string name *string
nameHash common.Hash nameHash common.Hash
startBlock uint64 startBlock uint64
lastPeriod uint32 lastPeriod uint32
@ -35,6 +43,19 @@ type resource struct {
updated time.Time updated time.Time
} }
// TODO Expire content after a defined period (to force resync)
func (r *resource) isSynced() bool {
return !r.updated.IsZero()
}
// Implement to activate validation of resource updates
// Specifically signing data and verification of signatures
type ResourceValidator interface {
checkAccess(string, common.Address) (bool, error)
nameHash(string) common.Hash // nameHashFunc
sign(common.Hash) (Signature, error) // SignFunc
}
// Mutable resource is an entity which allows updates to a resource // Mutable resource is an entity which allows updates to a resource
// without resorting to ENS on each update. // without resorting to ENS on each update.
// The update scheme is built on swarm chunks with chunk keys following // The update scheme is built on swarm chunks with chunk keys following
@ -44,8 +65,9 @@ type resource struct {
// expressed in terms of number of blocks. // expressed in terms of number of blocks.
// //
// The root entry of a mutable resource is tied to a unique identifier, // The root entry of a mutable resource is tied to a unique identifier,
// typically - but not necessarily - an ens name. It also contains the // typically - but not necessarily - an ens name. The identifier must be
// block number when the resource update was first registered, and // an valid IDNA string. It also contains the block number
// when the resource update was first registered, and
// the block frequency with which the resource will be updated, both of // the block frequency with which the resource will be updated, both of
// which are stored as little-endian uint64 values in the database (for a // which are stored as little-endian uint64 values in the database (for a
// total of 16 bytes). // total of 16 bytes).
@ -56,10 +78,6 @@ type resource struct {
// starting at block 4200 with frequency 42 will have updates on block 4242, // starting at block 4200 with frequency 42 will have updates on block 4242,
// 4284, 4326 and so on. // 4284, 4326 and so on.
// //
// The identifier is supplied as a string, but will be IDNA converted and
// passed through the ENS namehash function. Pure ascii identifiers without
// periods will thus merely be hashed.
//
// Note that the root entry is not required for the resource update scheme to // 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, // work. A normal chunk of the blocknumber/frequency data can also be created,
// and pointed to by an external resource (ENS or manifest entry) // and pointed to by an external resource (ENS or manifest entry)
@ -67,7 +85,7 @@ type resource struct {
// Actual data updates are also made in the form of swarm chunks. The keys // 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: // of the updates are the hash of a concatenation of properties as follows:
// //
// sha256(namehash|period|version) // sha256(period|version|namehash)
// //
// The period is (currentblock - startblock) / frequency // The period is (currentblock - startblock) / frequency
// //
@ -79,8 +97,12 @@ type resource struct {
// //
// A lookup agent need only know the identifier name in order to get the versions // A lookup agent need only know the identifier name in order to get the versions
// //
// the chunk data is: sign(resourcedata)|resourcedata // the resourcedata is:
// the resourcedata is: headerlength|period|version|name|data // headerlength|period|version|identifier|data
//
// if a validator is active, the chunk data is:
// sign(resourcedata)|resourcedata
// otherwise, the chunk data is the same as the resourcedata
// //
// headerlength is a 16 bit value containing the byte length of period|version|name // 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 // period and version are both 32 bit values. name can have arbitrary length
@ -91,12 +113,6 @@ type resource struct {
// stored using a separate store, and forwarding/syncing protocols carry per-chunk // 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 // flags to tell whether the chunk can be validated or not; if not it is to be
// treated as a resource update chunk. // treated as a resource update chunk.
type ResourceValidator interface {
isOwner(string) (bool, error)
nameHash(string) common.Hash
}
type ResourceHandler struct { type ResourceHandler struct {
ChunkStore ChunkStore
validator ResourceValidator validator ResourceValidator
@ -105,14 +121,19 @@ type ResourceHandler struct {
hashLock sync.Mutex hashLock sync.Mutex
resourceLock sync.RWMutex resourceLock sync.RWMutex
hasher SwarmHash hasher SwarmHash
privKey *ecdsa.PrivateKey nameHash nameHashFunc
maxChunkData int64 storeTimeout time.Duration
} }
// Create or open resource update chunk store // Create or open resource update chunk store
func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) { //
path := filepath.Join(datadir, "resource") // If validator is nil, signature and access validation will be deactivated
dbStore, err := NewDbStore(datadir, nil, singletonSwarmDbCapacity, 0) func NewResourceHandler(datadir string, cloudStore CloudStore, rpcClient *rpc.Client, validator ResourceValidator) (*ResourceHandler, error) {
hashfunc := MakeHashFunc(SHA3Hash)
path := filepath.Join(datadir, dbDirName)
dbStore, err := NewDbStore(datadir, hashfunc, singletonSwarmDbCapacity, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -120,88 +141,70 @@ func NewResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore Cl
memStore: NewMemStore(dbStore, singletonSwarmDbCapacity), memStore: NewMemStore(dbStore, singletonSwarmDbCapacity),
DbStore: dbStore, DbStore: dbStore,
} }
hasher := MakeHashFunc("SHA3")
rh := &ResourceHandler{ rh := &ResourceHandler{
ChunkStore: newResourceChunkStore(path, hasher, localStore, cloudStore), ChunkStore: newResourceChunkStore(path, hashfunc, localStore, cloudStore),
rpcClient: rpcClient, rpcClient: rpcClient,
resources: make(map[string]*resource), resources: make(map[string]*resource),
hasher: hasher(), hasher: hashfunc(),
privKey: privKey, validator: validator,
maxChunkData: DefaultBranches * int64(hasher().Size()), storeTimeout: defaultStoreTimeout,
} }
if validator != nil { if rh.validator != nil {
rh.validator = validator rh.nameHash = rh.validator.nameHash
} else { } else {
rh.validator = NewGenericValidator(func(name string) common.Hash { rh.nameHash = func(name string) common.Hash {
rh.hashLock.Lock() rh.hashLock.Lock()
defer rh.hashLock.Unlock() defer rh.hashLock.Unlock()
rh.hasher.Reset() rh.hasher.Reset()
rh.hasher.Write([]byte(name)) rh.hasher.Write([]byte(name))
return common.BytesToHash(rh.hasher.Sum(nil)) return common.BytesToHash(rh.hasher.Sum(nil))
}) }
} }
return rh, nil return rh, nil
} }
func validateInput(name string, frequency uint64) (string, error) { // \TODO should be hashsize * branches from the chosen chunker, implement with dpa
// frequency 0 is invalid func (self *ResourceHandler) chunkSize() int64 {
if frequency == 0 { return chunkSize
return "", fmt.Errorf("Frequency cannot be 0")
}
// must have name
if name == "" {
return "", fmt.Errorf("Name cannot be empty")
}
// make sure our ens identifier is idna safe
validname, err := idna.ToASCII(name)
if err != nil {
return "", err
}
return validname, nil
}
// Creates a standalone resource object
//
// Can be passed to SetResource if external root data lookups are used
func NewResource(name string, startBlock uint64, frequency uint64, nameHashFunc func(name string) common.Hash) (*resource, error) {
validname, err := validateInput(name, frequency)
if err != nil {
return nil, err
}
return &resource{
name: validname,
nameHash: nameHashFunc(validname),
startBlock: startBlock,
frequency: frequency,
}, nil
} }
// Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`. // Creates a new root entry for a mutable resource identified by `name` with the specified `frequency`.
// //
// The signature data should match the hash of the idna-converted name by the validator's namehash function, NOT the raw name bytes.
//
// The start block of the resource update will be the actual current block height of the connected network. // The start block of the resource update will be the actual current block height of the connected network.
func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resource, error) { func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resource, error) {
ok, err := self.validator.isOwner(name) // frequency 0 is invalid
if err != nil { if frequency == 0 {
return nil, err return nil, fmt.Errorf("Frequency cannot be 0")
} else if !ok {
return nil, fmt.Errorf("Not owner of '%s'", name)
} }
validname, err := validateInput(name, frequency) if !isSafeName(name) {
if err != nil { return nil, fmt.Errorf("Invalid name: '%s'", name)
return nil, err
} }
nameHash := self.validator.nameHash(validname) nameHash := self.nameHash(name)
if self.validator != nil {
signature, err := self.validator.sign(nameHash)
if err != nil {
return nil, fmt.Errorf("Sign fail: %v", err)
}
addr, err := getAddressFromDataSig(nameHash, signature)
if err != nil {
return nil, fmt.Errorf("Retrieve address from signature fail: %v", err)
}
ok, err := self.validator.checkAccess(name, addr)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("Not owner of '%s'", name)
}
}
// get our blockheight at this time // get our blockheight at this time
currentblock, err := self.getBlock() currentblock, err := self.getBlock()
@ -211,22 +214,19 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resour
// chunk with key equal to namehash points to data of first blockheight + update frequency // chunk with key equal to namehash points to data of first blockheight + update frequency
// from this we know from what blockheight we should look for updates, and how often // from this we know from what blockheight we should look for updates, and how often
chunk := NewChunk(Key(nameHash[:]), nil) chunk := NewChunk(Key(nameHash.Bytes()), nil)
chunk.SData = make([]byte, indexSize) chunk.SData = make([]byte, indexSize)
// resource update root chunks follow same convention as "normal" chunks
// with 8 bytes prefix specifying size
val := make([]byte, 8) val := make([]byte, 8)
chunk.SData[0] = 16 // size, little-endian
binary.LittleEndian.PutUint64(val, currentblock) binary.LittleEndian.PutUint64(val, currentblock)
copy(chunk.SData[8:16], val) copy(chunk.SData[:8], val)
binary.LittleEndian.PutUint64(val, frequency) binary.LittleEndian.PutUint64(val, frequency)
copy(chunk.SData[16:], val) copy(chunk.SData[8:], val)
self.Put(chunk) self.Put(chunk)
log.Debug("new resource", "name", validname, "key", nameHash, "startBlock", currentblock, "frequency", frequency) log.Debug("new resource", "name", name, "key", nameHash, "startBlock", currentblock, "frequency", frequency)
rsrc := &resource{ rsrc := &resource{
name: validname, name: &name,
nameHash: nameHash, nameHash: nameHash,
startBlock: currentblock, startBlock: currentblock,
frequency: frequency, frequency: frequency,
@ -234,43 +234,7 @@ func (self *ResourceHandler) NewResource(name string, frequency uint64) (*resour
} }
self.setResource(name, rsrc) self.setResource(name, rsrc)
return self.resources[name], nil return rsrc, nil
}
// Set an externally defined resource object
//
// If the resource update root chunk is located externally (for example as a normal
// chunk looked up by ENS) the data would be manually added with this method).
//
// Method will fail if resource is already registered in this session, unless
// `allowOverwrite` is set
func (self *ResourceHandler) SetExternalResource(rsrc *resource, allowOverwrite bool) error {
utfname, err := idna.ToUnicode(rsrc.name)
if err != nil {
return fmt.Errorf("Invalid IDNA rsrc name '%s'", rsrc.name)
}
if !allowOverwrite {
self.resourceLock.Lock()
_, ok := self.resources[utfname]
self.resourceLock.Unlock()
if ok {
return fmt.Errorf("Resource exists")
}
}
// get our blockheight at this time
currentblock, err := self.getBlock()
if err != nil {
return err
}
if rsrc.startBlock > currentblock {
return fmt.Errorf("Startblock cannot be higher than current block (%d > %d)", rsrc.startBlock, currentblock)
}
self.resources[utfname] = rsrc
return nil
} }
// Searches and retrieves the specific version of the resource update identified by `name` // Searches and retrieves the specific version of the resource update identified by `name`
@ -286,7 +250,7 @@ func (self *ResourceHandler) LookupVersion(name string, period uint32, version u
if err != nil { if err != nil {
return nil, err return nil, err
} }
return self.lookup(rsrc, name, period, version, refresh) return self.lookup(rsrc, period, version, refresh)
} }
// Retrieves the latest version of the resource update identified by `name` // Retrieves the latest version of the resource update identified by `name`
@ -302,7 +266,7 @@ func (self *ResourceHandler) LookupHistorical(name string, period uint32, refres
if err != nil { if err != nil {
return nil, err return nil, err
} }
return self.lookup(rsrc, name, period, 0, refresh) return self.lookup(rsrc, period, 0, refresh)
} }
// Retrieves the latest version of the resource update identified by `name` // Retrieves the latest version of the resource update identified by `name`
@ -327,11 +291,11 @@ func (self *ResourceHandler) LookupLatest(name string, refresh bool) (*resource,
return nil, err return nil, err
} }
nextperiod := getNextPeriod(rsrc.startBlock, currentblock, rsrc.frequency) nextperiod := getNextPeriod(rsrc.startBlock, currentblock, rsrc.frequency)
return self.lookup(rsrc, name, nextperiod, 0, refresh) return self.lookup(rsrc, nextperiod, 0, refresh)
} }
// base code for public lookup methods // base code for public lookup methods
func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32, version uint32, refresh bool) (*resource, error) { func (self *ResourceHandler) lookup(rsrc *resource, period uint32, version uint32, refresh bool) (*resource, error) {
if period == 0 { if period == 0 {
return nil, fmt.Errorf("period must be >0") return nil, fmt.Errorf("period must be >0")
@ -347,20 +311,20 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32,
} }
for period > 0 { for period > 0 {
key := self.resourceHash(rsrc.nameHash, period, version) key := self.resourceHash(period, version, rsrc.nameHash)
chunk, err := self.Get(key) chunk, err := self.Get(key)
if err == nil { if err == nil {
if specificversion { if specificversion {
return self.updateResourceIndex(rsrc, chunk, &name) return self.updateResourceIndex(rsrc, chunk)
} }
// check if we have versions > 1. If a version fails, the previous version is used and returned. // 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", period, "key", key) log.Trace("rsrc update version 1 found, checking for version updates", "period", period, "key", key)
for { for {
newversion := version + 1 newversion := version + 1
key := self.resourceHash(rsrc.nameHash, period, newversion) key := self.resourceHash(period, newversion, rsrc.nameHash)
newchunk, err := self.Get(key) newchunk, err := self.Get(key)
if err != nil { if err != nil {
return self.updateResourceIndex(rsrc, chunk, &name) return self.updateResourceIndex(rsrc, chunk)
} }
log.Trace("version update found, checking next", "version", version, "period", period, "key", key) log.Trace("version update found, checking next", "version", version, "period", period, "key", key)
chunk = newchunk chunk = newchunk
@ -375,19 +339,18 @@ func (self *ResourceHandler) lookup(rsrc *resource, name string, period uint32,
// load existing mutable resource into resource struct // load existing mutable resource into resource struct
func (self *ResourceHandler) 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 the resource is not known to this session we must load it
// if refresh is set, we force load // if refresh is set, we force load
rsrc := self.getResource(name) rsrc := self.getResource(name)
if rsrc == nil || refresh { if rsrc == nil || refresh {
rsrc = &resource{} rsrc = &resource{}
// make sure our ens identifier is idna safe // make sure our name is safe to use
validname, err := idna.ToASCII(name) if !isSafeName(name) {
if err != nil { return nil, fmt.Errorf("Invalid name '%s'", name)
return nil, err
} }
rsrc.name = validname rsrc.name = &name
rsrc.nameHash = self.validator.nameHash(validname) rsrc.nameHash = self.nameHash(name)
// get the root info chunk and update the cached value // get the root info chunk and update the cached value
chunk, err := self.Get(Key(rsrc.nameHash[:])) chunk, err := self.Get(Key(rsrc.nameHash[:]))
@ -395,18 +358,12 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource,
return nil, err return nil, err
} }
// sanity check for chunk data // minimum sanity check for chunk data
// data is prefixed by 8 bytes of size if len(chunk.SData) != indexSize {
if len(chunk.SData) < indexSize { return nil, fmt.Errorf("Invalid chunk length %d, should be %d", len(chunk.SData), indexSize)
return nil, fmt.Errorf("Invalid chunk length %d", len(chunk.SData))
} else {
chunklength := binary.LittleEndian.Uint64(chunk.SData[:8])
if chunklength != uint64(16) {
return nil, fmt.Errorf("Invalid chunk length header %d", chunklength)
}
} }
rsrc.startBlock = binary.LittleEndian.Uint64(chunk.SData[8:16]) rsrc.startBlock = binary.LittleEndian.Uint64(chunk.SData[:8])
rsrc.frequency = binary.LittleEndian.Uint64(chunk.SData[16:]) rsrc.frequency = binary.LittleEndian.Uint64(chunk.SData[8:])
} else { } else {
rsrc.name = self.resources[name].name rsrc.name = self.resources[name].name
rsrc.nameHash = self.resources[name].nameHash rsrc.nameHash = self.resources[name].nameHash
@ -417,44 +374,69 @@ func (self *ResourceHandler) loadResource(name string, refresh bool) (*resource,
} }
// update mutable resource index map with specified content // update mutable resource index map with specified content
func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk, indexname *string) (*resource, error) { func (self *ResourceHandler) updateResourceIndex(rsrc *resource, chunk *Chunk) (*resource, error) {
// rsrc update data chunks are total hacks // retrieve metadata from chunk data and check that it matches this mutable resource
// and have no size prefix :D signature, period, version, name, data, err := self.parseUpdate(chunk.SData)
err := self.verifyContent(chunk.SData) if *rsrc.name != name {
if err != nil { return nil, fmt.Errorf("Update belongs to '%s', but have '%s'", name, *rsrc.name)
return nil, err }
// only check signature if validator is present
if self.validator != nil {
digest := self.keyDataHash(chunk.Key, data)
_, err = getAddressFromDataSig(digest, *signature)
if err != nil {
return nil, fmt.Errorf("Invalid signature: %v", err)
}
} }
// update our rsrcs entry map // update our rsrcs entry map
period, version, _, data, err := parseUpdate(chunk.SData[signatureLength:])
rsrc.lastPeriod = period rsrc.lastPeriod = period
rsrc.version = version rsrc.version = version
rsrc.updated = time.Now() rsrc.updated = time.Now()
rsrc.data = make([]byte, len(data)) rsrc.data = make([]byte, len(data))
copy(rsrc.data, data) copy(rsrc.data, data)
log.Debug("Resource synced", "name", rsrc.name, "key", chunk.Key, "period", rsrc.lastPeriod, "version", rsrc.version) log.Debug("Resource synced", "name", *rsrc.name, "key", chunk.Key, "period", rsrc.lastPeriod, "version", rsrc.version)
self.setResource(*indexname, rsrc) self.setResource(*rsrc.name, rsrc)
return rsrc, nil return rsrc, nil
} }
func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, data []byte, err error) { // retrieve update metadata from chunk data
headerlength := binary.LittleEndian.Uint16(blob[:2]) // mirrors newUpdateChunk()
if int(headerlength+2) > len(blob) { func (self *ResourceHandler) parseUpdate(chunkdata []byte) (*Signature, uint32, uint32, string, []byte, error) {
return 0, 0, nil, nil, fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(blob)) var err error
cursor := 0
var signature *Signature
// omit signatures if we have no validator
var sigoffset int
if self.validator != nil {
signature = &Signature{}
copy(signature[:], chunkdata[:signatureLength])
sigoffset = signatureLength
cursor = sigoffset
} }
cursor := 2
period = binary.LittleEndian.Uint32(blob[cursor : cursor+4]) headerlength := binary.LittleEndian.Uint16(chunkdata[cursor : cursor+2])
if int(headerlength+2) > len(chunkdata) {
err = fmt.Errorf("Reported header length %d longer than actual data length %d", headerlength, len(chunkdata))
return nil, 0, 0, "", nil, err
}
var period uint32
var version uint32
var name string
var data []byte
cursor += 2
period = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4])
cursor += 4 cursor += 4
version = binary.LittleEndian.Uint32(blob[cursor : cursor+4]) version = binary.LittleEndian.Uint32(chunkdata[cursor : cursor+4])
cursor += 4 cursor += 4
namelength := int(headerlength) - cursor + 2 namelength := int(headerlength) - cursor + sigoffset + 2
ensname = make([]byte, namelength) name = string(chunkdata[cursor : cursor+namelength])
copy(ensname, blob[cursor:])
cursor += namelength cursor += namelength
data = make([]byte, len(blob)-cursor) data = make([]byte, len(chunkdata)-cursor)
copy(data, blob[cursor:]) copy(data, chunkdata[cursor:])
return return signature, period, version, name, data, err
} }
// Adds an actual data update // Adds an actual data update
@ -465,26 +447,24 @@ func parseUpdate(blob []byte) (period uint32, version uint32, ensname []byte, da
// A resource update cannot span chunks, and thus has max length 4096 // A resource update cannot span chunks, and thus has max length 4096
func (self *ResourceHandler) Update(name string, data []byte) (Key, error) { func (self *ResourceHandler) Update(name string, data []byte) (Key, error) {
ok, err := self.validator.isOwner(name) var sigoffset int
if err != nil { if self.validator != nil {
return nil, err sigoffset = signatureLength
} 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 {
return nil, fmt.Errorf("Data overflow: %d / %d bytes", len(data), 4096-signatureLength)
} }
// get the cached information // get the cached information
self.resourceLock.Lock() rsrc := self.getResource(name)
defer self.resourceLock.Unlock() if rsrc == nil {
resource, ok := self.resources[name] return nil, fmt.Errorf("Resource object not in index")
if !ok { }
return nil, fmt.Errorf("No such resource") if !rsrc.isSynced() {
} else if resource.updated.IsZero() { return nil, fmt.Errorf("Resource object not in sync")
return nil, fmt.Errorf("Invalid resource") }
// an update can be only one chunk long
datalimit := self.chunkSize() - int64(sigoffset-len(name)-8)
if int64(len(data)) > datalimit {
return nil, fmt.Errorf("Data overflow: %d / %d bytes", len(data), datalimit)
} }
// get our blockheight at this time and the next block of the update period // get our blockheight at this time and the next block of the update period
@ -492,51 +472,61 @@ func (self *ResourceHandler) Update(name string, data []byte) (Key, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
nextperiod := getNextPeriod(resource.startBlock, currentblock, resource.frequency) nextperiod := getNextPeriod(rsrc.startBlock, currentblock, rsrc.frequency)
// if we already have an update for this block then increment version // if we already have an update for this block then increment version
// (resource object MUST be in sync for version to be correct)
var version uint32 var version uint32
if self.hasUpdate(name, nextperiod) { if self.hasUpdate(name, nextperiod) {
version = resource.version version = rsrc.version
} }
version++ version++
// prepend version and period to allow reverse lookups // calculate the chunk key
// data header length does NOT include the header length prefix bytes themselves key := self.resourceHash(nextperiod, version, rsrc.nameHash)
headerlength := uint16(len(resource.nameHash) + 4 + 4)
fulldata := make([]byte, int(headerlength)+2+len(data))
cursor := 0 var signature *Signature
binary.LittleEndian.PutUint16(fulldata, headerlength) if self.validator != nil {
cursor += 2 // sign the data hash with the key
digest := self.keyDataHash(key, data)
sig, err := self.validator.sign(digest)
if err != nil {
return nil, err
}
signature = &sig
binary.LittleEndian.PutUint32(fulldata[cursor:], nextperiod) // get the address of the signer (which also checks that it's a valid signature)
cursor += 4 addr, err := getAddressFromDataSig(digest, *signature)
if err != nil {
return nil, fmt.Errorf("Invalid data/signature: %v", err)
}
binary.LittleEndian.PutUint32(fulldata[cursor:], version) // check if the signer has access to update
cursor += 4 ok, err := self.validator.checkAccess(name, addr)
if err != nil {
copy(fulldata[cursor:], resource.nameHash[:]) return nil, err
cursor += len(resource.nameHash) } else if !ok {
return nil, fmt.Errorf("Address %x does not have access to update %s", addr, name)
copy(fulldata[cursor:], data) }
// create the update chunk and send it
key := self.resourceHash(resource.nameHash, nextperiod, version)
chunk := NewChunk(key, nil)
chunk.SData, err = self.signContent(fulldata)
if err != nil {
return nil, err
} }
chunk.Size = int64(len(fulldata))
chunk := newUpdateChunk(key, signature, nextperiod, version, name, data)
// send the chunk
self.Put(chunk) self.Put(chunk)
log.Trace("resource update", "name", resource.name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData) timeout := time.NewTimer(self.storeTimeout)
select {
case <-chunk.dbStored:
case <-timeout.C:
}
log.Trace("resource update", "name", name, "key", key, "currentblock", currentblock, "lastperiod", nextperiod, "version", version, "data", chunk.SData)
// update our resources map entry and return the new key // update our resources map entry and return the new key
resource.lastPeriod = nextperiod rsrc.lastPeriod = nextperiod
resource.version = version rsrc.version = version
resource.data = make([]byte, len(data)) rsrc.data = make([]byte, len(data))
copy(resource.data, data) copy(rsrc.data, data)
return key, nil return key, nil
} }
@ -559,10 +549,12 @@ func (self *ResourceHandler) getBlock() (uint64, error) {
return strconv.ParseUint(currentblock, 10, 64) return strconv.ParseUint(currentblock, 10, 64)
} }
// Calculate the period index (aka major version number) from a given block number
func (self *ResourceHandler) 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) return getNextPeriod(self.resources[name].startBlock, blocknumber, self.resources[name].frequency)
} }
// Calculate the block number from a given period index (aka major version number)
func (self *ResourceHandler) 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) return self.resources[name].startBlock + (uint64(period) * self.resources[name].frequency)
} }
@ -580,69 +572,82 @@ func (self *ResourceHandler) setResource(name string, rsrc *resource) {
self.resources[name] = rsrc self.resources[name] = rsrc
} }
func (self *ResourceHandler) resourceHash(namehash common.Hash, period uint32, version uint32) Key { // used for chunk keys
// format is: hash(namehash|period|version) func (self *ResourceHandler) resourceHash(period uint32, version uint32, namehash common.Hash) Key {
// format is: hash(period|version|namehash)
self.hashLock.Lock() self.hashLock.Lock()
defer self.hashLock.Unlock() defer self.hashLock.Unlock()
self.hasher.Reset() self.hasher.Reset()
self.hasher.Write(namehash[:])
b := make([]byte, 4) b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, period) binary.LittleEndian.PutUint32(b, period)
self.hasher.Write(b) self.hasher.Write(b)
binary.LittleEndian.PutUint32(b, version) binary.LittleEndian.PutUint32(b, version)
self.hasher.Write(b) self.hasher.Write(b)
self.hasher.Write(namehash[:])
return self.hasher.Sum(nil) return self.hasher.Sum(nil)
} }
func (self *ResourceHandler) signContent(data []byte) ([]byte, error) { func (self *ResourceHandler) hasUpdate(name string, period uint32) bool {
self.hashLock.Lock() if self.resources[name].lastPeriod == period {
self.hasher.Reset() return true
self.hasher.Write(data)
datahash := self.hasher.Sum(nil)
self.hashLock.Unlock()
signature, err := crypto.Sign(datahash, self.privKey)
if err != nil {
return nil, err
} }
datawithsign := make([]byte, len(data)+signatureLength) return false
copy(datawithsign[:signatureLength], signature)
copy(datawithsign[signatureLength:], data)
return datawithsign, nil
} }
func (self *ResourceHandler) getContentAccount(chunkdata []byte) (common.Address, error) { func getAddressFromDataSig(datahash common.Hash, signature Signature) (common.Address, error) {
if len(chunkdata) <= signatureLength { pub, err := crypto.SigToPub(datahash.Bytes(), signature[:])
return common.Address{}, fmt.Errorf("zero-length data")
}
self.hashLock.Lock()
self.hasher.Reset()
self.hasher.Write(chunkdata[signatureLength:])
datahash := self.hasher.Sum(nil)
self.hashLock.Unlock()
pub, err := crypto.SigToPub(datahash, chunkdata[:signatureLength])
if err != nil { if err != nil {
return common.Address{}, err return common.Address{}, err
} }
return crypto.PubkeyToAddress(*pub), nil return crypto.PubkeyToAddress(*pub), nil
} }
func (self *ResourceHandler) verifyContent(chunkdata []byte) error { // create an update chunk
address, err := self.getContentAccount(chunkdata) func newUpdateChunk(key Key, signature *Signature, period uint32, version uint32, name string, data []byte) *Chunk {
if err != nil {
return err // no signatures if no validator
var sigoffset int
if signature != nil {
sigoffset = signatureLength
} }
log.Warn("ens owner lookup not implemented, verify will return true in all cases", "address", address)
return nil // prepend version and period to allow reverse lookups
} headerlength := uint16(len(name) + 4 + 4)
func (self *ResourceHandler) hasUpdate(name string, period uint32) bool { chunk := NewChunk(key, nil)
return self.resources[name].lastPeriod == period chunk.SData = make([]byte, sigoffset+int(headerlength)+2+len(data))
cursor := 0
if signature != nil {
copy(chunk.SData, (*signature)[:])
cursor += signatureLength
}
// data header length does NOT include the header length prefix bytes themselves
binary.LittleEndian.PutUint16(chunk.SData[cursor:], headerlength)
cursor += 2
binary.LittleEndian.PutUint32(chunk.SData[cursor:], period)
cursor += 4
binary.LittleEndian.PutUint32(chunk.SData[cursor:], version)
cursor += 4
namebytes := []byte(name)
copy(chunk.SData[cursor:], namebytes)
cursor += len(namebytes)
copy(chunk.SData[cursor:], data)
chunk.Size = int64(len(chunk.SData))
return chunk
} }
// \TODO chunkSize is a workaround until the ChunkStore interface exports a method to get the chunk size directly
type resourceChunkStore struct { type resourceChunkStore struct {
localStore ChunkStore localStore ChunkStore
netStore ChunkStore netStore ChunkStore
chunkSize int64
} }
func newResourceChunkStore(path string, hasher SwarmHasher, localStore *LocalStore, cloudStore CloudStore) *resourceChunkStore { func newResourceChunkStore(path string, hasher SwarmHasher, localStore *LocalStore, cloudStore CloudStore) *resourceChunkStore {
@ -687,3 +692,36 @@ func getNextPeriod(start uint64, current uint64, frequency uint64) uint32 {
period := blockdiff / frequency period := blockdiff / frequency
return uint32(period + 1) return uint32(period + 1)
} }
func ToSafeName(name string) (string, error) {
validname, err := idna.ToASCII(name)
if err != nil {
return "", err
}
return validname, nil
}
// check that name identifiers contain valid bytes
func isSafeName(name string) bool {
if name == "" {
return false
}
validname, err := idna.ToASCII(name)
if err != nil {
return false
}
if validname != name {
return false
}
return true
}
// convenience for creating signature hashes of update data
func (self *ResourceHandler) keyDataHash(key Key, data []byte) common.Hash {
self.hashLock.Lock()
defer self.hashLock.Unlock()
self.hasher.Reset()
self.hasher.Write(key[:])
self.hasher.Write(data)
return common.BytesToHash(self.hasher.Sum(nil))
}

View file

@ -1,54 +1,52 @@
package storage package storage
import ( import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/contracts/ens" "github.com/ethereum/go-ethereum/contracts/ens"
) )
// ENS validation of mutable resource owners type baseValidator struct {
type ENSValidator struct { signFunc SignFunc
owner common.Address
api *ens.ENS
} }
func NewENSValidator(owneraddress common.Address, contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts) (*ENSValidator, error) { func (b *baseValidator) sign(datahash common.Hash) (signature Signature, err error) {
if b.signFunc == nil {
return signature, fmt.Errorf("No signature function")
}
return b.signFunc(datahash)
}
// ENS validation of mutable resource owners
type ENSValidator struct {
*baseValidator
api *ens.ENS
}
func NewENSValidator(contractaddress common.Address, backend bind.ContractBackend, transactOpts *bind.TransactOpts, signFunc SignFunc) (*ENSValidator, error) {
var err error var err error
validator := &ENSValidator{} validator := &ENSValidator{
baseValidator: &baseValidator{
signFunc: signFunc,
},
}
validator.api, err = ens.NewENS(transactOpts, contractaddress, backend) validator.api, err = ens.NewENS(transactOpts, contractaddress, backend)
if err != nil { if err != nil {
return nil, err return nil, err
} }
validator.owner = owneraddress
return validator, nil return validator, nil
} }
func (self *ENSValidator) isOwner(name string) (bool, error) { func (self *ENSValidator) checkAccess(name string, address common.Address) (bool, error) {
owneraddr, err := self.api.Owner(self.nameHash(name)) owneraddr, err := self.api.Owner(self.nameHash(name))
if err != nil { if err != nil {
return false, err return false, err
} }
return owneraddr == self.owner, nil return owneraddr == address, nil
} }
func (self *ENSValidator) nameHash(name string) common.Hash { func (self *ENSValidator) nameHash(name string) common.Hash {
return ens.EnsNode(name) return ens.EnsNode(name)
} }
// Default fallthrough validation of mutable resource ownership
type GenericValidator struct {
hashFunc func(string) common.Hash
}
func NewGenericValidator(hashFunc func(string) common.Hash) *GenericValidator {
return &GenericValidator{
hashFunc: hashFunc,
}
}
func (self *GenericValidator) isOwner(name string) (bool, error) {
return true, nil
}
func (self *GenericValidator) nameHash(name string) common.Hash {
return self.hashFunc(name)
}

View file

@ -15,8 +15,6 @@ import (
"testing" "testing"
"time" "time"
"golang.org/x/net/idna"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -29,16 +27,22 @@ import (
) )
var ( var (
hasher = MakeHashFunc("SHA3")() testHasher = MakeHashFunc(SHA3Hash)()
zeroAddr = common.Address{} zeroAddr = common.Address{}
startBlock = uint64(4200) startBlock = uint64(4200)
resourceFrequency = uint64(42) resourceFrequency = uint64(42)
cleanF func() cleanF func()
domainName = "føø.bar" domainName = "føø.bar"
safeName string
) )
func init() { func init() {
var err error
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))) log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
safeName, err = ToSafeName(domainName)
if err != nil {
panic(err)
}
} }
// simulated backend does not have the blocknumber call // simulated backend does not have the blocknumber call
@ -65,29 +69,25 @@ func (r *FakeRPC) BlockNumber() (string, error) {
} }
// check that signature address matches update signer address // check that signature address matches update signer address
func TestResourceSignature(t *testing.T) { func TestResourceReverse(t *testing.T) {
// privkey for signing updates period := uint32(4)
privkey, err := crypto.GenerateKey() version := uint32(2)
// signer containing private key
signer, err := newTestSigner()
if err != nil { if err != nil {
return t.Fatal(err)
} }
// set up rpc and create resourcehandler // set up rpc and create resourcehandler
rh, _, err, teardownTest := setupTest(privkey, nil, nil) rh, _, _, teardownTest, err := setupTest(nil, newTestValidator(signer.signContent))
if err != nil {
teardownTest(t, err)
}
// create a new resource
validname, err := idna.ToASCII(domainName)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// generate a hash for block 4200 version 1 // generate a hash for block 4200 version 1
key := rh.resourceHash(ens.EnsNode(validname), 1, 1) key := rh.resourceHash(period, version, rh.nameHash(safeName))
chunk := NewChunk(key, nil)
// generate some bogus data for the chunk and sign it // generate some bogus data for the chunk and sign it
data := make([]byte, 8) data := make([]byte, 8)
@ -95,131 +95,76 @@ func TestResourceSignature(t *testing.T) {
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
hasher.Reset() testHasher.Reset()
hasher.Write(data) testHasher.Write(data)
datahash := hasher.Sum(nil) digest := rh.keyDataHash(key, data)
sig, err := crypto.Sign(datahash, privkey) sig, err := rh.validator.sign(digest)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// put sig and data in the chunk chunk := newUpdateChunk(key, &sig, period, version, safeName, data)
chunk.SData = make([]byte, 8+signatureLength)
copy(chunk.SData[:signatureLength], sig)
copy(chunk.SData[signatureLength:], data)
// check that we can recover the owner account from the update chunk's signature // check that we can recover the owner account from the update chunk's signature
// TODO: change this to verifyContent on ENS integration checksig, checkperiod, checkversion, checkname, checkdata, err := rh.parseUpdate(chunk.SData)
recoveredaddress, err := rh.getContentAccount(chunk.SData) checkdigest := rh.keyDataHash(chunk.Key, checkdata)
recoveredaddress, err := getAddressFromDataSig(checkdigest, *checksig)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, fmt.Errorf("Retrieve address from signature fail: %v", err))
} }
originaladdress := crypto.PubkeyToAddress(privkey.PublicKey) originaladdress := crypto.PubkeyToAddress(signer.privKey.PublicKey)
// check that the metadata retrieved from the chunk matches what we gave it
if recoveredaddress != originaladdress { if recoveredaddress != originaladdress {
teardownTest(t, fmt.Errorf("addresses dont match: %x != %x", originaladdress, recoveredaddress)) teardownTest(t, fmt.Errorf("addresses dont match: %x != %x", originaladdress, recoveredaddress))
} }
if !bytes.Equal(key[:], chunk.Key[:]) {
teardownTest(t, fmt.Errorf("Expected chunk key '%x', was '%x'", key, chunk.Key))
}
if period != checkperiod {
teardownTest(t, fmt.Errorf("Expected period '%d', was '%d'", period, checkperiod))
}
if version != checkversion {
teardownTest(t, fmt.Errorf("Expected version '%d', was '%d'", version, checkversion))
}
if safeName != checkname {
teardownTest(t, fmt.Errorf("Expected name '%s', was '%s'", safeName, checkname))
}
if !bytes.Equal(data, checkdata) {
teardownTest(t, fmt.Errorf("Expectedn data '%x', was '%x'", data, checkdata))
}
teardownTest(t, nil) teardownTest(t, nil)
} }
// determine resource update metadata from chunk data
func TestResourceReverseLookup(t *testing.T) {
// privkey for signing updates
privkey, err := crypto.GenerateKey()
if err != nil {
return
}
// make fake backend, set up rpc and create resourcehandler
backend := &fakeBackend{
blocknumber: startBlock,
}
rh, _, err, teardownTest := setupTest(privkey, backend, nil)
if err != nil {
teardownTest(t, err)
}
// create a new resource
rsrc, err := rh.NewResource(domainName, resourceFrequency)
if err != nil {
teardownTest(t, err)
}
// update data
fwdBlocks(int(resourceFrequency+1), backend)
data := []byte("foo")
resourcekey, err := rh.Update(domainName, data)
if err != nil {
teardownTest(t, err)
}
chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(resourcekey))
if err != nil {
teardownTest(t, err)
}
// check if data after header length offset is as expected
headerlength := binary.LittleEndian.Uint16(chunk.SData[signatureLength : signatureLength+2])
if !bytes.Equal(chunk.SData[signatureLength+headerlength+2:], data) {
teardownTest(t, fmt.Errorf("Expected chunk data with header length %d (pos %d) to match %x, but was %x", headerlength, signatureLength+headerlength+2, data, chunk.SData[signatureLength+headerlength+2:]))
}
// get name, period, version from chunk and check
revperiod, revversion, revname, revdata, err := parseUpdate(chunk.SData[signatureLength:])
if !bytes.Equal(revname, rsrc.nameHash.Bytes()) {
teardownTest(t, fmt.Errorf("Expected retrieved name from chunk data to be '%x', was '%x'", rsrc.nameHash.Bytes(), revname))
}
if !bytes.Equal(revdata, data) {
teardownTest(t, fmt.Errorf("Expected retrieved data from chunk data to be '%x', was '%x'", data, revdata))
}
if revperiod != 2 {
teardownTest(t, fmt.Errorf("Expected retrieved period from chunk data to be 1, was %d", revperiod))
}
if revversion != 1 {
teardownTest(t, fmt.Errorf("Expected retrieved version from chunk data to be 1, was %d", revversion))
}
}
// make updates and retrieve them based on periods and versions // make updates and retrieve them based on periods and versions
func TestResourceHandler(t *testing.T) { func TestResourceHandler(t *testing.T) {
// privkey for signing updates
privkey, err := crypto.GenerateKey()
if err != nil {
return
}
// make fake backend, set up rpc and create resourcehandler // make fake backend, set up rpc and create resourcehandler
backend := &fakeBackend{ backend := &fakeBackend{
blocknumber: startBlock, blocknumber: startBlock,
} }
rh, datadir, err, teardownTest := setupTest(privkey, backend, nil) rh, datadir, _, teardownTest, err := setupTest(backend, nil)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// create a new resource // create a new resource
resourcevalidname, err := idna.ToASCII(domainName) _, err = rh.NewResource(safeName, resourceFrequency)
if err != nil {
teardownTest(t, err)
}
_, err = rh.NewResource(domainName, resourceFrequency)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// check that the new resource is stored correctly // check that the new resource is stored correctly
namehash := rh.validator.nameHash(resourcevalidname) namehash := rh.nameHash(safeName)
chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(namehash[:])) chunk, err := rh.ChunkStore.(*resourceChunkStore).localStore.(*LocalStore).memStore.Get(Key(namehash[:]))
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} else if len(chunk.SData) < 16 { } else if len(chunk.SData) < 16 {
teardownTest(t, fmt.Errorf("chunk data must be minimum 16 bytes, is %d", len(chunk.SData))) teardownTest(t, fmt.Errorf("chunk data must be minimum 16 bytes, is %d", len(chunk.SData)))
} }
startblocknumber := binary.LittleEndian.Uint64(chunk.SData[8:16]) startblocknumber := binary.LittleEndian.Uint64(chunk.SData[:8])
chunkfrequency := binary.LittleEndian.Uint64(chunk.SData[16:]) chunkfrequency := binary.LittleEndian.Uint64(chunk.SData[8:])
if startblocknumber != backend.blocknumber { if startblocknumber != backend.blocknumber {
teardownTest(t, fmt.Errorf("stored block number %d does not match provided block number %d", startblocknumber, backend.blocknumber)) teardownTest(t, fmt.Errorf("stored block number %d does not match provided block number %d", startblocknumber, backend.blocknumber))
} }
@ -230,28 +175,32 @@ func TestResourceHandler(t *testing.T) {
// update halfway to first period // update halfway to first period
resourcekey := make(map[string]Key) resourcekey := make(map[string]Key)
fwdBlocks(int(resourceFrequency/2), backend) fwdBlocks(int(resourceFrequency/2), backend)
resourcekey["blinky"], err = rh.Update(domainName, []byte("blinky")) data := []byte("blinky")
resourcekey["blinky"], err = rh.Update(safeName, data)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// update on first period // update on first period
fwdBlocks(int(resourceFrequency/2), backend) fwdBlocks(int(resourceFrequency/2), backend)
resourcekey["pinky"], err = rh.Update(domainName, []byte("pinky")) data = []byte("pinky")
resourcekey["pinky"], err = rh.Update(safeName, data)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// update on second period // update on second period
fwdBlocks(int(resourceFrequency), backend) fwdBlocks(int(resourceFrequency), backend)
resourcekey["inky"], err = rh.Update(domainName, []byte("inky")) data = []byte("inky")
resourcekey["inky"], err = rh.Update(safeName, data)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// update just after second period // update just after second period
fwdBlocks(1, backend) fwdBlocks(1, backend)
resourcekey["clyde"], err = rh.Update(domainName, []byte("clyde")) data = []byte("clyde")
resourcekey["clyde"], err = rh.Update(safeName, data)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
@ -262,64 +211,45 @@ func TestResourceHandler(t *testing.T) {
// it will match on second iteration startblocknumber + (resourceFrequency * 3) // it will match on second iteration startblocknumber + (resourceFrequency * 3)
fwdBlocks(int(resourceFrequency*2)-1, backend) fwdBlocks(int(resourceFrequency*2)-1, backend)
rh2, err := NewResourceHandler(privkey, datadir, &testCloudStore{}, rh.rpcClient, nil) rh2, err := NewResourceHandler(datadir, &testCloudStore{}, rh.rpcClient, nil)
_, err = rh2.LookupLatest(domainName, true) _, err = rh2.LookupLatest(safeName, true)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// last update should be "clyde", version two, blockheight startblocknumber + (resourcefrequency * 3) // last update should be "clyde", version two, blockheight startblocknumber + (resourcefrequency * 3)
if !bytes.Equal(rh2.resources[domainName].data, []byte("clyde")) { if !bytes.Equal(rh2.resources[safeName].data, []byte("clyde")) {
teardownTest(t, fmt.Errorf("resource data was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) teardownTest(t, fmt.Errorf("resource data was %v, expected %v", rh2.resources[safeName].data, []byte("clyde")))
} }
if rh2.resources[domainName].version != 2 { if rh2.resources[safeName].version != 2 {
teardownTest(t, fmt.Errorf("resource version was %d, expected 2", rh2.resources[domainName].version)) teardownTest(t, fmt.Errorf("resource version was %d, expected 2", rh2.resources[safeName].version))
} }
if rh2.resources[domainName].lastPeriod != 3 { if rh2.resources[safeName].lastPeriod != 3 {
teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[domainName].lastPeriod)) teardownTest(t, fmt.Errorf("resource period was %d, expected 3", rh2.resources[safeName].lastPeriod))
}
rsrc, err := NewResource(domainName, startblocknumber, resourceFrequency, rh2.validator.nameHash)
if err != nil {
teardownTest(t, err)
}
err = rh2.SetExternalResource(rsrc, true)
if err != nil {
teardownTest(t, err)
}
// latest block, latest version
resource, err := rh2.LookupLatest(domainName, false) // if key is specified, refresh is implicit
if err != nil {
teardownTest(t, err)
}
// check data
if !bytes.Equal(resource.data, []byte("clyde")) {
teardownTest(t, fmt.Errorf("resource data (latest) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde")))
} }
log.Debug("Latest lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data)
// specific block, latest version // specific block, latest version
resource, err = rh2.LookupHistorical(domainName, 3, true) rsrc, err := rh2.LookupHistorical(safeName, 3, true)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// check data // check data
if !bytes.Equal(resource.data, []byte("clyde")) { if !bytes.Equal(rsrc.data, []byte("clyde")) {
teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde"))) teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("clyde")))
} }
log.Debug("Historical lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data)
// specific block, specific version // specific block, specific version
resource, err = rh2.LookupVersion(domainName, 3, 1, true) rsrc, err = rh2.LookupVersion(safeName, 3, 1, true)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// check data // check data
if !bytes.Equal(resource.data, []byte("inky")) { if !bytes.Equal(rsrc.data, []byte("inky")) {
teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("inky"))) teardownTest(t, fmt.Errorf("resource data (historical) was %v, expected %v", rh2.resources[domainName].data, []byte("inky")))
} }
log.Debug("Specific version lookup", "period", rh2.resources[safeName].lastPeriod, "version", rh2.resources[safeName].version, "data", rh2.resources[safeName].data)
teardownTest(t, nil) teardownTest(t, nil)
} }
@ -327,62 +257,54 @@ func TestResourceHandler(t *testing.T) {
// create ENS enabled resource update, with and without valid owner // create ENS enabled resource update, with and without valid owner
func TestResourceENSOwner(t *testing.T) { func TestResourceENSOwner(t *testing.T) {
// privkey for signing updates // signer containing private key
privkey, err := crypto.GenerateKey() signer, err := newTestSigner()
if err != nil { if err != nil {
return t.Fatal(err)
}
// privkey for checking wrong owner
privkeytwo, err := crypto.GenerateKey()
if err != nil {
return
} }
// ens address and transact options // ens address and transact options
addr := crypto.PubkeyToAddress(privkey.PublicKey) addr := crypto.PubkeyToAddress(signer.privKey.PublicKey)
transactOpts := bind.NewKeyedTransactor(privkey) transactOpts := bind.NewKeyedTransactor(signer.privKey)
// set up ENS sim // set up ENS sim
domainparts := strings.Split(domainName, ".") domainparts := strings.Split(safeName, ".")
contractAddr, contractbackend, err := setupENS(addr, transactOpts, domainparts[0], domainparts[1]) contractAddr, contractbackend, err := setupENS(addr, transactOpts, domainparts[0], domainparts[1])
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
validator, err := NewENSValidator(addr, contractAddr, contractbackend, transactOpts) validator, err := NewENSValidator(contractAddr, contractbackend, transactOpts, signer.signContent)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// set up rpc and create resourcehandler with ENS sim backend // set up rpc and create resourcehandler with ENS sim backend
rh, _, err, teardownTest := setupTest(privkey, contractbackend, validator) rh, _, _, teardownTest, err := setupTest(contractbackend, validator)
if err != nil { if err != nil {
teardownTest(t, err) teardownTest(t, err)
} }
// create new resource when we are owner = ok // create new resource when we are owner = ok
_, err = rh.NewResource(domainName, 42) _, err = rh.NewResource(safeName, resourceFrequency)
if err != nil { if err != nil {
teardownTest(t, fmt.Errorf("Create resource fail: %v", err)) teardownTest(t, fmt.Errorf("Create resource fail: %v", err))
} }
data := []byte("foo")
// update resource when we are owner = ok // update resource when we are owner = ok
_, err = rh.Update(domainName, []byte("foo")) _, err = rh.Update(safeName, data)
if err != nil { if err != nil {
teardownTest(t, fmt.Errorf("Update resource fail: %v", err)) teardownTest(t, fmt.Errorf("Update resource fail: %v", err))
} }
// create new resource when we are NOT owner = !ok
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"))
}
// update resource when we are owner = ok // update resource when we are owner = ok
_, err = rh.Update(domainName, []byte("foo")) signertwo, err := newTestSigner()
if err != nil {
teardownTest(t, err)
}
rh.validator.(*ENSValidator).signFunc = signertwo.signContent
_, err = rh.Update(safeName, data)
if err == nil { if err == nil {
teardownTest(t, fmt.Errorf("Expected resource update fail due to owner mismatch")) teardownTest(t, fmt.Errorf("Expected resource update fail due to owner mismatch"))
} }
@ -398,7 +320,7 @@ func fwdBlocks(count int, backend *fakeBackend) {
} }
// create rpc and resourcehandler // create rpc and resourcehandler
func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend, validator ResourceValidator) (rh *ResourceHandler, datadir string, err error, teardown func(*testing.T, error)) { func setupTest(contractbackend bind.ContractBackend, validator ResourceValidator) (rh *ResourceHandler, datadir string, signer *testSigner, teardown func(*testing.T, error), err error) {
var fsClean func() var fsClean func()
var rpcClean func() var rpcClean func()
@ -448,8 +370,7 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
return return
} }
// choose if with ens or not rh, err = NewResourceHandler(datadir, &testCloudStore{}, rpcclient, validator)
rh, err = NewResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, validator)
teardown = func(t *testing.T, err error) { teardown = func(t *testing.T, err error) {
cleanF() cleanF()
if err != nil { if err != nil {
@ -467,12 +388,12 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string,
var tophash [32]byte var tophash [32]byte
var subhash [32]byte var subhash [32]byte
hasher.Reset() testHasher.Reset()
hasher.Write([]byte(top)) testHasher.Write([]byte(top))
copy(tophash[:], hasher.Sum(nil)) copy(tophash[:], testHasher.Sum(nil))
hasher.Reset() testHasher.Reset()
hasher.Write([]byte(sub)) testHasher.Write([]byte(sub))
copy(subhash[:], hasher.Sum(nil)) copy(subhash[:], testHasher.Sum(nil))
// initialize contract backend and deploy // initialize contract backend and deploy
contractBackend := &fakeBackend{ contractBackend := &fakeBackend{
@ -503,6 +424,33 @@ func setupENS(addr common.Address, transactOpts *bind.TransactOpts, sub string,
return contractAddress, contractBackend, nil return contractAddress, contractBackend, nil
} }
// implementation of an external signer to pass to validator
type testSigner struct {
privKey *ecdsa.PrivateKey
hasher SwarmHash
}
func newTestSigner() (*testSigner, error) {
privKey, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
return &testSigner{
privKey: privKey,
hasher: testHasher,
}, nil
}
// matches the SignFunc type
func (self *testSigner) signContent(data common.Hash) (signature Signature, err error) {
signaturebytes, err := crypto.Sign(data.Bytes(), self.privKey)
if err != nil {
return
}
copy(signature[:], signaturebytes)
return
}
type testCloudStore struct { type testCloudStore struct {
} }
@ -514,3 +462,31 @@ func (c *testCloudStore) Deliver(*Chunk) {
func (c *testCloudStore) Retrieve(*Chunk) { func (c *testCloudStore) Retrieve(*Chunk) {
} }
// Default fallthrough validation of mutable resource ownership
type testValidator struct {
*baseValidator
hashFunc func(string) common.Hash
}
func newTestValidator(signFunc SignFunc) *testValidator {
return &testValidator{
baseValidator: &baseValidator{
signFunc: signFunc,
},
hashFunc: func(name string) common.Hash {
testHasher.Reset()
testHasher.Write([]byte(name))
return common.BytesToHash(testHasher.Sum(nil))
},
}
}
func (self *testValidator) checkAccess(name string, address common.Address) (bool, error) {
return true, nil
}
func (self *testValidator) nameHash(name string) common.Hash {
return self.hashFunc(name)
}