mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 01:13:45 +00:00
swarm/storage: Check ENS ownership on resource create
This commit is contained in:
parent
dfb45e382c
commit
91cfddd375
3 changed files with 79 additions and 37 deletions
|
|
@ -40,10 +40,6 @@ type resource struct {
|
||||||
// The update scheme is built on swarm chunks with chunk keys following
|
// The update scheme is built on swarm chunks with chunk keys following
|
||||||
// a predictable, versionable pattern.
|
// 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
|
// Updates are defined to be periodic in nature, where periods are
|
||||||
// expressed in terms of number of blocks.
|
// 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
|
// 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
|
// 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,
|
// starting at block 4200 with frequency 42 will have updates on block 4242,
|
||||||
// 4284, 4326 and so on.
|
// 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
|
// 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 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
|
// 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|blocknumber|version)
|
// sha256(namehash|period|version)
|
||||||
//
|
//
|
||||||
// The blocknumber here is the next block period after the current block
|
// The period is (currentblock - startblock) / frequency
|
||||||
// calculated from the start block and frequency of the resource update.
|
//
|
||||||
// Using our previous example, this means that an update made at block 4285,
|
// Using our previous example, this means that a period 3 will have 4326 as
|
||||||
// and even 4284, will have 4326 as the block number.
|
// the block number.
|
||||||
//
|
//
|
||||||
// If more than one update is made to the same block number, incremental
|
// If more than one update is made to the same block number, incremental
|
||||||
// version numbers are used successively.
|
// version numbers are used successively.
|
||||||
//
|
//
|
||||||
// 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 data itself is prefixed with a signed hash of the data. The sigining key
|
// the chunk data is: sign(resourcedata)|resourcedata
|
||||||
// is used to verify the authenticity of the update, for example by looking
|
// the resourcedata is: headerlength|period|version|name|data
|
||||||
// up the ownership of the namehash in ENS and comparing to the address derived
|
//
|
||||||
// from it
|
// 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
|
// NOTE: the following is yet to be implemented
|
||||||
// The resource update chunks will be stored in the swarm, but receive special
|
// 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
|
// 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.
|
||||||
//
|
|
||||||
// TODO: signature validation
|
|
||||||
type ResourceHandler interface {
|
type ResourceHandler interface {
|
||||||
ChunkStore
|
ChunkStore
|
||||||
NewResource(name string, frequency uint64) (*resource, error)
|
NewResource(name string, frequency uint64) (*resource, error)
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,31 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"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"
|
||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"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 {
|
type ENSResourceHandler struct {
|
||||||
ResourceHandler
|
*RawResourceHandler
|
||||||
ethapi *ethclient.Client
|
addr common.Address
|
||||||
ensapi *ens.ENS
|
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)
|
transactOpts := bind.NewKeyedTransactor(privKey)
|
||||||
ethapi := ethclient.NewClient(rpcClient)
|
ensinstance, err := ens.NewENS(transactOpts, ensAddr, backend)
|
||||||
ensinstance, err := ens.NewENS(transactOpts, ensAddr, ethapi)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -32,8 +39,19 @@ func NewENSResourceHandler(privKey *ecdsa.PrivateKey, datadir string, cloudStore
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ENSResourceHandler{
|
return &ENSResourceHandler{
|
||||||
ResourceHandler: rh,
|
RawResourceHandler: rh,
|
||||||
ethapi: ethclient.NewClient(rpcClient),
|
addr: crypto.PubkeyToAddress(privKey.PublicKey),
|
||||||
ensapi: ensinstance,
|
ensapi: ensinstance,
|
||||||
}, nil
|
}, 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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ func init() {
|
||||||
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)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// simulated backend does not have the blocknumber call
|
||||||
|
// so we use this wrapper to fake returning the block count
|
||||||
type fakeBackend struct {
|
type fakeBackend struct {
|
||||||
*backends.SimulatedBackend
|
*backends.SimulatedBackend
|
||||||
blocknumber uint64
|
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()
|
privkey, err := crypto.GenerateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// privkey for signing updates
|
||||||
|
privkeytwo, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up ENS sim
|
||||||
domainparts := strings.Split(domainName, ".")
|
domainparts := strings.Split(domainName, ".")
|
||||||
addr, contractbackend, err := setupENS(privkey, domainparts[0], domainparts[1])
|
addr, contractbackend, err := setupENS(privkey, domainparts[0], domainparts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -336,14 +345,31 @@ func TestResourceENS(t *testing.T) {
|
||||||
teardownTest(t, err)
|
teardownTest(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create new resource when we are owner = ok
|
||||||
_, err = rh.NewResource(domainName, 42)
|
_, err = rh.NewResource(domainName, 42)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
teardownTest(t, err)
|
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)
|
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)) {
|
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()
|
var fsClean func()
|
||||||
|
|
@ -394,8 +420,9 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// choose if with ens or not
|
||||||
if ensaddr != zeroAddr {
|
if ensaddr != zeroAddr {
|
||||||
rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, ensaddr)
|
rh, err = NewENSResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, contractbackend, ensaddr)
|
||||||
} else {
|
} else {
|
||||||
rh, err = NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, nil)
|
rh, err = NewRawResourceHandler(privkey, datadir, &testCloudStore{}, rpcclient, nil)
|
||||||
}
|
}
|
||||||
|
|
@ -408,7 +435,11 @@ func setupTest(privkey *ecdsa.PrivateKey, contractbackend bind.ContractBackend,
|
||||||
|
|
||||||
return
|
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) {
|
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 tophash [32]byte
|
||||||
var subhash [32]byte
|
var subhash [32]byte
|
||||||
hashfunc.Reset()
|
hashfunc.Reset()
|
||||||
|
|
@ -417,17 +448,22 @@ func setupENS(privkey *ecdsa.PrivateKey, sub string, top string) (common.Address
|
||||||
hashfunc.Reset()
|
hashfunc.Reset()
|
||||||
hashfunc.Write([]byte(sub))
|
hashfunc.Write([]byte(sub))
|
||||||
copy(subhash[:], hashfunc.Sum(nil))
|
copy(subhash[:], hashfunc.Sum(nil))
|
||||||
|
|
||||||
|
// private key -> address is owner of domain
|
||||||
addr := crypto.PubkeyToAddress(privkey.PublicKey)
|
addr := crypto.PubkeyToAddress(privkey.PublicKey)
|
||||||
|
|
||||||
|
// initialize contract backend and deploy
|
||||||
|
transactOpts := bind.NewKeyedTransactor(privkey)
|
||||||
contractBackend := &fakeBackend{
|
contractBackend := &fakeBackend{
|
||||||
SimulatedBackend: backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}}),
|
SimulatedBackend: backends.NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(1000000000)}}),
|
||||||
}
|
}
|
||||||
transactOpts := bind.NewKeyedTransactor(privkey)
|
|
||||||
|
|
||||||
ensAddr, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend)
|
ensAddr, _, ensinstance, err := contract.DeployENS(transactOpts, contractBackend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return zeroAddr, nil, fmt.Errorf("can't deploy: %v", err)
|
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 {
|
if _, err = ensinstance.SetOwner(transactOpts, [32]byte{}, addr); err != nil {
|
||||||
return zeroAddr, nil, fmt.Errorf("can't setowner: %v", err)
|
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()
|
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
|
return ensAddr, contractBackend, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue