mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-23 05:06:43 +00:00
Merge remote-tracking branch 'shpere/bzz' into bzz
This commit is contained in:
commit
8d860bb2d4
62 changed files with 7555 additions and 509 deletions
2
Godeps/Godeps.json
generated
2
Godeps/Godeps.json
generated
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/huin/goupnp",
|
"ImportPath": "github.com/huin/goupnp",
|
||||||
"Rev": "c57ae84388ab59076fd547f1abeab71c2edb0a21"
|
"Rev": "5cff77a69fb22f5f1774c4451ea2aab63d4d2f20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/jackpal/go-nat-pmp",
|
"ImportPath": "github.com/jackpal/go-nat-pmp",
|
||||||
|
|
|
||||||
9
Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go
generated
vendored
9
Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go
generated
vendored
|
|
@ -19,7 +19,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
"golang.org/x/net/html/charset"
|
"golang.org/x/net/html/charset"
|
||||||
|
|
||||||
"github.com/huin/goupnp/httpu"
|
"github.com/huin/goupnp/httpu"
|
||||||
|
|
@ -64,7 +64,6 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
|
||||||
maybe := &results[i]
|
maybe := &results[i]
|
||||||
loc, err := response.Location()
|
loc, err := response.Location()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
maybe.Err = ContextError{"unexpected bad location from search", err}
|
maybe.Err = ContextError{"unexpected bad location from search", err}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +92,11 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestXml(url string, defaultSpace string, doc interface{}) error {
|
func requestXml(url string, defaultSpace string, doc interface{}) error {
|
||||||
resp, err := http.Get(url)
|
timeout := time.Duration(3 * time.Second)
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
452
bzz/api.go
Normal file
452
bzz/api.go
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
// "net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hashMatcher = regexp.MustCompile("^[0-9A-Fa-f]{64}")
|
||||||
|
slashes = regexp.MustCompile("/+")
|
||||||
|
domainAndVersion = regexp.MustCompile("[@:;,]+")
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Api implements webserver/file system related content storage and retrieval
|
||||||
|
on top of the dpa
|
||||||
|
it is the public interface of the dpa which is included in the ethereum stack
|
||||||
|
*/
|
||||||
|
type Api struct {
|
||||||
|
Chunker *TreeChunker
|
||||||
|
Port string
|
||||||
|
Registrar registrar.VersionedRegistrar
|
||||||
|
dpa *DPA
|
||||||
|
netStore *netStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
the api constructor initialises
|
||||||
|
- the netstore endpoint for chunk store logic
|
||||||
|
- the chunker (bzz hash)
|
||||||
|
- the dpa - single document retrieval api
|
||||||
|
*/
|
||||||
|
func NewApi(datadir, port string) (self *Api, err error) {
|
||||||
|
|
||||||
|
self = &Api{
|
||||||
|
Chunker: &TreeChunker{},
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.netStore, err = newNetStore(filepath.Join(datadir, "bzz"), filepath.Join(datadir, "bzzpeers.json"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dpa = &DPA{
|
||||||
|
Chunker: self.Chunker,
|
||||||
|
ChunkStore: self.netStore,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local swarm without netStore
|
||||||
|
func NewLocalApi(datadir string) (self *Api, err error) {
|
||||||
|
|
||||||
|
self = &Api{
|
||||||
|
Chunker: &TreeChunker{},
|
||||||
|
}
|
||||||
|
dbStore, err := newDbStore(datadir)
|
||||||
|
dbStore.setCapacity(50000)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memStore := newMemStore(dbStore)
|
||||||
|
localStore := &localStore{
|
||||||
|
memStore,
|
||||||
|
dbStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dpa = &DPA{
|
||||||
|
Chunker: self.Chunker,
|
||||||
|
ChunkStore: localStore,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bzz returns the bzz protocol class instances of which run on every peer
|
||||||
|
func (self *Api) Bzz() (p2p.Protocol, error) {
|
||||||
|
return BzzProtocol(self.netStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Start is called when the ethereum stack is started
|
||||||
|
- calls Init() on treechunker
|
||||||
|
- launches the dpa (listening for chunk store/retrieve requests)
|
||||||
|
- launches the netStore (starts kademlia hive peer management)
|
||||||
|
- starts an http server
|
||||||
|
*/
|
||||||
|
func (self *Api) Start(node *discover.Node, connectPeer func(string) error) {
|
||||||
|
self.Chunker.Init()
|
||||||
|
self.dpa.Start()
|
||||||
|
if node != nil && self.netStore != nil && connectPeer != nil {
|
||||||
|
self.netStore.start(node, connectPeer)
|
||||||
|
dpaLogger.Infof("Swarm network started.")
|
||||||
|
} else {
|
||||||
|
dpaLogger.Infof("Local Swarm started without network")
|
||||||
|
}
|
||||||
|
if self.Port != "" {
|
||||||
|
go startHttpServer(self, self.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Api) Stop() {
|
||||||
|
self.dpa.Stop()
|
||||||
|
self.netStore.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get uses iterative manifest retrieval and prefix matching
|
||||||
|
// to resolve path to content using dpa retrieve
|
||||||
|
func (self *Api) Get(bzzpath string) (content []byte, mimeType string, status int, size int, err error) {
|
||||||
|
var reader SectionReader
|
||||||
|
reader, mimeType, status, err = self.getPath("/" + bzzpath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = make([]byte, reader.Size())
|
||||||
|
size, err = reader.Read(content)
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put provides singleton manifest creation and optional name registration
|
||||||
|
// on top of dpa store
|
||||||
|
func (self *Api) Put(content, contentType string) (string, error) {
|
||||||
|
sr := io.NewSectionReader(strings.NewReader(content), 0, int64(len(content)))
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
key, err := self.dpa.Store(sr, wg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
manifest := fmt.Sprintf(`{"entries":[{"hash":"%064x","contentType":"%s"}]}`, key, contentType)
|
||||||
|
sr = io.NewSectionReader(strings.NewReader(manifest), 0, int64(len(manifest)))
|
||||||
|
key, err = self.dpa.Store(sr, wg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return fmt.Sprintf("%064x", key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Api) Modify(rootHash, path, contentHash, contentType string) (newRootHash string, err error) {
|
||||||
|
root := common.Hex2Bytes(rootHash)
|
||||||
|
trie, err := loadManifest(self.dpa, root)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentHash != "" {
|
||||||
|
entry := &manifestTrieEntry{
|
||||||
|
Path: path,
|
||||||
|
Hash: contentHash,
|
||||||
|
ContentType: contentType,
|
||||||
|
}
|
||||||
|
trie.addEntry(entry)
|
||||||
|
} else {
|
||||||
|
trie.deleteEntry(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = trie.recalcAndStore()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%064x", trie.hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download replicates the manifest path structure on the local filesystem
|
||||||
|
// under localpath
|
||||||
|
func (self *Api) Download(bzzpath, localpath string) (err error) {
|
||||||
|
lpath, err := filepath.Abs(filepath.Clean(localpath))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(lpath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := slashes.Split(bzzpath, 3)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return fmt.Errorf("Invalid bzz path")
|
||||||
|
}
|
||||||
|
hostPort := parts[1]
|
||||||
|
var path string
|
||||||
|
if len(parts) > 2 {
|
||||||
|
path = regularSlashes(parts[2]) + "/"
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("Swarm: host: '%s', path '%s' requested.", hostPort, path)
|
||||||
|
|
||||||
|
//resolving host and port
|
||||||
|
var key Key
|
||||||
|
key, err = self.Resolve(hostPort)
|
||||||
|
if err != nil {
|
||||||
|
err = errResolve(err)
|
||||||
|
dpaLogger.Debugf("Swarm: error : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trie, err := loadManifest(self.dpa, key)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: loadManifestTrie error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPath := lpath
|
||||||
|
trie.listWithPrefix(path, func(entry *manifestTrieEntry, suffix string) { // TODO: paralellize
|
||||||
|
key := common.Hex2Bytes(entry.Hash)
|
||||||
|
reader := self.dpa.Retrieve(key)
|
||||||
|
path := lpath + "/" + suffix
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if dir != prevPath {
|
||||||
|
os.MkdirAll(dir, os.ModePerm) // TODO: handle errors
|
||||||
|
prevPath = dir
|
||||||
|
}
|
||||||
|
f, _ := os.Create(path) // TODO: handle errors, ??path separators
|
||||||
|
writer := bufio.NewWriter(f)
|
||||||
|
//io.Copy(writer, reader) // TODO: handle errors
|
||||||
|
io.CopyN(writer, reader, reader.Size()) // TODO: handle errors
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
f.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxParallelFiles = 5
|
||||||
|
|
||||||
|
// Upload replicates a local directory as a manifest file and uploads it
|
||||||
|
// using dpa store
|
||||||
|
// TODO: localpath should point to a manifest
|
||||||
|
func (self *Api) Upload(lpath, index string) (string, error) {
|
||||||
|
var list []*manifestTrieEntry
|
||||||
|
localpath, err := filepath.Abs(filepath.Clean(lpath))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(localpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var start int
|
||||||
|
if stat.IsDir() {
|
||||||
|
start = len(localpath)
|
||||||
|
dpaLogger.Debugf("uploading '%s'", localpath)
|
||||||
|
err = filepath.Walk(localpath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if (err == nil) && !info.IsDir() {
|
||||||
|
//fmt.Printf("lp %s path %s\n", localpath, path)
|
||||||
|
if len(path) <= start {
|
||||||
|
return fmt.Errorf("Path is too short")
|
||||||
|
}
|
||||||
|
if path[:start] != localpath {
|
||||||
|
return fmt.Errorf("Path prefix of '%s' does not match localpath '%s'", path, localpath)
|
||||||
|
}
|
||||||
|
entry := &manifestTrieEntry{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dir := filepath.Dir(localpath)
|
||||||
|
start = len(dir)
|
||||||
|
if len(localpath) <= start {
|
||||||
|
return "", fmt.Errorf("Path is too short")
|
||||||
|
}
|
||||||
|
if localpath[:start] != dir {
|
||||||
|
return "", fmt.Errorf("Path prefix of '%s' does not match dir '%s'", localpath, dir)
|
||||||
|
}
|
||||||
|
entry := &manifestTrieEntry{
|
||||||
|
Path: localpath,
|
||||||
|
}
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt := len(list)
|
||||||
|
errors := make([]error, cnt)
|
||||||
|
done := make(chan bool, maxParallelFiles)
|
||||||
|
dcnt := 0
|
||||||
|
|
||||||
|
for i, entry := range list {
|
||||||
|
if i >= dcnt+maxParallelFiles {
|
||||||
|
<-done
|
||||||
|
dcnt++
|
||||||
|
}
|
||||||
|
go func(i int, entry *manifestTrieEntry, done chan bool) {
|
||||||
|
f, err := os.Open(entry.Path)
|
||||||
|
if err == nil {
|
||||||
|
stat, _ := f.Stat()
|
||||||
|
sr := io.NewSectionReader(f, 0, stat.Size())
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
var hash Key
|
||||||
|
hash, err = self.dpa.Store(sr, wg)
|
||||||
|
if hash != nil {
|
||||||
|
list[i].Hash = fmt.Sprintf("%064x", hash)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if err == nil {
|
||||||
|
first512 := make([]byte, 512)
|
||||||
|
fread, _ := sr.ReadAt(first512, 0)
|
||||||
|
if fread > 0 {
|
||||||
|
mimeType := http.DetectContentType(first512[:fread])
|
||||||
|
if filepath.Ext(entry.Path) == ".css" {
|
||||||
|
mimeType = "text/css"
|
||||||
|
}
|
||||||
|
list[i].ContentType = mimeType
|
||||||
|
//fmt.Printf("%v %v %v\n", entry.Path, mimeType, filepath.Ext(entry.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
errors[i] = err
|
||||||
|
done <- true
|
||||||
|
}(i, entry, done)
|
||||||
|
}
|
||||||
|
for dcnt < cnt {
|
||||||
|
<-done
|
||||||
|
dcnt++
|
||||||
|
}
|
||||||
|
|
||||||
|
trie := &manifestTrie{
|
||||||
|
dpa: self.dpa,
|
||||||
|
}
|
||||||
|
for i, entry := range list {
|
||||||
|
if errors[i] != nil {
|
||||||
|
return "", errors[i]
|
||||||
|
}
|
||||||
|
entry.Path = regularSlashes(entry.Path[start:])
|
||||||
|
if entry.Path == index {
|
||||||
|
ientry := &manifestTrieEntry{
|
||||||
|
Path: "",
|
||||||
|
Hash: entry.Hash,
|
||||||
|
ContentType: entry.ContentType,
|
||||||
|
}
|
||||||
|
trie.addEntry(ientry)
|
||||||
|
}
|
||||||
|
trie.addEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
err2 := trie.recalcAndStore()
|
||||||
|
var hs string
|
||||||
|
if err2 == nil {
|
||||||
|
hs = fmt.Sprintf("%064x", trie.hash)
|
||||||
|
}
|
||||||
|
return hs, err2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Api) Register(sender common.Address, domain string, hash common.Hash) (err error) {
|
||||||
|
domainhash := common.BytesToHash(crypto.Sha3([]byte(domain)))
|
||||||
|
|
||||||
|
if self.Registrar != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: host '%s' (hash: '%v') to be registered as '%v'", domain, domainhash.Hex(), hash.Hex())
|
||||||
|
_, err = self.Registrar.Registry().SetHashToHash(sender, domainhash, hash)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("no registry: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type errResolve error
|
||||||
|
|
||||||
|
func (self *Api) Resolve(hostPort string) (contentHash Key, err error) {
|
||||||
|
host := hostPort
|
||||||
|
if hashMatcher.MatchString(host) {
|
||||||
|
contentHash = Key(common.Hex2Bytes(host))
|
||||||
|
dpaLogger.Debugf("Swarm: host is a contentHash: '%064x'", contentHash)
|
||||||
|
} else {
|
||||||
|
if self.Registrar != nil {
|
||||||
|
var hash common.Hash
|
||||||
|
var version *big.Int
|
||||||
|
parts := domainAndVersion.Split(host, 3)
|
||||||
|
if len(parts) > 1 && parts[1] != "" {
|
||||||
|
host = parts[0]
|
||||||
|
version = common.Big(parts[1])
|
||||||
|
}
|
||||||
|
hostHash := common.BytesToHash(crypto.Sha3([]byte(host)))
|
||||||
|
hash, err = self.Registrar.Resolver(version).HashToHash(hostHash)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to resolve '%s': %v", hostPort, err)
|
||||||
|
}
|
||||||
|
contentHash = Key(hash.Bytes())
|
||||||
|
dpaLogger.Debugf("Swarm: resolve host '%s' to contentHash: '%064x'", hostPort, contentHash)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("no resolver '%s': %v", hostPort, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Api) getPath(uri string) (reader SectionReader, mimeType string, status int, err error) {
|
||||||
|
parts := slashes.Split(uri, 3)
|
||||||
|
hostPort := parts[1]
|
||||||
|
var path string
|
||||||
|
if len(parts) > 2 {
|
||||||
|
path = parts[2]
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("Swarm: host: '%s', path '%s' requested.", hostPort, path)
|
||||||
|
|
||||||
|
//resolving host and port
|
||||||
|
var key Key
|
||||||
|
key, err = self.Resolve(hostPort)
|
||||||
|
if err != nil {
|
||||||
|
err = errResolve(err)
|
||||||
|
dpaLogger.Debugf("Swarm: error : %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trie, err := loadManifest(self.dpa, key)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: loadManifestTrie error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dpaLogger.Debugf("Swarm: getEntry(%s)", path)
|
||||||
|
entry, _ := trie.getEntry(path)
|
||||||
|
if entry != nil {
|
||||||
|
key = common.Hex2Bytes(entry.Hash)
|
||||||
|
status = entry.Status
|
||||||
|
mimeType = entry.ContentType
|
||||||
|
dpaLogger.Debugf("Swarm: content lookup key: '%064x' (%v)", key, mimeType)
|
||||||
|
reader = self.dpa.Retrieve(key)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("manifest entry for '%s' not found", path)
|
||||||
|
dpaLogger.Debugf("Swarm: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
143
bzz/api_test.go
Normal file
143
bzz/api_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testDir string
|
||||||
|
datadir = "/tmp/bzz"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_, filename, _, _ := runtime.Caller(1)
|
||||||
|
testDir = path.Join(path.Dir(filename), "bzztest")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testApi() (api *Api, err error) {
|
||||||
|
os.RemoveAll(datadir)
|
||||||
|
api, err = NewLocalApi(datadir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.Start(nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiPut(t *testing.T) {
|
||||||
|
api, err := testApi()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expContent := "hello"
|
||||||
|
expMimeType := "text/plain"
|
||||||
|
expStatus := 0
|
||||||
|
expSize := len(expContent)
|
||||||
|
bzzhash, err := api.Put(expContent, expMimeType)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testGet(t, api, bzzhash, []byte(expContent), expMimeType, expStatus, expSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGet(t *testing.T, api *Api, bzzhash string, expContent []byte, expMimeType string, expStatus int, expSize int) {
|
||||||
|
content, mimeType, status, size, err := api.Get(bzzhash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bytes.Equal(content, expContent) {
|
||||||
|
t.Errorf("incorrect content. expected '%s...', got '%s...'", string(expContent), string(content))
|
||||||
|
}
|
||||||
|
if mimeType != expMimeType {
|
||||||
|
t.Errorf("incorrect mimeType. expected '%s', got '%s'", expMimeType, mimeType)
|
||||||
|
}
|
||||||
|
if status != expStatus {
|
||||||
|
t.Errorf("incorrect status. expected '%d', got '%d'", expStatus, status)
|
||||||
|
}
|
||||||
|
if size != expSize {
|
||||||
|
t.Errorf("incorrect size. expected '%d', got '%d'", expSize, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiDirUpload(t *testing.T) {
|
||||||
|
api, err := testApi()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bzzhash, err := api.Upload(path.Join(testDir, "test0"), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||||
|
testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html", 0, 202)
|
||||||
|
|
||||||
|
content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css"))
|
||||||
|
testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/plain", 0, 132)
|
||||||
|
|
||||||
|
content, err = ioutil.ReadFile(path.Join(testDir, "test0", "img", "logo.png"))
|
||||||
|
testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "image/png", 0, 18136)
|
||||||
|
|
||||||
|
_, _, _, _, err = api.Get(bzzhash)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiDirUploadWithRootFile(t *testing.T) {
|
||||||
|
api, err := testApi()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bzzhash, err := api.Upload(path.Join(testDir, "test0"), "index.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||||
|
testGet(t, api, bzzhash, content, "text/html", 0, 202)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiFileUpload(t *testing.T) {
|
||||||
|
api, err := testApi()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||||
|
testGet(t, api, path.Join(bzzhash, "index.html"), content, "text/html", 0, 202)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiFileUploadWithRootFile(t *testing.T) {
|
||||||
|
api, err := testApi()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bzzhash, err := api.Upload(path.Join(testDir, "test0", "index.html"), "index.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path.Join(testDir, "test0", "index.html"))
|
||||||
|
testGet(t, api, bzzhash, content, "text/html", 0, 202)
|
||||||
|
}
|
||||||
191
bzz/bzzcontract/bzzcontract_test.go
Normal file
191
bzz/bzzcontract/bzzcontract_test.go
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
package bzzcontract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
//"github.com/ethereum/go-ethereum/core/state"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
xe "github.com/ethereum/go-ethereum/xeth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testFrontend struct {
|
||||||
|
t *testing.T
|
||||||
|
ethereum *eth.Ethereum
|
||||||
|
xeth *xe.XEth
|
||||||
|
api *rpc.EthereumApi
|
||||||
|
coinbase string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *testFrontend) UnlockAccount(acc []byte) bool {
|
||||||
|
f.t.Logf("Unlocking account %v\n", common.Bytes2Hex(acc))
|
||||||
|
f.ethereum.AccountManager().Unlock(acc, "password")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *testFrontend) ConfirmTransaction(tx *types.Transaction) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = 30300
|
||||||
|
|
||||||
|
func testEth(t *testing.T) (ethereum *eth.Ethereum, err error) {
|
||||||
|
os.RemoveAll("/tmp/eth/")
|
||||||
|
err = os.MkdirAll("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/", os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.MkdirAll("/tmp/eth/data", os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ks := crypto.NewKeyStorePlain("/tmp/eth/keys")
|
||||||
|
ioutil.WriteFile("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/e273f01c99144c438695e10f24926dc1f9fbf62d",
|
||||||
|
[]byte(`{"Id":"RhRXD+fNRKS4jx+7ZfEsNA==","Address":"4nPwHJkUTEOGleEPJJJtwfn79i0=","PrivateKey":"h4ACVpe74uIvi5Cg/2tX/Yrm2xdr3J7QoMbMtNX2CNc="}`), os.ModePerm)
|
||||||
|
|
||||||
|
port++
|
||||||
|
ethereum, err = eth.New(ð.Config{
|
||||||
|
DataDir: "/tmp/eth",
|
||||||
|
AccountManager: accounts.NewManager(ks),
|
||||||
|
Port: fmt.Sprintf("%d", port),
|
||||||
|
MaxPeers: 10,
|
||||||
|
Name: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInit(t *testing.T) (self *testFrontend) {
|
||||||
|
|
||||||
|
ethereum, err := testEth(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating jsre, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ethereum.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error starting ethereum: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self = &testFrontend{t: t, ethereum: ethereum}
|
||||||
|
self.xeth = xe.New(ethereum, self)
|
||||||
|
self.api = rpc.NewEthereumApi(self.xeth)
|
||||||
|
|
||||||
|
addr := self.xeth.Coinbase()
|
||||||
|
self.coinbase = addr
|
||||||
|
if addr != "0x"+core.TestAccount {
|
||||||
|
t.Errorf("CoinBase %v does not match TestAccount 0x%v", addr, core.TestAccount)
|
||||||
|
}
|
||||||
|
t.Logf("CoinBase is %v", addr)
|
||||||
|
|
||||||
|
balance := self.xeth.BalanceAt(core.TestAccount)
|
||||||
|
t.Logf("Balance is %v", balance)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *testFrontend) insertTx(addr, contract, fnsig string, args []string) {
|
||||||
|
|
||||||
|
//cb := common.HexToAddress(self.coinbase)
|
||||||
|
//coinbase := self.ethereum.ChainManager().State().GetStateObject(cb)
|
||||||
|
|
||||||
|
hash := common.Bytes2Hex(crypto.Sha3([]byte(fnsig)))
|
||||||
|
data := "0x" + hash[0:8]
|
||||||
|
for _, arg := range args {
|
||||||
|
data = data + common.Bytes2Hex(common.Hex2BytesFixed(arg, 32))
|
||||||
|
}
|
||||||
|
self.t.Logf("Tx data: %v", data)
|
||||||
|
|
||||||
|
jsontx := `
|
||||||
|
[{
|
||||||
|
"from": "` + addr + `",
|
||||||
|
"to": "0x` + contract + `",
|
||||||
|
"value": "100000000000",
|
||||||
|
"gas": "100000",
|
||||||
|
"gasPrice": "100000",
|
||||||
|
"data": "` + data + `"
|
||||||
|
}]
|
||||||
|
`
|
||||||
|
req := &rpc.RpcRequest{
|
||||||
|
Jsonrpc: "2.0",
|
||||||
|
Method: "eth_transact",
|
||||||
|
Params: []byte(jsontx),
|
||||||
|
Id: 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply interface{}
|
||||||
|
err0 := self.api.GetRequestReply(req, &reply)
|
||||||
|
if err0 != nil {
|
||||||
|
self.t.Errorf("GetRequestReply error: %v", err0)
|
||||||
|
}
|
||||||
|
|
||||||
|
//self.xeth.Transact(addr, contract, "100000000000", "100000", "100000", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *testFrontend) applyTxs() {
|
||||||
|
|
||||||
|
cb := common.HexToAddress(self.coinbase)
|
||||||
|
stateDb := self.ethereum.ChainManager().State().Copy()
|
||||||
|
block := self.ethereum.ChainManager().NewBlock(cb)
|
||||||
|
coinbase := stateDb.GetStateObject(cb)
|
||||||
|
coinbase.SetGasPool(big.NewInt(1000000))
|
||||||
|
txs := self.ethereum.TxPool().GetTransactions()
|
||||||
|
|
||||||
|
for i := 0; i < len(txs); i++ {
|
||||||
|
for _, tx := range txs {
|
||||||
|
if tx.Nonce() == uint64(i) {
|
||||||
|
_, gas, err := core.ApplyMessage(core.NewEnv(stateDb, self.ethereum.ChainManager(), tx, block), tx, coinbase)
|
||||||
|
//self.ethereum.TxPool().RemoveSet([]*types.Transaction{tx})
|
||||||
|
self.t.Logf("ApplyMessage: gas %v err %v", gas, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ethereum.TxPool().RemoveSet(txs)
|
||||||
|
self.xeth = self.xeth.WithState(stateDb)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageAddress(varidx uint32, key []byte) string {
|
||||||
|
data := make([]byte, 64)
|
||||||
|
binary.BigEndian.PutUint32(data[60:64], varidx)
|
||||||
|
copy(data[0:32], key[0:32])
|
||||||
|
return "0x" + common.Bytes2Hex(crypto.Sha3(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwarmContract(t *testing.T) {
|
||||||
|
|
||||||
|
tf := testInit(t)
|
||||||
|
defer tf.ethereum.Stop()
|
||||||
|
|
||||||
|
tf.insertTx(tf.coinbase, core.ContractAddrSwarm, "signup(uint256)", []string{"1000"})
|
||||||
|
tf.applyTxs()
|
||||||
|
|
||||||
|
addr := common.Hex2BytesFixed(tf.coinbase[2:], 32)
|
||||||
|
key := storageAddress(0, addr)
|
||||||
|
data := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key)
|
||||||
|
key = key[:65] + "6"
|
||||||
|
data2 := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key)
|
||||||
|
|
||||||
|
t.Logf("addr = %x key = %v data = %v, %v", addr, key, data, data2)
|
||||||
|
|
||||||
|
}
|
||||||
161
bzz/bzzcontract/swarm.sol
Normal file
161
bzz/bzzcontract/swarm.sol
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
/// @title Swarm Distributed Preimage Archive
|
||||||
|
/// @author Daniel A. Nagy <daniel@ethdev.com>
|
||||||
|
contract Swarm
|
||||||
|
{
|
||||||
|
|
||||||
|
uint constant GRACE = 50; // grace period for lost information in blocks
|
||||||
|
uint constant REWARD_FRACTION = 10; // this fraction of a deposit is paid as reward
|
||||||
|
|
||||||
|
bytes32 constant MAGIC_NUMBER = "Swarm receipt";
|
||||||
|
|
||||||
|
struct Bee {
|
||||||
|
uint deposit; // amount deposited by this member
|
||||||
|
uint expiry; // expiration time of the deposit
|
||||||
|
bytes32 missing; // member accused of losing this swarm chunk
|
||||||
|
uint deadline; // block number before which chunk must be presented
|
||||||
|
address reporter; // receipt reported by this address
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping (address => Bee) swarm;
|
||||||
|
|
||||||
|
// block number of transactions presenting chunks
|
||||||
|
mapping (bytes32 => uint) presentedChunks;
|
||||||
|
|
||||||
|
function max(uint a, uint b) private returns (uint c) {
|
||||||
|
if(a >= b) return a;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Sign up as a Swarm node for `time` seconds.
|
||||||
|
/// No term extension for nodes with non-clean status.
|
||||||
|
///
|
||||||
|
/// @dev Guards against term overflow and unauthorized extension,
|
||||||
|
/// but all funds are added to deposite irrespective of status.
|
||||||
|
///
|
||||||
|
/// @param time term of Swarm membership in seconds from now.
|
||||||
|
function signup(uint time) {
|
||||||
|
Bee b = swarm[tx.origin];
|
||||||
|
if(isClean(msg.sender) && now + time > now) {
|
||||||
|
b.expiry = max(b.expiry, now + time);
|
||||||
|
}
|
||||||
|
b.deposit += msg.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Withdraw from Swarm, refund deposit.
|
||||||
|
///
|
||||||
|
/// @dev Only allowed with clean status and expired term.
|
||||||
|
function withdraw() {
|
||||||
|
Bee b = swarm[tx.origin];
|
||||||
|
if(now > b.expiry && isClean(msg.sender)) {
|
||||||
|
msg.sender.send(b.deposit);
|
||||||
|
b.deposit = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Total deposit for address `addr`.
|
||||||
|
/// No change in state.
|
||||||
|
///
|
||||||
|
/// @dev Not meaningful for "Guilty" status.
|
||||||
|
///
|
||||||
|
/// @param addr queried address.
|
||||||
|
///
|
||||||
|
/// @return balance of queried address.
|
||||||
|
function balance(address addr) returns (uint d) {
|
||||||
|
Bee b = swarm[addr];
|
||||||
|
return b.deposit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Determine clean status of address `addr`.
|
||||||
|
/// Changes the state, but only as a matter of optimization.
|
||||||
|
/// Works as accessor.
|
||||||
|
///
|
||||||
|
/// @dev Defined as no signed receipt has been presented for missing chunk.
|
||||||
|
///
|
||||||
|
/// @param addr queried address.
|
||||||
|
///
|
||||||
|
/// @return true if status is "Clean".
|
||||||
|
function isClean(address addr) returns (bool s) {
|
||||||
|
Bee b = swarm[addr];
|
||||||
|
if(b.missing != 0 && presentedChunks[b.missing] != 0) b.missing = 0;
|
||||||
|
return b.missing == 0; // nothing they signed is missing
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @param suspect address of reported Swarm node
|
||||||
|
event Report(address suspect);
|
||||||
|
|
||||||
|
/// @notice Find out what is missing in case of a Report event.
|
||||||
|
///
|
||||||
|
/// @return 0 if nothing is missing, swarm hash otherwise
|
||||||
|
function whatIsMissing() returns (bytes32 h) {
|
||||||
|
bytes32 missing = swarm[tx.origin].missing;
|
||||||
|
if(presentedChunks[missing] != 0) missing = 0;
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Report chunk `swarmHash` as missing.
|
||||||
|
///
|
||||||
|
/// @param swarmHash sha3 hash of the missing chunk
|
||||||
|
/// @param expiry expiration time of receipt
|
||||||
|
/// @param sig_v signature parameter v
|
||||||
|
/// @param sig_r signature parameter r
|
||||||
|
/// @param sig_s signature parameter s
|
||||||
|
function reportMissingChunk(bytes32 swarmHash, uint expiry,
|
||||||
|
uint8 sig_v, bytes32 sig_r, bytes32 sig_s) {
|
||||||
|
if(expiry < now) return;
|
||||||
|
bytes32 recptHash = sha3(MAGIC_NUMBER, swarmHash, expiry);
|
||||||
|
address signer = ecrecover(recptHash, sig_v, sig_r, sig_s);
|
||||||
|
if(!isClean(signer) || !expiresAfter(signer, now)) return;
|
||||||
|
Bee b = swarm[signer];
|
||||||
|
b.missing = swarmHash;
|
||||||
|
b.deadline = block.number + GRACE;
|
||||||
|
b.reporter = msg.sender;
|
||||||
|
Report(signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Present a chunk in order to avoid losing deposit.
|
||||||
|
///
|
||||||
|
/// @param chunk chunk data
|
||||||
|
function presentMissingChunk(bytes chunk) external {
|
||||||
|
bytes32 swarmHash = sha3(chunk);
|
||||||
|
presentedChunks[swarmHash] = block.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Determine guilty status of address `addr`.
|
||||||
|
/// No change in state.
|
||||||
|
///
|
||||||
|
/// @dev Definition of guilty is failing to present missing chunk within grace period.
|
||||||
|
///
|
||||||
|
/// @param addr queried address.
|
||||||
|
///
|
||||||
|
/// @return true, if status is "Guilty".
|
||||||
|
function isGuilty(address addr) returns (bool g){
|
||||||
|
if(isClean(addr)) return false;
|
||||||
|
Bee b = swarm[addr];
|
||||||
|
return b.deadline < block.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Collect rewards for successfully prosecuting `addr`.
|
||||||
|
///
|
||||||
|
/// @dev This implies burning 9/10 of the security deposit.
|
||||||
|
///
|
||||||
|
/// @param addr guilty defendant address
|
||||||
|
function claimReporterReward(address addr) {
|
||||||
|
if(!isGuilty(addr)) return;
|
||||||
|
Bee b = swarm[addr];
|
||||||
|
msg.sender.send(b.deposit / REWARD_FRACTION); // reporter rewarded
|
||||||
|
delete swarm[addr]; // rest of deposit burnt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Determine if the deposit for `addr` is unaccessible until `time`.
|
||||||
|
/// No change in state.
|
||||||
|
///
|
||||||
|
/// @param addr queried address.
|
||||||
|
///
|
||||||
|
/// @param time queried time.
|
||||||
|
///
|
||||||
|
/// @return true if deposit expires after queried time.
|
||||||
|
function expiresAfter(address addr, uint time) returns (bool s) {
|
||||||
|
Bee b = swarm[addr];
|
||||||
|
return b.expiry > time;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
bzz/bzzhash/bzzhash.go
Normal file
38
bzz/bzzhash/bzzhash.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// bzzhash
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ethereum/go-ethereum/bzz"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: bzzhash <file name>")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
f, err := os.Open(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error opening file " + os.Args[1])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, _ := f.Stat()
|
||||||
|
sr := io.NewSectionReader(f, 0, stat.Size())
|
||||||
|
chunker := &bzz.TreeChunker{}
|
||||||
|
chunker.Init()
|
||||||
|
hash := make([]byte, chunker.KeySize())
|
||||||
|
errC := chunker.Split(hash, sr, nil, nil)
|
||||||
|
err, ok := <-errC
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("%064x\n", hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
bzz/bzzup/bzzhandler.html
Normal file
15
bzz/bzzup/bzzhandler.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Register Swarm protocol handler</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
navigator.registerProtocolHandler("bzz",
|
||||||
|
"http://localhost:8500/%s",
|
||||||
|
"Swarm handler");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Register Swarm protocol handler</h1>
|
||||||
|
<p>This web page will install a web protocol handler for the <code>bzz:</code> protocol.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
bzz/bzzup/bzzup.sh
Executable file
38
bzz/bzzup/bzzup.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
INDEX='index.html'
|
||||||
|
|
||||||
|
delimiter='{"entries":[{'
|
||||||
|
|
||||||
|
if [ -f "$1" ]; then
|
||||||
|
hash=`wget -q -O- --post-file="$1" http://localhost:8500/raw`
|
||||||
|
mime=`file --mime-type -b "$1"`
|
||||||
|
wget -q -O- --post-data="$delimiter\"hash\":\"$hash\",\"contentType\":\"$mime\"}]}" http://localhost:8500/raw
|
||||||
|
echo
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
[ -d "$1" ] || exit -1
|
||||||
|
|
||||||
|
bzzroot="$1"
|
||||||
|
[ "_$1" = _ ] && bzzroot=.
|
||||||
|
|
||||||
|
pushd "$bzzroot" > /dev/null
|
||||||
|
|
||||||
|
(for path in `find . -type f`
|
||||||
|
do
|
||||||
|
name=`echo "$path" | cut -c3-`
|
||||||
|
[ _`basename "$name"` = "_$INDEX" ] && name=`dirname "$name"`
|
||||||
|
echo -n "$delimiter"
|
||||||
|
hash=`wget -q -O- --post-file="$path" http://localhost:8500/raw`
|
||||||
|
mime=`file --mime-type -b "$path"`
|
||||||
|
echo -n "\"hash\":\"$hash\",\"path\":\"$name\",\"contentType\":\"$mime\""
|
||||||
|
delimiter='},{'
|
||||||
|
|
||||||
|
done
|
||||||
|
echo -n '}]}') | wget -q -O- --post-data=`cat` http://localhost:8500/raw
|
||||||
|
echo
|
||||||
|
|
||||||
|
popd > /dev/null
|
||||||
|
|
||||||
|
fi
|
||||||
194
bzz/chunkIO.go
Normal file
194
bzz/chunkIO.go
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bounded interface {
|
||||||
|
Size() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sliced interface {
|
||||||
|
Slice(int64, int64) (b []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size, Seek, Read, ReadAt
|
||||||
|
type SectionReader interface {
|
||||||
|
Bounded
|
||||||
|
io.Seeker
|
||||||
|
io.Reader
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkReader implements SectionReader on a section
|
||||||
|
// of an underlying ReaderAt.
|
||||||
|
type ChunkReader struct {
|
||||||
|
r io.ReaderAt
|
||||||
|
base int64
|
||||||
|
off int64
|
||||||
|
limit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunkReader returns a ChunkReader that reads from r
|
||||||
|
// starting at offset off and stops with EOF after n bytes.
|
||||||
|
func NewChunkReader(r io.ReaderAt, off int64, n int64) *ChunkReader {
|
||||||
|
return &ChunkReader{r: r, base: off, off: off, limit: off + n}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSliceReader just extends byte.Reader to make base slice accessible
|
||||||
|
type ByteSliceReader struct {
|
||||||
|
*bytes.Reader
|
||||||
|
base []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewByteSliceReader(b []byte) *ByteSliceReader {
|
||||||
|
return &ByteSliceReader{
|
||||||
|
base: b,
|
||||||
|
Reader: bytes.NewReader(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSliceReader implements the Sliced interface
|
||||||
|
func (self *ByteSliceReader) Slice(from, to int64) (b []byte, err error) {
|
||||||
|
if from < 0 || to >= int64(self.Len()) {
|
||||||
|
err = io.EOF
|
||||||
|
} else {
|
||||||
|
b = self.base[from:to]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunkReaderFromBytes is a convenience shortcut to get a SectionReader over a byte slice
|
||||||
|
func NewChunkReaderFromBytes(b []byte) *ChunkReader {
|
||||||
|
return NewChunkReader(NewByteSliceReader(b), 0, int64(len(b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The following is adapted from io.SectionReader
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (s *ChunkReader) Size() int64 {
|
||||||
|
return s.limit - s.base
|
||||||
|
}
|
||||||
|
|
||||||
|
var errWhence = errors.New("Seek: invalid whence")
|
||||||
|
var errOffset = errors.New("Seek: invalid offset")
|
||||||
|
|
||||||
|
func (s *ChunkReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
default:
|
||||||
|
return 0, errWhence
|
||||||
|
case 0:
|
||||||
|
offset += s.base
|
||||||
|
case 1:
|
||||||
|
offset += s.off
|
||||||
|
case 2:
|
||||||
|
offset += s.limit
|
||||||
|
}
|
||||||
|
if offset < s.base {
|
||||||
|
return 0, errOffset
|
||||||
|
}
|
||||||
|
s.off = offset
|
||||||
|
return offset - s.base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChunkReader) Read(p []byte) (n int, err error) {
|
||||||
|
if s.off >= s.limit {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if max := s.limit - s.off; int64(len(p)) > max {
|
||||||
|
p = p[0:max]
|
||||||
|
}
|
||||||
|
n, err = s.r.ReadAt(p, s.off)
|
||||||
|
s.off += int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ChunkReader) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
|
if off < 0 || off >= s.limit-s.base {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
off += s.base
|
||||||
|
if max := s.limit - off; int64(len(p)) > max {
|
||||||
|
p = p[0:max]
|
||||||
|
n, err = s.r.ReadAt(p, off)
|
||||||
|
if err == nil {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
n, err = s.r.ReadAt(p, off)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// added methods to that ChunkReader implements the Sliced interface
|
||||||
|
func (s *ChunkReader) Slice(from, to int64) (b []byte, err error) {
|
||||||
|
if from < 0 || to >= s.Size() {
|
||||||
|
err = io.EOF
|
||||||
|
} else {
|
||||||
|
if sl, ok := s.r.(Sliced); ok {
|
||||||
|
b, err = sl.Slice(s.base+from, s.base+to)
|
||||||
|
} else {
|
||||||
|
err = errors.New("not sliceable base")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// added method so that ChunkReader implements the io.WriterTo interface
|
||||||
|
// WriteTo method is used by io.Copy
|
||||||
|
// This is so that we avoid one extra step of allocation (if the underlying initial Reader implements Sliced
|
||||||
|
func (r *ChunkReader) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
var b []byte
|
||||||
|
var m int
|
||||||
|
// if b, _ := r.Slice(r.off-r.base, r.limit-r.base); b == nil {
|
||||||
|
// if slices not available we do it with extra allocation
|
||||||
|
b = make([]byte, r.limit-r.off)
|
||||||
|
m, err = r.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
m, err = w.Write(b)
|
||||||
|
if m > len(b) {
|
||||||
|
panic("bytes.Reader.WriteTo: invalid Write count")
|
||||||
|
}
|
||||||
|
r.off = r.base + int64(m)
|
||||||
|
n = int64(m)
|
||||||
|
if m != len(b) && err == nil {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
// w
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LazyChunkReader) Size() (n int64) {
|
||||||
|
self.ReadAt(nil, 0)
|
||||||
|
return self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LazyChunkReader) Read(b []byte) (read int, err error) {
|
||||||
|
read, err = self.ReadAt(b, self.off)
|
||||||
|
self.off += int64(read)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LazyChunkReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
default:
|
||||||
|
return 0, errWhence
|
||||||
|
case 0:
|
||||||
|
offset += 0
|
||||||
|
case 1:
|
||||||
|
offset += s.off
|
||||||
|
case 2:
|
||||||
|
offset += s.size
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errOffset
|
||||||
|
}
|
||||||
|
s.off = offset
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
438
bzz/chunker.go
Normal file
438
bzz/chunker.go
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
/*
|
||||||
|
The distributed storage implemented in this package requires fix sized chunks of content
|
||||||
|
Chunker is the interface to a component that is responsible for disassembling and assembling larger data.
|
||||||
|
|
||||||
|
TreeChunker implements a Chunker based on a tree structure defined as follows:
|
||||||
|
|
||||||
|
1 each node in the tree including the root and other branching nodes are stored as a chunk.
|
||||||
|
|
||||||
|
2 branching nodes encode data contents that includes the size of the dataslice covered by its entire subtree under the node as well as the hash keys of all its children :
|
||||||
|
data_{i} := size(subtree_{i}) || key_{j} || key_{j+1} .... || key_{j+n-1}
|
||||||
|
|
||||||
|
3 Leaf nodes encode an actual subslice of the input data.
|
||||||
|
|
||||||
|
4 if data size is not more than maximum chunksize, the data is stored in a single chunk
|
||||||
|
key = sha256(int64(size) + data)
|
||||||
|
|
||||||
|
2 if data size is more than chunksize*Branches^l, but no more than chunksize*
|
||||||
|
Branches^(l+1), the data vector is split into slices of chunksize*
|
||||||
|
Branches^l length (except the last one).
|
||||||
|
key = sha256(int64(size) + key(slice0) + key(slice1) + ...)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hasherfunc crypto.Hash = crypto.SHA256 // http://golang.org/pkg/hash/#Hash
|
||||||
|
defaultBranches int64 = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// hashSize int64 = hasherfunc.New().Size() // hasher knows about its own length in bytes
|
||||||
|
// chunksize int64 = branches * hashSize // chunk is defined as this
|
||||||
|
joinTimeout = 120 * time.Second
|
||||||
|
splitTimeout = 120 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Key []byte
|
||||||
|
|
||||||
|
/*
|
||||||
|
Chunker is the interface to a component that is responsible for disassembling and assembling larger data and indended to be the dependency of a DPA storage system with fixed maximum chunksize.
|
||||||
|
It relies on the underlying chunking model.
|
||||||
|
When calling Split, the caller provides a channel (chan *Chunk) on which it receives chunks to store. The DPA delegates to storage layers (implementing ChunkStore interface). NewChunkstore(DB) is a convenience wrapper with which all DBs (conforming to DB interface) can serve as ChunkStores. See chunkStore.go
|
||||||
|
After getting notified that all the data has been split (the error channel is closed), the caller can safely read or save the root key. Optionally it times out if not all chunks get stored or not the entire stream of data has been processed. By inspecting the errc channel the caller can check if any explicit errors (typically IO read/write failures) occured during splitting.
|
||||||
|
|
||||||
|
When calling Join with a root key, the caller gets returned a lazy reader. The caller again provides a channel and receives an error channel. The chunk channel is the one on which the caller receives placeholder chunks with missing data. The DPA is supposed to forward this to the chunk stores and notify the chunker if the data has been delivered (i.e. retrieved from memory cache, disk-persisted db or cloud based swarm delivery). The chunker then puts these together and notifies the DPA if data has been assembled by a closed error channel. Once the DPA finds the data has been joined, it is free to deliver it back to swarm in full (if the original request was via the bzz protocol) or save and serve if it it was a local client request.
|
||||||
|
|
||||||
|
*/
|
||||||
|
type Chunker interface {
|
||||||
|
/*
|
||||||
|
When splitting, data is given as a SectionReader, and the key is a hashSize long byte slice (Key), the root hash of the entire content will fill this once processing finishes.
|
||||||
|
New chunks to store are coming to caller via the chunk storage channel, which the caller provides.
|
||||||
|
wg is a Waitgroup (can be nil) that can be used to block until the local storage finishes
|
||||||
|
The caller gets returned an error channel, if an error is encountered during splitting, it is fed to errC error channel.
|
||||||
|
A closed error signals process completion at which point the key can be considered final if there were no errors.
|
||||||
|
*/
|
||||||
|
Split(key Key, data SectionReader, chunkC chan *Chunk, wg *sync.WaitGroup) chan error
|
||||||
|
/*
|
||||||
|
Join reconstructs original content based on a root key.
|
||||||
|
When joining, the caller gets returned a Lazy SectionReader
|
||||||
|
New chunks to retrieve are coming to caller via the Chunk channel, which the caller provides.
|
||||||
|
If an error is encountered during joining, it appears as a reader error.
|
||||||
|
The SectionReader provides on-demand fetching of chunks.
|
||||||
|
*/
|
||||||
|
Join(key Key, chunkC chan *Chunk) SectionReader
|
||||||
|
|
||||||
|
// returns the key length
|
||||||
|
KeySize() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tree chunker is a concrete implementation of data chunking.
|
||||||
|
This chunker works in a simple way, it builds a tree out of the document so that each node either represents a chunk of real data or a chunk of data representing an branching non-leaf node of the tree. In particular each such non-leaf chunk will represent is a concatenation of the hash of its respective children. This scheme simultaneously guarantees data integrity as well as self addressing. Abstract nodes are transparent since their represented size component is strictly greater than their maximum data size, since they encode a subtree.
|
||||||
|
|
||||||
|
If all is well it is possible to implement this by simply composing readers so that no extra allocation or buffering is necessary for the data splitting and joining. This means that in principle there can be direct IO between : memory, file system, network socket (bzz peers storage request is read from the socket ). In practice there may be need for several stages of internal buffering.
|
||||||
|
Unfortunately the hashing itself does use extra copies and allocation though since it does need it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type TreeChunker struct {
|
||||||
|
Branches int64
|
||||||
|
HashFunc crypto.Hash
|
||||||
|
JoinTimeout time.Duration
|
||||||
|
SplitTimeout time.Duration
|
||||||
|
// calculated
|
||||||
|
hashSize int64 // self.HashFunc.New().Size()
|
||||||
|
chunkSize int64 // hashSize* Branches
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TreeChunker) Init() {
|
||||||
|
if self.HashFunc == 0 {
|
||||||
|
self.HashFunc = hasherfunc
|
||||||
|
}
|
||||||
|
if self.Branches == 0 {
|
||||||
|
self.Branches = defaultBranches
|
||||||
|
}
|
||||||
|
if self.JoinTimeout == 0 {
|
||||||
|
self.JoinTimeout = joinTimeout
|
||||||
|
}
|
||||||
|
if self.SplitTimeout == 0 {
|
||||||
|
self.SplitTimeout = splitTimeout
|
||||||
|
}
|
||||||
|
self.hashSize = int64(self.HashFunc.New().Size())
|
||||||
|
self.chunkSize = self.hashSize * self.Branches
|
||||||
|
// dpaLogger.Debugf("Chunker initialised: branches: %v, hashsize: %v, chunksize: %v, join timeout: %v , split timeout: %v", self.Branches, self.hashSize, self.chunkSize, self.JoinTimeout, self.SplitTimeout)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TreeChunker) KeySize() int64 {
|
||||||
|
return self.hashSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// String() for pretty printing
|
||||||
|
func (self *Chunk) String() string {
|
||||||
|
var size int64
|
||||||
|
var n int
|
||||||
|
return fmt.Sprintf("Key: [%x..] TreeSize: %v Chunksize: %v Data: %x\n", self.Key[:4], self.Size, size, self.SData[:n])
|
||||||
|
}
|
||||||
|
|
||||||
|
// The treeChunkers own Hash hashes together
|
||||||
|
// - the size (of the subtree encoded in the Chunk)
|
||||||
|
// - the Chunk, ie. the contents read from the input reader
|
||||||
|
func (self *TreeChunker) Hash(input []byte) []byte {
|
||||||
|
hasher := self.HashFunc.New()
|
||||||
|
hasher.Write(input)
|
||||||
|
return hasher.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TreeChunker) Split(key Key, data SectionReader, chunkC chan *Chunk, swg *sync.WaitGroup) (errC chan error) {
|
||||||
|
|
||||||
|
if swg != nil {
|
||||||
|
swg.Add(1)
|
||||||
|
defer swg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.chunkSize <= 0 {
|
||||||
|
panic("chunker must be initialised")
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(len(key)) != self.hashSize {
|
||||||
|
panic(fmt.Sprintf("root key buffer must be allocated byte slice of length %d", self.hashSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
errC = make(chan error)
|
||||||
|
rerrC := make(chan error)
|
||||||
|
timeout := time.After(self.SplitTimeout)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
|
||||||
|
depth := 0
|
||||||
|
treeSize := self.chunkSize
|
||||||
|
size := data.Size()
|
||||||
|
// takes lowest depth such that chunksize*HashCount^(depth+1) > size
|
||||||
|
// power series, will find the order of magnitude of the data size in base hashCount or numbers of levels of branching in the resulting tree.
|
||||||
|
|
||||||
|
for ; treeSize < size; treeSize *= self.Branches {
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
|
||||||
|
// dpaLogger.Debugf("split request received for data (%v bytes, depth: %v)", size, depth)
|
||||||
|
|
||||||
|
//launch actual recursive function passing the workgroup
|
||||||
|
self.split(depth, treeSize/self.Branches, key, data, chunkC, rerrC, wg, swg)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// closes internal error channel if all subprocesses in the workgroup finished
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(rerrC)
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
// waiting for request to end with wg finishing, error, or timeout
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case err := <-rerrC:
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
} // otherwise splitting is complete
|
||||||
|
case <-timeout:
|
||||||
|
errC <- fmt.Errorf("split time out")
|
||||||
|
}
|
||||||
|
close(errC)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TreeChunker) split(depth int, treeSize int64, key Key, data SectionReader, chunkC chan *Chunk, errc chan error, parentWg *sync.WaitGroup, swg *sync.WaitGroup) {
|
||||||
|
|
||||||
|
defer parentWg.Done()
|
||||||
|
|
||||||
|
size := data.Size()
|
||||||
|
var newChunk *Chunk
|
||||||
|
var hash Key
|
||||||
|
// dpaLogger.Debugf("depth: %v, max subtree size: %v, data size: %v", depth, treeSize, size)
|
||||||
|
|
||||||
|
for depth > 0 && size < treeSize {
|
||||||
|
treeSize /= self.Branches
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth == 0 {
|
||||||
|
// leaf nodes -> content chunks
|
||||||
|
chunkData := make([]byte, data.Size()+8)
|
||||||
|
binary.LittleEndian.PutUint64(chunkData[0:8], uint64(size))
|
||||||
|
data.ReadAt(chunkData[8:], 0)
|
||||||
|
hash = self.Hash(chunkData)
|
||||||
|
// dpaLogger.Debugf("content chunk: max subtree size: %v, data size: %v", treeSize, size)
|
||||||
|
newChunk = &Chunk{
|
||||||
|
Key: hash,
|
||||||
|
SData: chunkData,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// intermediate chunk containing child nodes hashes
|
||||||
|
branchCnt := int64((size + treeSize - 1) / treeSize)
|
||||||
|
// dpaLogger.Debugf("intermediate node: setting branches: %v, depth: %v, max subtree size: %v, data size: %v", branches, depth, treeSize, size)
|
||||||
|
|
||||||
|
var chunk []byte = make([]byte, branchCnt*self.hashSize+8)
|
||||||
|
var pos, i int64
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint64(chunk[0:8], uint64(size))
|
||||||
|
|
||||||
|
childrenWg := &sync.WaitGroup{}
|
||||||
|
var secSize int64
|
||||||
|
for i < branchCnt {
|
||||||
|
// the last item can have shorter data
|
||||||
|
if size-pos < treeSize {
|
||||||
|
secSize = size - pos
|
||||||
|
} else {
|
||||||
|
secSize = treeSize
|
||||||
|
}
|
||||||
|
// take the section of the data corresponding encoded in the subTree
|
||||||
|
subTreeData := NewChunkReader(data, pos, secSize)
|
||||||
|
// the hash of that data
|
||||||
|
subTreeKey := chunk[8+i*self.hashSize : 8+(i+1)*self.hashSize]
|
||||||
|
|
||||||
|
childrenWg.Add(1)
|
||||||
|
go self.split(depth-1, treeSize/self.Branches, subTreeKey, subTreeData, chunkC, errc, childrenWg, swg)
|
||||||
|
|
||||||
|
i++
|
||||||
|
pos += treeSize
|
||||||
|
}
|
||||||
|
// wait for all the children to complete calculating their hashes and copying them onto sections of the chunk
|
||||||
|
childrenWg.Wait()
|
||||||
|
// now we got the hashes in the chunk, then hash the chunk
|
||||||
|
/* chunkReader := NewChunkReaderFromBytes(chunk) // bytes.Reader almost implements SectionReader
|
||||||
|
chunkData := make([]byte, chunkReader.Size())
|
||||||
|
chunkReader.ReadAt(chunkData, 0)*/
|
||||||
|
|
||||||
|
hash = self.Hash(chunk)
|
||||||
|
newChunk = &Chunk{
|
||||||
|
Key: hash,
|
||||||
|
SData: chunk,
|
||||||
|
Size: size,
|
||||||
|
wg: swg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if swg != nil {
|
||||||
|
swg.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send off new chunk to storage
|
||||||
|
if chunkC != nil {
|
||||||
|
chunkC <- newChunk
|
||||||
|
}
|
||||||
|
// report hash of this chunk one level up (keys corresponds to the proper subslice of the parent chunk)x
|
||||||
|
copy(key, hash)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *TreeChunker) Join(key Key, chunkC chan *Chunk) SectionReader {
|
||||||
|
|
||||||
|
return &LazyChunkReader{
|
||||||
|
key: key,
|
||||||
|
chunkC: chunkC,
|
||||||
|
quitC: make(chan bool),
|
||||||
|
errC: make(chan error),
|
||||||
|
chunker: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LazyChunkReader implements LazySectionReader
|
||||||
|
type LazyChunkReader struct {
|
||||||
|
key Key
|
||||||
|
chunkC chan *Chunk
|
||||||
|
size int64
|
||||||
|
off int64
|
||||||
|
quitC chan bool
|
||||||
|
errC chan error
|
||||||
|
chunker *TreeChunker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LazyChunkReader) ReadAt(b []byte, off int64) (read int, err error) {
|
||||||
|
self.errC = make(chan error)
|
||||||
|
chunk := &Chunk{
|
||||||
|
Key: self.key,
|
||||||
|
C: make(chan bool), // close channel to signal data delivery
|
||||||
|
}
|
||||||
|
self.chunkC <- chunk // submit retrieval request, someone should be listening on the other side (or we will time out globally)
|
||||||
|
dpaLogger.Debugf("readAt: reading %x into %d bytes at offset %d.", chunk.Key[:4], len(b), off)
|
||||||
|
|
||||||
|
// waiting for the chunk retrieval
|
||||||
|
select {
|
||||||
|
case <-self.quitC:
|
||||||
|
// this is how we control process leakage (quitC is closed once join is finished (after timeout))
|
||||||
|
// dpaLogger.Debugf("quit")
|
||||||
|
return
|
||||||
|
case <-chunk.C: // bells are ringing, data have been delivered
|
||||||
|
// dpaLogger.Debugf("chunk data received for %x", chunk.Key[:4])
|
||||||
|
}
|
||||||
|
if len(chunk.SData) == 0 {
|
||||||
|
// dpaLogger.Debugf("No payload in %x", chunk.Key)
|
||||||
|
return 0, notFound
|
||||||
|
}
|
||||||
|
chunk.Size = int64(binary.LittleEndian.Uint64(chunk.SData[0:8]))
|
||||||
|
self.size = chunk.Size
|
||||||
|
if b == nil {
|
||||||
|
// dpaLogger.Debugf("Size query for %x", chunk.Key[:4])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := int64(len(b))
|
||||||
|
if off+want > self.size {
|
||||||
|
want = self.size - off
|
||||||
|
}
|
||||||
|
var treeSize int64
|
||||||
|
var depth int
|
||||||
|
// calculate depth and max treeSize
|
||||||
|
treeSize = self.chunker.chunkSize
|
||||||
|
for ; treeSize < chunk.Size; treeSize *= self.chunker.Branches {
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
go self.join(b, off, off+want, depth, treeSize/self.chunker.Branches, chunk, &wg)
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(self.errC)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err = <-self.errC:
|
||||||
|
dpaLogger.Debugf("ReadAt received %v", err)
|
||||||
|
read = len(b)
|
||||||
|
if off+int64(read) == self.size {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("ReadAt returning at %d: %v", read, err)
|
||||||
|
case <-self.quitC:
|
||||||
|
dpaLogger.Debugf("ReadAt aborted at %d: %v", read, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LazyChunkReader) join(b []byte, off int64, eoff int64, depth int, treeSize int64, chunk *Chunk, parentWg *sync.WaitGroup) {
|
||||||
|
defer parentWg.Done()
|
||||||
|
|
||||||
|
// dpaLogger.Debugf("depth: %v, loff: %v, eoff: %v, chunk.Size: %v, treeSize: %v", depth, off, eoff, chunk.Size, treeSize)
|
||||||
|
|
||||||
|
chunk.Size = int64(binary.LittleEndian.Uint64(chunk.SData[0:8]))
|
||||||
|
|
||||||
|
// find appropriate block level
|
||||||
|
for chunk.Size < treeSize && depth > 0 {
|
||||||
|
treeSize /= self.chunker.Branches
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth == 0 {
|
||||||
|
// dpaLogger.Debugf("depth: %v, len(b): %v, off: %v, eoff: %v, chunk.Size: %v, treeSize: %v", depth, len(b), off, eoff, chunk.Size, treeSize)
|
||||||
|
if int64(len(b)) != eoff-off {
|
||||||
|
//fmt.Printf("len(b) = %v off = %v eoff = %v", len(b), off, eoff)
|
||||||
|
panic("len(b) does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(b, chunk.SData[8+off:8+eoff])
|
||||||
|
return // simply give back the chunks reader for content chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
// subtree index
|
||||||
|
start := off / treeSize
|
||||||
|
end := (eoff + treeSize - 1) / treeSize
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
|
||||||
|
soff := i * treeSize
|
||||||
|
roff := soff
|
||||||
|
seoff := soff + treeSize
|
||||||
|
|
||||||
|
if soff < off {
|
||||||
|
soff = off
|
||||||
|
}
|
||||||
|
if seoff > eoff {
|
||||||
|
seoff = eoff
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(j int64) {
|
||||||
|
childKey := chunk.SData[8+j*self.chunker.hashSize : 8+(j+1)*self.chunker.hashSize]
|
||||||
|
dpaLogger.Debugf("subtree index: %v -> %x", j, childKey[:4])
|
||||||
|
|
||||||
|
ch := &Chunk{
|
||||||
|
Key: childKey,
|
||||||
|
C: make(chan bool), // close channel to signal data delivery
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("chunk data sent for %x (key interval in chunk %v-%v)", ch.Key[:4], j*self.chunker.hashSize, (j+1)*self.chunker.hashSize)
|
||||||
|
self.chunkC <- ch // submit retrieval request, someone should be listening on the other side (or we will time out globally)
|
||||||
|
|
||||||
|
// waiting for the chunk retrieval
|
||||||
|
select {
|
||||||
|
case <-self.quitC:
|
||||||
|
// this is how we control process leakage (quitC is closed once join is finished (after timeout))
|
||||||
|
return
|
||||||
|
case <-ch.C: // bells are ringing, data have been delivered
|
||||||
|
// dpaLogger.Debugf("chunk data received")
|
||||||
|
}
|
||||||
|
if soff < off {
|
||||||
|
soff = off
|
||||||
|
}
|
||||||
|
if len(ch.SData) == 0 {
|
||||||
|
self.errC <- fmt.Errorf("chunk %v-%v not found", off, off+treeSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.join(b[soff-off:seoff-off], soff-roff, seoff-roff, depth-1, treeSize/self.chunker.Branches, ch, &wg)
|
||||||
|
}(i)
|
||||||
|
} //for
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
220
bzz/chunker_test.go
Normal file
220
bzz/chunker_test.go
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
// "fmt"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tests TreeChunker by splitting and joining a random byte slice
|
||||||
|
*/
|
||||||
|
|
||||||
|
type chunkerTester struct {
|
||||||
|
errors []error
|
||||||
|
chunks []*Chunk
|
||||||
|
timeout bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *chunkerTester) checkChunks(t *testing.T, want int) {
|
||||||
|
l := len(self.chunks)
|
||||||
|
if l != want {
|
||||||
|
t.Errorf("expected %v chunks, got %v", want, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *chunkerTester) Split(chunker *TreeChunker, l int) (key Key, input []byte) {
|
||||||
|
// reset
|
||||||
|
self.errors = nil
|
||||||
|
self.chunks = nil
|
||||||
|
self.timeout = false
|
||||||
|
|
||||||
|
data, slice := testDataReader(l)
|
||||||
|
input = slice
|
||||||
|
key = make([]byte, 32)
|
||||||
|
chunkC := make(chan *Chunk, 1000)
|
||||||
|
errC := chunker.Split(key, data, chunkC, nil)
|
||||||
|
quitC := make(chan bool)
|
||||||
|
timeout := time.After(600 * time.Second)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
LOOP:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
self.timeout = true
|
||||||
|
break LOOP
|
||||||
|
|
||||||
|
case chunk := <-chunkC:
|
||||||
|
if chunk != nil {
|
||||||
|
self.chunks = append(self.chunks, chunk)
|
||||||
|
} else {
|
||||||
|
break LOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
case err, ok := <-errC:
|
||||||
|
if err != nil {
|
||||||
|
self.errors = append(self.errors, err)
|
||||||
|
}
|
||||||
|
// fmt.Printf("err %v", err)
|
||||||
|
if !ok {
|
||||||
|
close(chunkC)
|
||||||
|
errC = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(quitC)
|
||||||
|
}()
|
||||||
|
<-quitC // waiting for it to finish
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *chunkerTester) Join(chunker *TreeChunker, key Key, c int) SectionReader {
|
||||||
|
// reset but not the chunks
|
||||||
|
self.errors = nil
|
||||||
|
self.timeout = false
|
||||||
|
chunkC := make(chan *Chunk, 1000)
|
||||||
|
|
||||||
|
reader := chunker.Join(key, chunkC)
|
||||||
|
|
||||||
|
quitC := make(chan bool)
|
||||||
|
timeout := time.After(600 * time.Second)
|
||||||
|
i := 0
|
||||||
|
go func() {
|
||||||
|
LOOP:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-quitC:
|
||||||
|
break LOOP
|
||||||
|
|
||||||
|
case <-timeout:
|
||||||
|
self.timeout = true
|
||||||
|
break LOOP
|
||||||
|
|
||||||
|
case chunk := <-chunkC:
|
||||||
|
i++
|
||||||
|
// dpaLogger.DebugDetailf("TESTER: chunk request %x", chunk.Key[:4])
|
||||||
|
// this just mocks the behaviour of a chunk store retrieval
|
||||||
|
var found bool
|
||||||
|
for _, ch := range self.chunks {
|
||||||
|
if bytes.Equal(chunk.Key, ch.Key) {
|
||||||
|
found = true
|
||||||
|
chunk.SData = ch.SData
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
// fmt.Printf("TESTER: chunk unknown for %x", chunk.Key[:4])
|
||||||
|
}
|
||||||
|
close(chunk.C)
|
||||||
|
// dpaLogger.DebugDetailf("TESTER: chunk request served %x", chunk.Key[:4])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRandomData(chunker *TreeChunker, tester *chunkerTester, n int, chunks int, t *testing.T) {
|
||||||
|
key, input := tester.Split(chunker, n)
|
||||||
|
|
||||||
|
t.Logf(" Key = %x\n", key)
|
||||||
|
|
||||||
|
tester.checkChunks(t, chunks)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
reader := tester.Join(chunker, key, 0)
|
||||||
|
output := make([]byte, n)
|
||||||
|
r, err := reader.Read(output)
|
||||||
|
if r != n || err != io.EOF {
|
||||||
|
t.Errorf("read error read: %v n = %v err = %v\n", r, n, err)
|
||||||
|
}
|
||||||
|
// t.Logf(" IN: %x\nOUT: %x\n", input, output)
|
||||||
|
if !bytes.Equal(output, input) {
|
||||||
|
t.Errorf("input and output mismatch\n IN: %x\nOUT: %x\n", input, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomData(t *testing.T) {
|
||||||
|
chunker := &TreeChunker{
|
||||||
|
Branches: 2,
|
||||||
|
SplitTimeout: 10 * time.Second,
|
||||||
|
JoinTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
chunker.Init()
|
||||||
|
tester := &chunkerTester{}
|
||||||
|
testRandomData(chunker, tester, 60, 1, t)
|
||||||
|
testRandomData(chunker, tester, 179, 5, t)
|
||||||
|
testRandomData(chunker, tester, 253, 7, t)
|
||||||
|
// t.Logf("chunks %v", tester.chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chunkerAndTester() (chunker *TreeChunker, tester *chunkerTester) {
|
||||||
|
chunker = &TreeChunker{
|
||||||
|
Branches: 2,
|
||||||
|
SplitTimeout: 10 * time.Second,
|
||||||
|
JoinTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
chunker.Init()
|
||||||
|
tester = &chunkerTester{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAll(reader SectionReader, result []byte) {
|
||||||
|
size := int64(len(result))
|
||||||
|
|
||||||
|
var end int64
|
||||||
|
for pos := int64(0); pos < size; pos += 1000 {
|
||||||
|
if pos+1000 > size {
|
||||||
|
end = size
|
||||||
|
} else {
|
||||||
|
end = pos + 1000
|
||||||
|
}
|
||||||
|
reader.ReadAt(result[pos:end], pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchReadAll(reader SectionReader) {
|
||||||
|
size := reader.Size()
|
||||||
|
output := make([]byte, 1000)
|
||||||
|
for pos := int64(0); pos < size; pos += 1000 {
|
||||||
|
reader.ReadAt(output, pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkJoinRandomData(n int, chunks int, t *testing.B) {
|
||||||
|
t.StopTimer()
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
// fmt.Printf("round %v\n", i)
|
||||||
|
chunker, tester := chunkerAndTester()
|
||||||
|
key, _ := tester.Split(chunker, n)
|
||||||
|
// fmt.Printf("split done %v, joining...\n", i)
|
||||||
|
t.StartTimer()
|
||||||
|
reader := tester.Join(chunker, key, i)
|
||||||
|
// fmt.Printf("join done %v, reading...\n", i)
|
||||||
|
benchReadAll(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkSplitRandomData(n int, chunks int, t *testing.B) {
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
chunker, tester := chunkerAndTester()
|
||||||
|
tester.Split(chunker, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkJoinRandomData_100_2(t *testing.B) { benchmarkJoinRandomData(100, 3, t) }
|
||||||
|
func BenchmarkJoinRandomData_1000_2(t *testing.B) { benchmarkJoinRandomData(1000, 3, t) }
|
||||||
|
func BenchmarkJoinRandomData_10000_2(t *testing.B) { benchmarkJoinRandomData(10000, 3, t) }
|
||||||
|
func BenchmarkJoinRandomData_100000_2(t *testing.B) { benchmarkJoinRandomData(100000, 3, t) }
|
||||||
|
func BenchmarkJoinRandomData_1000000_2(t *testing.B) { benchmarkJoinRandomData(1000000, 3, t) }
|
||||||
|
|
||||||
|
func BenchmarkSplitRandomData_100_2(t *testing.B) { benchmarkSplitRandomData(100, 3, t) }
|
||||||
|
func BenchmarkSplitRandomData_1000_2(t *testing.B) { benchmarkSplitRandomData(1000, 3, t) }
|
||||||
|
func BenchmarkSplitRandomData_10000_2(t *testing.B) { benchmarkSplitRandomData(10000, 3, t) }
|
||||||
|
func BenchmarkSplitRandomData_100000_2(t *testing.B) { benchmarkSplitRandomData(100000, 3, t) }
|
||||||
|
func BenchmarkSplitRandomData_1000000_2(t *testing.B) { benchmarkSplitRandomData(1000000, 3, t) }
|
||||||
|
func BenchmarkSplitRandomData_10000000_2(t *testing.B) { benchmarkSplitRandomData(10000000, 3, t) }
|
||||||
|
|
||||||
|
// go test -bench ./bzz -cpuprofile cpu.out -memprofile mem.out
|
||||||
89
bzz/common_test.go
Normal file
89
bzz/common_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testDataReader(l int) (r *ChunkReader, slice []byte) {
|
||||||
|
slice = make([]byte, l)
|
||||||
|
if _, err := rand.Read(slice); err != nil {
|
||||||
|
panic("rand error")
|
||||||
|
}
|
||||||
|
r = NewChunkReaderFromBytes(slice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomChunks(l int64, branches int64, chunkC chan *Chunk) (key Key, errC chan error) {
|
||||||
|
chunker := &TreeChunker{
|
||||||
|
Branches: branches,
|
||||||
|
}
|
||||||
|
chunker.Init()
|
||||||
|
key = make([]byte, 32)
|
||||||
|
b := make([]byte, l)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic("no rand")
|
||||||
|
}
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
errC = chunker.Split(key, NewChunkReaderFromBytes(b), chunkC, wg)
|
||||||
|
wg.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStore(m ChunkStore, l int64, branches int64, t *testing.T) {
|
||||||
|
|
||||||
|
chunkC := make(chan *Chunk)
|
||||||
|
key, errC := randomChunks(l, branches, chunkC)
|
||||||
|
|
||||||
|
SPLIT:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case chunk := <-chunkC:
|
||||||
|
m.Put(chunk)
|
||||||
|
case err, ok := <-errC:
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Chunker error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break SPLIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunker := &TreeChunker{
|
||||||
|
Branches: branches,
|
||||||
|
}
|
||||||
|
chunker.Init()
|
||||||
|
chunkC = make(chan *Chunk)
|
||||||
|
var r SectionReader
|
||||||
|
r = chunker.Join(key, chunkC)
|
||||||
|
|
||||||
|
quit := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for ch := range chunkC {
|
||||||
|
go func(chunk *Chunk) {
|
||||||
|
storedChunk, err := m.Get(chunk.Key)
|
||||||
|
if err == notFound {
|
||||||
|
dpaLogger.DebugDetailf("chunk '%x' not found", chunk.Key)
|
||||||
|
} else if err != nil {
|
||||||
|
dpaLogger.DebugDetailf("error retrieving chunk %x: %v", chunk.Key, err)
|
||||||
|
} else {
|
||||||
|
chunk.SData = storedChunk.SData
|
||||||
|
}
|
||||||
|
dpaLogger.DebugDetailf("chunk '%x' not found", chunk.Key[:4])
|
||||||
|
close(chunk.C)
|
||||||
|
}(ch)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
b := make([]byte, l)
|
||||||
|
n, err := r.ReadAt(b, 0)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("read error (%v/%v) %v", n, l, err)
|
||||||
|
close(quit)
|
||||||
|
}
|
||||||
|
}
|
||||||
96
bzz/database.go
Normal file
96
bzz/database.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
// this is a clone of an earlier state of the ethereum ethdb/database
|
||||||
|
// no need for queueing/caching
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/compression/rle"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const openFileLimit = 128
|
||||||
|
|
||||||
|
type LDBDatabase struct {
|
||||||
|
db *leveldb.DB
|
||||||
|
comp bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLDBDatabase(file string) (*LDBDatabase, error) {
|
||||||
|
// Open the db
|
||||||
|
db, err := leveldb.OpenFile(file, &opt.Options{OpenFilesCacheCapacity: openFileLimit})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
database := &LDBDatabase{db: db, comp: false}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Put(key []byte, value []byte) {
|
||||||
|
if self.comp {
|
||||||
|
value = rle.Compress(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := self.db.Put(key, value, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error put", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Get(key []byte) ([]byte, error) {
|
||||||
|
dat, err := self.db.Get(key, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.comp {
|
||||||
|
return rle.Decompress(dat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Delete(key []byte) error {
|
||||||
|
return self.db.Delete(key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) LastKnownTD() []byte {
|
||||||
|
data, _ := self.Get([]byte("LTD"))
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
data = []byte{0x0}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) NewIterator() iterator.Iterator {
|
||||||
|
return self.db.NewIterator(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Write(batch *leveldb.Batch) error {
|
||||||
|
return self.db.Write(batch, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Close() {
|
||||||
|
// Close the leveldb database
|
||||||
|
self.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Print() {
|
||||||
|
iter := self.db.NewIterator(nil, nil)
|
||||||
|
for iter.Next() {
|
||||||
|
key := iter.Key()
|
||||||
|
value := iter.Value()
|
||||||
|
|
||||||
|
fmt.Printf("%x(%d): ", key, len(key))
|
||||||
|
node := common.NewValueFromBytes(value)
|
||||||
|
fmt.Printf("%v\n", node)
|
||||||
|
}
|
||||||
|
}
|
||||||
427
bzz/dbstore.go
Normal file
427
bzz/dbstore.go
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
// disk storage layer for the package bzz
|
||||||
|
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gcArraySize = 10000
|
||||||
|
const gcArrayFreeRatio = 0.1
|
||||||
|
|
||||||
|
// key prefixes for leveldb storage
|
||||||
|
const kpIndex = 0
|
||||||
|
const kpData = 1
|
||||||
|
|
||||||
|
var keyAccessCnt = []byte{2}
|
||||||
|
var keyEntryCnt = []byte{3}
|
||||||
|
var keyDataIdx = []byte{4}
|
||||||
|
var keyGCPos = []byte{5}
|
||||||
|
|
||||||
|
type gcItem struct {
|
||||||
|
idx uint64
|
||||||
|
value uint64
|
||||||
|
idxKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbStore struct {
|
||||||
|
db *LDBDatabase
|
||||||
|
|
||||||
|
// this should be stored in db, accessed transactionally
|
||||||
|
entryCnt, accessCnt, dataIdx, capacity uint64
|
||||||
|
|
||||||
|
gcPos, gcStartPos []byte
|
||||||
|
gcArray []*gcItem
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type dpaDBIndex struct {
|
||||||
|
Idx uint64
|
||||||
|
Access uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToU64(data []byte) uint64 {
|
||||||
|
|
||||||
|
if len(data) < 8 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return binary.LittleEndian.Uint64(data)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func u64ToBytes(val uint64) []byte {
|
||||||
|
|
||||||
|
data := make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(data, val)
|
||||||
|
return data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexGCValue(index *dpaDBIndex) uint64 {
|
||||||
|
|
||||||
|
return index.Access
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) updateIndexAccess(index *dpaDBIndex) {
|
||||||
|
|
||||||
|
index.Access = s.accessCnt
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexKey(hash Key) []byte {
|
||||||
|
|
||||||
|
HashSize := len(hash)
|
||||||
|
key := make([]byte, HashSize+1)
|
||||||
|
key[0] = 0
|
||||||
|
// db keys derived from hash:
|
||||||
|
// two halves swapped for uniformly distributed prefix
|
||||||
|
copy(key[1:HashSize/2+1], hash[HashSize/2:HashSize])
|
||||||
|
copy(key[HashSize/2+1:HashSize+1], hash[0:HashSize/2])
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataKey(idx uint64) []byte {
|
||||||
|
|
||||||
|
key := make([]byte, 9)
|
||||||
|
key[0] = 1
|
||||||
|
binary.BigEndian.PutUint64(key[1:9], idx)
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeIndex(index *dpaDBIndex) []byte {
|
||||||
|
|
||||||
|
data, _ := rlp.EncodeToBytes(index)
|
||||||
|
return data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeData(chunk *Chunk) []byte {
|
||||||
|
|
||||||
|
/* var rlpEntry struct {
|
||||||
|
Data []byte
|
||||||
|
Size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
rlpEntry.Data = chunk.Data
|
||||||
|
rlpEntry.Size = uint64(chunk.Size)
|
||||||
|
|
||||||
|
data, _ := rlp.EncodeToBytes(rlpEntry)
|
||||||
|
return data*/
|
||||||
|
|
||||||
|
return chunk.SData
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeIndex(data []byte, index *dpaDBIndex) {
|
||||||
|
|
||||||
|
dec := rlp.NewStream(bytes.NewReader(data), 0)
|
||||||
|
dec.Decode(index)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeData(data []byte, chunk *Chunk) {
|
||||||
|
|
||||||
|
/* var rlpEntry struct {
|
||||||
|
Data []byte
|
||||||
|
Size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := rlp.NewStream(bytes.NewReader(data))
|
||||||
|
err := dec.Decode(&rlpEntry)
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
chunk.Data = rlpEntry.Data
|
||||||
|
chunk.Size = int64(rlpEntry.Size)*/
|
||||||
|
|
||||||
|
chunk.SData = data
|
||||||
|
chunk.Size = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func gcListPartition(list []*gcItem, left int, right int, pivotIndex int) int {
|
||||||
|
pivotValue := list[pivotIndex].value
|
||||||
|
dd := list[pivotIndex]
|
||||||
|
list[pivotIndex] = list[right]
|
||||||
|
list[right] = dd
|
||||||
|
storeIndex := left
|
||||||
|
for i := left; i < right; i++ {
|
||||||
|
if list[i].value < pivotValue {
|
||||||
|
dd = list[storeIndex]
|
||||||
|
list[storeIndex] = list[i]
|
||||||
|
list[i] = dd
|
||||||
|
storeIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dd = list[storeIndex]
|
||||||
|
list[storeIndex] = list[right]
|
||||||
|
list[right] = dd
|
||||||
|
return storeIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func gcListSelect(list []*gcItem, left int, right int, n int) int {
|
||||||
|
if left == right {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
pivotIndex := (left + right) / 2
|
||||||
|
pivotIndex = gcListPartition(list, left, right, pivotIndex)
|
||||||
|
if n == pivotIndex {
|
||||||
|
return n
|
||||||
|
} else {
|
||||||
|
if n < pivotIndex {
|
||||||
|
return gcListSelect(list, left, pivotIndex-1, n)
|
||||||
|
} else {
|
||||||
|
return gcListSelect(list, pivotIndex+1, right, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) collectGarbage(ratio float32) {
|
||||||
|
|
||||||
|
it := s.db.NewIterator()
|
||||||
|
it.Seek(s.gcPos)
|
||||||
|
if it.Valid() {
|
||||||
|
s.gcPos = it.Key()
|
||||||
|
} else {
|
||||||
|
s.gcPos = nil
|
||||||
|
}
|
||||||
|
gcnt := 0
|
||||||
|
|
||||||
|
for (gcnt < gcArraySize) && (uint64(gcnt) < s.entryCnt) {
|
||||||
|
|
||||||
|
if (s.gcPos == nil) || (s.gcPos[0] != kpIndex) {
|
||||||
|
it.Seek(s.gcStartPos)
|
||||||
|
if it.Valid() {
|
||||||
|
s.gcPos = it.Key()
|
||||||
|
} else {
|
||||||
|
s.gcPos = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.gcPos == nil) || (s.gcPos[0] != kpIndex) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
gci := new(gcItem)
|
||||||
|
gci.idxKey = s.gcPos
|
||||||
|
var index dpaDBIndex
|
||||||
|
decodeIndex(it.Value(), &index)
|
||||||
|
gci.idx = index.Idx
|
||||||
|
// the smaller, the more likely to be gc'd
|
||||||
|
gci.value = getIndexGCValue(&index)
|
||||||
|
s.gcArray[gcnt] = gci
|
||||||
|
gcnt++
|
||||||
|
it.Next()
|
||||||
|
if it.Valid() {
|
||||||
|
s.gcPos = it.Key()
|
||||||
|
} else {
|
||||||
|
s.gcPos = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cutidx := gcListSelect(s.gcArray, 0, gcnt-1, int(float32(gcnt)*ratio))
|
||||||
|
cutval := s.gcArray[cutidx].value
|
||||||
|
|
||||||
|
// fmt.Print(gcnt, " ", s.entryCnt, " ")
|
||||||
|
|
||||||
|
// actual gc
|
||||||
|
for i := 0; i < gcnt; i++ {
|
||||||
|
if s.gcArray[i].value <= cutval {
|
||||||
|
batch := new(leveldb.Batch)
|
||||||
|
batch.Delete(s.gcArray[i].idxKey)
|
||||||
|
batch.Delete(getDataKey(s.gcArray[i].idx))
|
||||||
|
s.entryCnt--
|
||||||
|
batch.Put(keyEntryCnt, u64ToBytes(s.entryCnt))
|
||||||
|
s.db.Write(batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Println(s.entryCnt)
|
||||||
|
|
||||||
|
s.db.Put(keyGCPos, s.gcPos)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) Put(chunk *Chunk) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
ikey := getIndexKey(chunk.Key)
|
||||||
|
var index dpaDBIndex
|
||||||
|
|
||||||
|
if s.tryAccessIdx(ikey, &index) {
|
||||||
|
if chunk.dbStored != nil {
|
||||||
|
close(chunk.dbStored)
|
||||||
|
}
|
||||||
|
return // already exists, only update access
|
||||||
|
}
|
||||||
|
|
||||||
|
data := encodeData(chunk)
|
||||||
|
//data := ethutil.Encode([]interface{}{entry})
|
||||||
|
|
||||||
|
if s.entryCnt >= s.capacity {
|
||||||
|
s.collectGarbage(gcArrayFreeRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := new(leveldb.Batch)
|
||||||
|
|
||||||
|
s.entryCnt++
|
||||||
|
batch.Put(keyEntryCnt, u64ToBytes(s.entryCnt))
|
||||||
|
s.dataIdx++
|
||||||
|
batch.Put(keyDataIdx, u64ToBytes(s.dataIdx))
|
||||||
|
s.accessCnt++
|
||||||
|
batch.Put(keyAccessCnt, u64ToBytes(s.accessCnt))
|
||||||
|
|
||||||
|
batch.Put(getDataKey(s.dataIdx), data)
|
||||||
|
|
||||||
|
index.Idx = s.dataIdx
|
||||||
|
s.updateIndexAccess(&index)
|
||||||
|
|
||||||
|
idata := encodeIndex(&index)
|
||||||
|
batch.Put(ikey, idata)
|
||||||
|
|
||||||
|
s.db.Write(batch)
|
||||||
|
if chunk.dbStored != nil {
|
||||||
|
close(chunk.dbStored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to find index; if found, update access cnt and return true
|
||||||
|
func (s *dbStore) tryAccessIdx(ikey []byte, index *dpaDBIndex) bool {
|
||||||
|
|
||||||
|
idata, err := s.db.Get(ikey)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
decodeIndex(idata, index)
|
||||||
|
|
||||||
|
batch := new(leveldb.Batch)
|
||||||
|
|
||||||
|
s.accessCnt++
|
||||||
|
batch.Put(keyAccessCnt, u64ToBytes(s.accessCnt))
|
||||||
|
s.updateIndexAccess(index)
|
||||||
|
idata = encodeIndex(index)
|
||||||
|
batch.Put(ikey, idata)
|
||||||
|
|
||||||
|
s.db.Write(batch)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) Get(key Key) (chunk *Chunk, err error) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
var index dpaDBIndex
|
||||||
|
|
||||||
|
if s.tryAccessIdx(getIndexKey(key), &index) {
|
||||||
|
var data []byte
|
||||||
|
data, err = s.db.Get(getDataKey(index.Idx))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := hasherfunc.New()
|
||||||
|
hasher.Write(data)
|
||||||
|
if bytes.Compare(hasher.Sum(nil), key) != 0 {
|
||||||
|
s.db.Delete(getDataKey(index.Idx))
|
||||||
|
err = notFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = &Chunk{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
decodeData(data, chunk)
|
||||||
|
} else {
|
||||||
|
err = notFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) updateAccessCnt(key Key) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
var index dpaDBIndex
|
||||||
|
s.tryAccessIdx(getIndexKey(key), &index) // result_chn == nil, only update access cnt
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) setCapacity(c uint64) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
s.capacity = c
|
||||||
|
|
||||||
|
if s.entryCnt > c {
|
||||||
|
var ratio float32
|
||||||
|
ratio = float32(1.01) - float32(c)/float32(s.entryCnt)
|
||||||
|
if ratio < gcArrayFreeRatio {
|
||||||
|
ratio = gcArrayFreeRatio
|
||||||
|
}
|
||||||
|
if ratio > 1 {
|
||||||
|
ratio = 1
|
||||||
|
}
|
||||||
|
for s.entryCnt > c {
|
||||||
|
s.collectGarbage(ratio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) getEntryCnt() uint64 {
|
||||||
|
|
||||||
|
return s.entryCnt
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDbStore(path string) (s *dbStore, err error) {
|
||||||
|
|
||||||
|
s = new(dbStore)
|
||||||
|
|
||||||
|
s.db, err = NewLDBDatabase(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setCapacity(5000000) // TODO define default capacity as constant
|
||||||
|
|
||||||
|
s.gcStartPos = make([]byte, 1)
|
||||||
|
s.gcStartPos[0] = kpIndex
|
||||||
|
s.gcArray = make([]*gcItem, gcArraySize)
|
||||||
|
|
||||||
|
data, _ := s.db.Get(keyEntryCnt)
|
||||||
|
s.entryCnt = bytesToU64(data)
|
||||||
|
data, _ = s.db.Get(keyAccessCnt)
|
||||||
|
s.accessCnt = bytesToU64(data)
|
||||||
|
data, _ = s.db.Get(keyDataIdx)
|
||||||
|
s.dataIdx = bytesToU64(data)
|
||||||
|
s.gcPos, _ = s.db.Get(keyGCPos)
|
||||||
|
if s.gcPos == nil {
|
||||||
|
s.gcPos = s.gcStartPos
|
||||||
|
}
|
||||||
|
return
|
||||||
|
// fmt.Println(s.entryCnt)
|
||||||
|
// fmt.Println(s.accessCnt)
|
||||||
|
// fmt.Println(s.dataIdx)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbStore) close() {
|
||||||
|
s.db.Close()
|
||||||
|
}
|
||||||
51
bzz/dbstore_test.go
Normal file
51
bzz/dbstore_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDbStore() (m *dbStore) {
|
||||||
|
os.RemoveAll("/tmp/bzz")
|
||||||
|
m, err := newDbStore("/tmp/bzz")
|
||||||
|
if err != nil {
|
||||||
|
panic("no dbStore")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDbStore(l int64, branches int64, t *testing.T) {
|
||||||
|
m := initDbStore()
|
||||||
|
defer m.close()
|
||||||
|
testStore(m, l, branches, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStore128_0x1000000(t *testing.T) {
|
||||||
|
testDbStore(0x1000000, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStore128_10000(t *testing.T) {
|
||||||
|
testDbStore(10000, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStore128_1000(t *testing.T) {
|
||||||
|
testDbStore(1000, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStore128_100(t *testing.T) {
|
||||||
|
testDbStore(100, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStore2_100(t *testing.T) {
|
||||||
|
testDbStore(100, 2, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbStoreNotFound(t *testing.T) {
|
||||||
|
m := initDbStore()
|
||||||
|
defer m.close()
|
||||||
|
zeroKey := make([]byte, 32)
|
||||||
|
_, err := m.Get(zeroKey)
|
||||||
|
if err != notFound {
|
||||||
|
t.Errorf("Expected notFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
172
bzz/dpa.go
Normal file
172
bzz/dpa.go
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
ethlogger "github.com/ethereum/go-ethereum/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
DPA provides the client API entrypoints Store and Retrieve to store and retrieve
|
||||||
|
It can store anything that has a byte slice representation, so files or serialised objects etc.
|
||||||
|
|
||||||
|
Storage: DPA calls the Chunker to segment the input datastream of any size to a merkle hashed tree of chunks. The key of the root block is returned to the client.
|
||||||
|
|
||||||
|
Retrieval: given the key of the root block, the DPA retrieves the block chunks and reconstructs the original data and passes it back as a lazy reader. A lazy reader is a reader with on-demand delayed processing, i.e. the chunks needed to reconstruct a large file are only fetched and processed if that particular part of the document is actually read.
|
||||||
|
|
||||||
|
As the chunker produces chunks, DPA dispatches them to the chunk store for storage or retrieval.
|
||||||
|
|
||||||
|
The ChunkStore interface is implemented by :
|
||||||
|
|
||||||
|
- memStore: a memory cache
|
||||||
|
- dbStore: local disk/db store
|
||||||
|
- localStore: a combination (sequence of) memStore and dbStore
|
||||||
|
- netStore: dht storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
const (
|
||||||
|
storeChanCapacity = 100
|
||||||
|
retrieveChanCapacity = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
notFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
var dpaLogger = ethlogger.NewLogger("BZZ")
|
||||||
|
|
||||||
|
type DPA struct {
|
||||||
|
Chunker Chunker
|
||||||
|
ChunkStore ChunkStore
|
||||||
|
storeC chan *Chunk
|
||||||
|
retrieveC chan *Chunk
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
running bool
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
quitC chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk serves also serves as a request object passed to ChunkStores
|
||||||
|
// in case it is a retrieval request, Data is nil and Size is 0
|
||||||
|
// Note that Size is not the size of the data chunk, which is Data.Size() see SectionReader
|
||||||
|
// but the size of the subtree encoded in the chunk
|
||||||
|
// 0 if request, to be supplied by the dpa
|
||||||
|
type Chunk struct {
|
||||||
|
SData []byte // nil if request, to be supplied by dpa
|
||||||
|
Size int64 // size of the data covered by the subtree encoded in this chunk
|
||||||
|
Key Key // always
|
||||||
|
C chan bool // to signal data delivery by the dpa
|
||||||
|
req *requestStatus //
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
dbStored chan bool
|
||||||
|
source *peer
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChunkStore interface {
|
||||||
|
Put(*Chunk) // effectively there is no error even if there is no error
|
||||||
|
Get(Key) (*Chunk, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) Retrieve(key Key) SectionReader {
|
||||||
|
|
||||||
|
return self.Chunker.Join(key, self.retrieveC)
|
||||||
|
// we can add subscriptions etc. or timeout here
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) Store(data SectionReader, wg *sync.WaitGroup) (key Key, err error) {
|
||||||
|
key = make([]byte, self.Chunker.KeySize())
|
||||||
|
errC := self.Chunker.Split(key, data, self.storeC, wg)
|
||||||
|
|
||||||
|
SPLIT:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err, ok := <-errC:
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Warnf("chunker split error: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break SPLIT
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-self.quitC:
|
||||||
|
break SPLIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) Start() {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if self.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.running = true
|
||||||
|
self.quitC = make(chan bool)
|
||||||
|
self.storeLoop()
|
||||||
|
self.retrieveLoop()
|
||||||
|
dpaLogger.Debugf("Swarm DPA started.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) Stop() {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if !self.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.running = false
|
||||||
|
close(self.quitC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) retrieveLoop() {
|
||||||
|
self.retrieveC = make(chan *Chunk, retrieveChanCapacity)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
RETRIEVE:
|
||||||
|
for ch := range self.retrieveC {
|
||||||
|
|
||||||
|
go func(chunk *Chunk) {
|
||||||
|
dpaLogger.DebugDetailf("dpa: retrieve loop : chunk '%x'", chunk.Key)
|
||||||
|
storedChunk, err := self.ChunkStore.Get(chunk.Key)
|
||||||
|
if err == notFound {
|
||||||
|
dpaLogger.DebugDetailf("chunk '%x' not found", chunk.Key)
|
||||||
|
} else if err != nil {
|
||||||
|
dpaLogger.DebugDetailf("error retrieving chunk %x: %v", chunk.Key, err)
|
||||||
|
} else {
|
||||||
|
chunk.SData = storedChunk.SData
|
||||||
|
chunk.Size = storedChunk.Size
|
||||||
|
}
|
||||||
|
close(chunk.C)
|
||||||
|
}(ch)
|
||||||
|
select {
|
||||||
|
case <-self.quitC:
|
||||||
|
break RETRIEVE
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DPA) storeLoop() {
|
||||||
|
self.storeC = make(chan *Chunk)
|
||||||
|
go func() {
|
||||||
|
STORE:
|
||||||
|
for ch := range self.storeC {
|
||||||
|
go func(chunk *Chunk) {
|
||||||
|
self.ChunkStore.Put(chunk)
|
||||||
|
if chunk.wg != nil {
|
||||||
|
dpaLogger.Debugf("DPA.storeLoop %064x", chunk.Key)
|
||||||
|
chunk.wg.Done()
|
||||||
|
}
|
||||||
|
}(ch)
|
||||||
|
select {
|
||||||
|
case <-self.quitC:
|
||||||
|
break STORE
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
135
bzz/dpa_test.go
Normal file
135
bzz/dpa_test.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testDataSize = 0x1000000
|
||||||
|
|
||||||
|
func TestDPArandom(t *testing.T) {
|
||||||
|
os.RemoveAll("/tmp/bzz")
|
||||||
|
dbStore, err := newDbStore("/tmp/bzz")
|
||||||
|
dbStore.setCapacity(50000)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DB error: %v", err)
|
||||||
|
}
|
||||||
|
memStore := newMemStore(dbStore)
|
||||||
|
localStore := &localStore{
|
||||||
|
memStore,
|
||||||
|
dbStore,
|
||||||
|
}
|
||||||
|
chunker := &TreeChunker{}
|
||||||
|
chunker.Init()
|
||||||
|
dpa := &DPA{
|
||||||
|
Chunker: chunker,
|
||||||
|
ChunkStore: localStore,
|
||||||
|
}
|
||||||
|
dpa.Start()
|
||||||
|
reader, slice := testDataReader(testDataSize)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
key, err := dpa.Store(reader, wg)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Store error: %v", err)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
resultReader := dpa.Retrieve(key)
|
||||||
|
resultSlice := make([]byte, len(slice))
|
||||||
|
n, err := resultReader.ReadAt(resultSlice, 0)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("Retrieve error: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(slice) {
|
||||||
|
t.Errorf("Slice size error got %d, expected %d.", n, len(slice))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(slice, resultSlice) {
|
||||||
|
t.Errorf("Comparison error.")
|
||||||
|
}
|
||||||
|
ioutil.WriteFile("/tmp/slice.bzz.16M", slice, 0666)
|
||||||
|
ioutil.WriteFile("/tmp/result.bzz.16M", resultSlice, 0666)
|
||||||
|
localStore.memStore = newMemStore(dbStore)
|
||||||
|
resultReader = dpa.Retrieve(key)
|
||||||
|
for i, _ := range resultSlice {
|
||||||
|
resultSlice[i] = 0
|
||||||
|
}
|
||||||
|
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("Retrieve error after removing memStore: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(slice) {
|
||||||
|
t.Errorf("Slice size error after removing memStore got %d, expected %d.", n, len(slice))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(slice, resultSlice) {
|
||||||
|
t.Errorf("Comparison error after removing memStore.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDPA_capacity(t *testing.T) {
|
||||||
|
os.RemoveAll("/tmp/bzz")
|
||||||
|
dbStore, err := newDbStore("/tmp/bzz")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DB error: %v", err)
|
||||||
|
}
|
||||||
|
memStore := newMemStore(dbStore)
|
||||||
|
localStore := &localStore{
|
||||||
|
memStore,
|
||||||
|
dbStore,
|
||||||
|
}
|
||||||
|
localStore.memStore.setCapacity(0)
|
||||||
|
chunker := &TreeChunker{}
|
||||||
|
chunker.Init()
|
||||||
|
dpa := &DPA{
|
||||||
|
Chunker: chunker,
|
||||||
|
ChunkStore: localStore,
|
||||||
|
}
|
||||||
|
dpa.Start()
|
||||||
|
reader, slice := testDataReader(testDataSize)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
key, err := dpa.Store(reader, wg)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Store error: %v", err)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
resultReader := dpa.Retrieve(key)
|
||||||
|
resultSlice := make([]byte, len(slice))
|
||||||
|
n, err := resultReader.ReadAt(resultSlice, 0)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("Retrieve error: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(slice) {
|
||||||
|
t.Errorf("Slice size error got %d, expected %d.", n, len(slice))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(slice, resultSlice) {
|
||||||
|
t.Errorf("Comparison error.")
|
||||||
|
}
|
||||||
|
// Clear memStore
|
||||||
|
localStore.memStore.setCapacity(0)
|
||||||
|
// check whether it is, indeed, empty
|
||||||
|
dpa.ChunkStore = memStore
|
||||||
|
resultReader = dpa.Retrieve(key)
|
||||||
|
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Was able to read %d bytes from an empty memStore.")
|
||||||
|
}
|
||||||
|
// check how it works with localStore
|
||||||
|
dpa.ChunkStore = localStore
|
||||||
|
// localStore.dbStore.setCapacity(0)
|
||||||
|
resultReader = dpa.Retrieve(key)
|
||||||
|
for i, _ := range resultSlice {
|
||||||
|
resultSlice[i] = 0
|
||||||
|
}
|
||||||
|
n, err = resultReader.ReadAt(resultSlice, 0)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("Retrieve error after clearing memStore: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(slice) {
|
||||||
|
t.Errorf("Slice size error after clearing memStore got %d, expected %d.", n, len(slice))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(slice, resultSlice) {
|
||||||
|
t.Errorf("Comparison error after clearing memStore.")
|
||||||
|
}
|
||||||
|
}
|
||||||
127
bzz/hive.go
Normal file
127
bzz/hive.go
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/kademlia"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peer struct {
|
||||||
|
*bzzProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
// peer not necessary here
|
||||||
|
// bzz protocol could implement kademlia.Node interface with
|
||||||
|
// Addr(), LastActive() and Drop()
|
||||||
|
|
||||||
|
// Hive is the logistic manager of the swarm
|
||||||
|
// it uses a generic kademlia nodetable to find best peer list
|
||||||
|
// for any target
|
||||||
|
// this is used by the netstore to search for content in the swarm
|
||||||
|
// the bzz protocol peersMsgData exchange is relayed to Kademlia
|
||||||
|
// for db storage and filtering
|
||||||
|
// connections and disconnections are reported and relayed
|
||||||
|
// to keep the nodetable uptodate
|
||||||
|
|
||||||
|
type hive struct {
|
||||||
|
addr kademlia.Address
|
||||||
|
kad *kademlia.Kademlia
|
||||||
|
path string
|
||||||
|
ping chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHive(hivepath string) *hive {
|
||||||
|
return &hive{
|
||||||
|
path: hivepath,
|
||||||
|
kad: kademlia.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *hive) start(address kademlia.Address, connectPeer func(string) error) (err error) {
|
||||||
|
self.ping = make(chan bool)
|
||||||
|
self.addr = address
|
||||||
|
self.kad.Start(address)
|
||||||
|
err = self.kad.Load(self.path)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Warnf("Warning: error reading kademlia node db (skipping): %v", err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// whenever pinged ask kademlia about most preferred peer
|
||||||
|
for _ = range self.ping {
|
||||||
|
node, full := self.kad.GetNodeRecord()
|
||||||
|
if node != nil {
|
||||||
|
// if Url known, connect to peer
|
||||||
|
if len(node.Url) > 0 {
|
||||||
|
dpaLogger.Debugf("hive: attempt to connect kaddb node %v", node)
|
||||||
|
connectPeer(node.Url)
|
||||||
|
} else if !full {
|
||||||
|
// a random peer is taken from the table
|
||||||
|
peers := self.kad.GetNodes(kademlia.RandomAddress(), 1)
|
||||||
|
if len(peers) > 0 {
|
||||||
|
// a random address at prox bin 0 is sent for lookup
|
||||||
|
req := &retrieveRequestMsgData{
|
||||||
|
Key: Key(common.Hash(kademlia.RandomAddressAt(self.addr, 0)).Bytes()),
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("hive: look up random address with prox order 0 from peer %v", peers[0])
|
||||||
|
peers[0].(peer).retrieve(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *hive) stop() error {
|
||||||
|
// closing ping channel quits the updateloop
|
||||||
|
close(self.ping)
|
||||||
|
return self.kad.Stop(self.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *hive) addPeer(p peer) {
|
||||||
|
dpaLogger.Debugf("hive: add peer %v", p)
|
||||||
|
self.kad.AddNode(p)
|
||||||
|
// self lookup
|
||||||
|
req := &retrieveRequestMsgData{
|
||||||
|
Key: Key(common.Hash(self.addr).Bytes()),
|
||||||
|
}
|
||||||
|
p.retrieve(req)
|
||||||
|
self.ping <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *hive) removePeer(p peer) {
|
||||||
|
dpaLogger.Debugf("hive: remove peer %v", p)
|
||||||
|
self.kad.RemoveNode(p)
|
||||||
|
self.ping <- false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a list of live peers that are closer to target than us
|
||||||
|
func (self *hive) getPeers(target Key, max int) (peers []peer) {
|
||||||
|
var addr kademlia.Address
|
||||||
|
copy(addr[:], target[:])
|
||||||
|
for _, node := range self.kad.GetNodes(addr, max) {
|
||||||
|
peers = append(peers, node.(peer))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNodeRecord(addr *peerAddr) *kademlia.NodeRecord {
|
||||||
|
return &kademlia.NodeRecord{
|
||||||
|
Address: addr.addr(),
|
||||||
|
Active: 0,
|
||||||
|
Url: addr.url(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by the protocol upon receiving peerset (for target address)
|
||||||
|
// peersMsgData is converted to a slice of NodeRecords for Kademlia
|
||||||
|
// this is to store all thats needed
|
||||||
|
func (self *hive) addPeerEntries(req *peersMsgData) {
|
||||||
|
var nrs []*kademlia.NodeRecord
|
||||||
|
for _, p := range req.Peers {
|
||||||
|
nrs = append(nrs, newNodeRecord(p))
|
||||||
|
}
|
||||||
|
self.kad.AddNodeRecords(nrs)
|
||||||
|
}
|
||||||
197
bzz/httpaccess.go
Normal file
197
bzz/httpaccess.go
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
A simple http server interface to Swarm
|
||||||
|
*/
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rawType = "application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rawUrl = regexp.MustCompile("^/+raw/*")
|
||||||
|
trailingSlashes = regexp.MustCompile("/+$")
|
||||||
|
// forever = time.Unix(0, 0)
|
||||||
|
forever = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
|
type sequentialReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
pos int64
|
||||||
|
ahead map[int64](chan bool)
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// starts up http server
|
||||||
|
// TODO: started by dpa/api rather than backend
|
||||||
|
func startHttpServer(api *Api, port string) {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler(w, r, api)
|
||||||
|
})
|
||||||
|
go http.ListenAndServe(":"+port, nil)
|
||||||
|
dpaLogger.Infof("Swarm HTTP proxy started on localhost:%s", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request, api *Api) {
|
||||||
|
requestURL := r.URL
|
||||||
|
if requestURL.Host == "" {
|
||||||
|
var err error
|
||||||
|
requestURL, err = url.Parse(r.Referer() + requestURL.String())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("Swarm: HTTP request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.RequestURI, requestURL.Host, requestURL.Path, r.Referer(), r.Header.Get("Accept"))
|
||||||
|
uri := requestURL.Path
|
||||||
|
var raw bool
|
||||||
|
path := rawUrl.ReplaceAllStringFunc(uri, func(string) string {
|
||||||
|
raw = true
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.Method == "POST":
|
||||||
|
if raw {
|
||||||
|
dpaLogger.Debugf("Swarm: POST request received.")
|
||||||
|
key, err := api.dpa.Store(io.NewSectionReader(&sequentialReader{
|
||||||
|
reader: r.Body,
|
||||||
|
ahead: make(map[int64]chan bool),
|
||||||
|
}, 0, r.ContentLength), nil)
|
||||||
|
if err == nil {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(common.Bytes2Hex(key))))
|
||||||
|
dpaLogger.Debugf("Swarm: Content for '%064x' stored", key)
|
||||||
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "No POST to "+uri+" allowed.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case r.Method == "GET" || r.Method == "HEAD":
|
||||||
|
path = trailingSlashes.ReplaceAllString(path, "")
|
||||||
|
if raw {
|
||||||
|
dpaLogger.Debugf("Swarm: Raw GET request '%s' received", uri)
|
||||||
|
|
||||||
|
// resolving host
|
||||||
|
key, err := api.Resolve(path)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieving content
|
||||||
|
reader := api.dpa.Retrieve(key)
|
||||||
|
dpaLogger.Debugf("Swarm: Reading %d bytes.", reader.Size())
|
||||||
|
|
||||||
|
// setting mime type
|
||||||
|
qv := requestURL.Query()
|
||||||
|
mimeType := qv.Get("content_type")
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = rawType
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", mimeType)
|
||||||
|
http.ServeContent(w, r, uri, forever, reader)
|
||||||
|
dpaLogger.Debugf("Swarm: Serve raw content '%s' (%d bytes) as '%s'", uri, reader.Size(), mimeType)
|
||||||
|
|
||||||
|
// retrieve path via manifest
|
||||||
|
} else {
|
||||||
|
|
||||||
|
dpaLogger.Debugf("Swarm: Structured GET request '%s' received.", uri)
|
||||||
|
|
||||||
|
// call to api.getPath on uri
|
||||||
|
reader, mimeType, status, err := api.getPath(path)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(errResolve); ok {
|
||||||
|
dpaLogger.Debugf("Swarm: %v", err)
|
||||||
|
status = http.StatusBadRequest
|
||||||
|
} else {
|
||||||
|
dpaLogger.Debugf("Swarm: error retrieving '%s': %v", uri, err)
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set mime type and status headers
|
||||||
|
w.Header().Set("Content-Type", mimeType)
|
||||||
|
if status > 0 {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
} else {
|
||||||
|
status = 200
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("Swarm: Served '%s' (%d bytes) as '%s' (status code: %v)", uri, reader.Size(), mimeType, status)
|
||||||
|
|
||||||
|
http.ServeContent(w, r, path, forever, reader)
|
||||||
|
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *sequentialReader) ReadAt(target []byte, off int64) (n int, err error) {
|
||||||
|
self.lock.Lock()
|
||||||
|
// assert self.pos <= off
|
||||||
|
if self.pos > off {
|
||||||
|
dpaLogger.Errorf("Swarm: non-sequential read attempted from sequentialReader; %d > %d",
|
||||||
|
self.pos, off)
|
||||||
|
panic("Non-sequential read attempt")
|
||||||
|
}
|
||||||
|
if self.pos != off {
|
||||||
|
dpaLogger.Debugf("Swarm: deferred read in POST at position %d, offset %d.",
|
||||||
|
self.pos, off)
|
||||||
|
wait := make(chan bool)
|
||||||
|
self.ahead[off] = wait
|
||||||
|
self.lock.Unlock()
|
||||||
|
if <-wait {
|
||||||
|
// failed read behind
|
||||||
|
n = 0
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.lock.Lock()
|
||||||
|
}
|
||||||
|
localPos := 0
|
||||||
|
for localPos < len(target) {
|
||||||
|
n, err = self.reader.Read(target[localPos:])
|
||||||
|
localPos += n
|
||||||
|
dpaLogger.Debugf("Swarm: Read %d bytes into buffer size %d from POST, error %v.",
|
||||||
|
n, len(target), err)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: POST stream's reading terminated with %v.", err)
|
||||||
|
for i := range self.ahead {
|
||||||
|
self.ahead[i] <- true
|
||||||
|
delete(self.ahead, i)
|
||||||
|
}
|
||||||
|
self.lock.Unlock()
|
||||||
|
return localPos, err
|
||||||
|
}
|
||||||
|
self.pos += int64(n)
|
||||||
|
}
|
||||||
|
wait := self.ahead[self.pos]
|
||||||
|
if wait != nil {
|
||||||
|
dpaLogger.Debugf("Swarm: deferred read in POST at position %d triggered.",
|
||||||
|
self.pos)
|
||||||
|
delete(self.ahead, self.pos)
|
||||||
|
close(wait)
|
||||||
|
}
|
||||||
|
self.lock.Unlock()
|
||||||
|
return localPos, err
|
||||||
|
}
|
||||||
215
bzz/js_api.go
Normal file
215
bzz/js_api.go
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
// "net/http"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/jsre"
|
||||||
|
"github.com/robertkrimen/otto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewJSApi(vm *jsre.JSRE, api *Api) (jsapi *JSApi) {
|
||||||
|
jsapi = &JSApi{
|
||||||
|
vm: vm,
|
||||||
|
api: api,
|
||||||
|
}
|
||||||
|
vm.Set("bzz", struct{}{})
|
||||||
|
t, _ := vm.Get("bzz")
|
||||||
|
o := t.Object()
|
||||||
|
o.Set("register", jsapi.register)
|
||||||
|
o.Set("resolve", jsapi.resolve)
|
||||||
|
o.Set("download", jsapi.download)
|
||||||
|
o.Set("upload", jsapi.upload)
|
||||||
|
o.Set("get", jsapi.get)
|
||||||
|
o.Set("put", jsapi.put)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSApi struct {
|
||||||
|
vm *jsre.JSRE
|
||||||
|
api *Api
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) register(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 3 {
|
||||||
|
fmt.Println("requires 3 arguments: bzz.register(address, contenthash, domain)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var sender, contenthash, domain string
|
||||||
|
sender, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
domain, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
contenthash, err = call.Argument(2).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := common.HexToHash(contenthash)
|
||||||
|
|
||||||
|
err = self.api.Register(common.HexToAddress(sender), domain, hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
return otto.TrueValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) resolve(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
fmt.Println("requires 1 argument: bzz.resolve(domain)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var domain string
|
||||||
|
domain, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var contentHash Key
|
||||||
|
contentHash, err = self.api.Resolve(domain)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(common.ToHex(contentHash[:]))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) get(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
fmt.Println("requires 1 argument: bzz.get(path)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var bzzpath string
|
||||||
|
bzzpath, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var content []byte
|
||||||
|
var mimeType string
|
||||||
|
var status, size int
|
||||||
|
content, mimeType, status, size, err = self.api.Get(bzzpath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := map[string]string{
|
||||||
|
"content": string(content),
|
||||||
|
"contentType": mimeType,
|
||||||
|
"status": fmt.Sprintf("%v", status),
|
||||||
|
"size": fmt.Sprintf("%v", size),
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(obj)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) put(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 2 {
|
||||||
|
fmt.Println("requires 2 arguments: bzz.put(content, content-type)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var res, content, contentType string
|
||||||
|
content, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
contentType, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = self.api.Put(content, contentType)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(res)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) download(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 2 {
|
||||||
|
fmt.Println("requires 2 arguments: bzz.download(bzzpath, localpath)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var bzzpath, localpath string
|
||||||
|
bzzpath, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
localpath, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = self.api.Download(bzzpath, localpath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *JSApi) upload(call otto.FunctionCall) otto.Value {
|
||||||
|
var err error
|
||||||
|
var index string
|
||||||
|
if len(call.ArgumentList) == 2 {
|
||||||
|
index, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
} else if len(call.ArgumentList) != 1 {
|
||||||
|
fmt.Println("requires 1 or 2 arguments: bzz.put(localpath[, index])")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
var localpath, res string
|
||||||
|
localpath, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = self.api.Upload(localpath, index)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(res)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// http.PostForm("http://example.com/form",
|
||||||
|
// url.Values{"key": {"Value"}, "id": {"123"}})
|
||||||
38
bzz/localstore.go
Normal file
38
bzz/localstore.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// localstore.go
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
type localStore struct {
|
||||||
|
memStore *memStore
|
||||||
|
dbStore *dbStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStore is itself a chunk store , to stores a chunk only
|
||||||
|
// its integrity is checked ?
|
||||||
|
func (self *localStore) Put(chunk *Chunk) {
|
||||||
|
chunk.dbStored = make(chan bool)
|
||||||
|
self.memStore.Put(chunk)
|
||||||
|
if chunk.wg != nil {
|
||||||
|
chunk.wg.Add(1)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
self.dbStore.Put(chunk)
|
||||||
|
if chunk.wg != nil {
|
||||||
|
chunk.wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get(chunk *Chunk) looks up a chunk in the local stores
|
||||||
|
// This method is blocking until the chunk is retrieved so additional timeout is needed to wrap this call
|
||||||
|
func (self *localStore) Get(key Key) (chunk *Chunk, err error) {
|
||||||
|
chunk, err = self.memStore.Get(key)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunk, err = self.dbStore.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.memStore.Put(chunk)
|
||||||
|
return
|
||||||
|
}
|
||||||
305
bzz/manifest.go
Normal file
305
bzz/manifest.go
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
manifestType = "application/bzz-manifest+json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type manifestTrie struct {
|
||||||
|
dpa *DPA
|
||||||
|
entries [257]*manifestTrieEntry // indexed by first character of path, entries[256] is the empty path entry
|
||||||
|
hash Key // if hash != nil, it is stored
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestJSON struct {
|
||||||
|
Entries []*manifestTrieEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestTrieEntry struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hash string `json:"hash"` // for manifest content type, empty until subtrie is evaluated
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
subtrie *manifestTrie
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadManifest(dpa *DPA, hash Key) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand
|
||||||
|
|
||||||
|
dpaLogger.Debugf("Swarm: manifest lookup key: '%064x'.", hash)
|
||||||
|
// retrieve manifest via DPA
|
||||||
|
manifestReader := dpa.Retrieve(hash)
|
||||||
|
return readManifest(manifestReader, hash, dpa)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readManifest(manifestReader SectionReader, hash Key, dpa *DPA) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand
|
||||||
|
|
||||||
|
// TODO check size for oversized manifests
|
||||||
|
manifestData := make([]byte, manifestReader.Size())
|
||||||
|
var size int
|
||||||
|
size, err = manifestReader.Read(manifestData)
|
||||||
|
if int64(size) < manifestReader.Size() {
|
||||||
|
dpaLogger.Debugf("Swarm: Manifest %064x not found.", hash)
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("Manifest retrieval cut short: %v < %v", size, manifestReader.Size())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dpaLogger.Debugf("Swarm: Manifest %064x retrieved", hash)
|
||||||
|
man := manifestJSON{}
|
||||||
|
err = json.Unmarshal(manifestData, &man)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Manifest %064x is malformed: %v", hash, err)
|
||||||
|
dpaLogger.Debugf("Swarm: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dpaLogger.Debugf("Swarm: Manifest %064x has %d entries.", hash, len(man.Entries))
|
||||||
|
|
||||||
|
trie = &manifestTrie{
|
||||||
|
dpa: dpa,
|
||||||
|
}
|
||||||
|
for _, entry := range man.Entries {
|
||||||
|
trie.addEntry(entry)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) addEntry(entry *manifestTrieEntry) {
|
||||||
|
self.hash = nil // trie modified, hash needs to be re-calculated on demand
|
||||||
|
|
||||||
|
if len(entry.Path) == 0 {
|
||||||
|
self.entries[256] = entry
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := byte(entry.Path[0])
|
||||||
|
if (self.entries[b] == nil) || (self.entries[b].Path == entry.Path) {
|
||||||
|
self.entries[b] = entry
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldentry := self.entries[b]
|
||||||
|
cpl := 0
|
||||||
|
for (len(entry.Path) > cpl) && (len(oldentry.Path) > cpl) && (entry.Path[cpl] == oldentry.Path[cpl]) {
|
||||||
|
cpl++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldentry.ContentType == manifestType) && (cpl == len(oldentry.Path)) {
|
||||||
|
if self.loadSubTrie(oldentry) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry.Path = entry.Path[cpl:]
|
||||||
|
oldentry.subtrie.addEntry(entry)
|
||||||
|
oldentry.Hash = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commonPrefix := entry.Path[:cpl]
|
||||||
|
|
||||||
|
subtrie := &manifestTrie{
|
||||||
|
dpa: self.dpa,
|
||||||
|
}
|
||||||
|
entry.Path = entry.Path[cpl:]
|
||||||
|
oldentry.Path = oldentry.Path[cpl:]
|
||||||
|
subtrie.addEntry(entry)
|
||||||
|
subtrie.addEntry(oldentry)
|
||||||
|
|
||||||
|
self.entries[b] = &manifestTrieEntry{
|
||||||
|
Path: commonPrefix,
|
||||||
|
Hash: "",
|
||||||
|
ContentType: manifestType,
|
||||||
|
subtrie: subtrie,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) getCountLast() (cnt int, entry *manifestTrieEntry) {
|
||||||
|
for _, e := range self.entries {
|
||||||
|
if e != nil {
|
||||||
|
cnt++
|
||||||
|
entry = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) deleteEntry(path string) {
|
||||||
|
self.hash = nil // trie modified, hash needs to be re-calculated on demand
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
self.entries[256] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := byte(path[0])
|
||||||
|
entry := self.entries[b]
|
||||||
|
if (entry != nil) && (entry.Path == path) {
|
||||||
|
self.entries[b] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
epl := len(entry.Path)
|
||||||
|
if (entry.ContentType == manifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) {
|
||||||
|
if self.loadSubTrie(entry) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry.subtrie.deleteEntry(path[epl:])
|
||||||
|
entry.Hash = ""
|
||||||
|
// remove subtree if it has less than 2 elements
|
||||||
|
cnt, lastentry := entry.subtrie.getCountLast()
|
||||||
|
if cnt < 2 {
|
||||||
|
if lastentry != nil {
|
||||||
|
lastentry.Path = entry.Path + lastentry.Path
|
||||||
|
}
|
||||||
|
self.entries[b] = lastentry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) recalcAndStore() error {
|
||||||
|
if self.hash != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteString(`{"entries":[`)
|
||||||
|
|
||||||
|
list := &manifestJSON{}
|
||||||
|
for _, entry := range self.entries {
|
||||||
|
if entry != nil {
|
||||||
|
if entry.Hash == "" { // TODO: paralellize
|
||||||
|
err := entry.subtrie.recalcAndStore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entry.Hash = fmt.Sprintf("%064x", entry.subtrie.hash)
|
||||||
|
}
|
||||||
|
list.Entries = append(list.Entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := json.Marshal(list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sr := io.NewSectionReader(bytes.NewReader(manifest), 0, int64(len(manifest)))
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
key, err2 := self.dpa.Store(sr, wg)
|
||||||
|
wg.Wait()
|
||||||
|
self.hash = key
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) loadSubTrie(entry *manifestTrieEntry) (err error) {
|
||||||
|
if entry.subtrie == nil {
|
||||||
|
hash := common.Hex2Bytes(entry.Hash)
|
||||||
|
entry.subtrie, err = loadManifest(self.dpa, hash)
|
||||||
|
entry.Hash = "" // might not match, should be recalculated
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) listWithPrefixInt(prefix, rp string, cb func(entry *manifestTrieEntry, suffix string)) {
|
||||||
|
plen := len(prefix)
|
||||||
|
var start, stop int
|
||||||
|
if plen == 0 {
|
||||||
|
start = 0
|
||||||
|
stop = 256
|
||||||
|
} else {
|
||||||
|
start = int(prefix[0])
|
||||||
|
stop = start
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := start; i <= stop; i++ {
|
||||||
|
entry := self.entries[i]
|
||||||
|
if entry != nil {
|
||||||
|
epl := len(entry.Path)
|
||||||
|
if entry.ContentType == manifestType {
|
||||||
|
l := plen
|
||||||
|
if epl < l {
|
||||||
|
l = epl
|
||||||
|
}
|
||||||
|
if (prefix[:l] == entry.Path[:l]) && (self.loadSubTrie(entry) == nil) { // TODO: handle errors
|
||||||
|
entry.subtrie.listWithPrefixInt(prefix[l:], rp+entry.Path[l:], cb)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (epl >= plen) && (prefix == entry.Path[:plen]) {
|
||||||
|
cb(entry, rp+entry.Path[plen:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) listWithPrefix(prefix string, cb func(entry *manifestTrieEntry, suffix string)) {
|
||||||
|
self.listWithPrefixInt(prefix, "", cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) findPrefixOf(path string) (entry *manifestTrieEntry, pos int) {
|
||||||
|
|
||||||
|
dpaLogger.Debugf("findPrefixOf(%s)", path)
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
return self.entries[256], 0
|
||||||
|
}
|
||||||
|
|
||||||
|
b := byte(path[0])
|
||||||
|
entry = self.entries[b]
|
||||||
|
if entry == nil {
|
||||||
|
return self.entries[256], 0
|
||||||
|
}
|
||||||
|
epl := len(entry.Path)
|
||||||
|
dpaLogger.Debugf("path = %v entry.Path = %v epl = %v", path, entry.Path, epl)
|
||||||
|
if (len(path) >= epl) && (path[:epl] == entry.Path) {
|
||||||
|
dpaLogger.Debugf("entry.ContentType = %v", entry.ContentType)
|
||||||
|
if entry.ContentType == manifestType {
|
||||||
|
if self.loadSubTrie(entry) != nil {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
entry, pos = entry.subtrie.findPrefixOf(path[epl:])
|
||||||
|
if entry != nil {
|
||||||
|
pos += epl
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pos = epl
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// file system manifest always contains regularized paths
|
||||||
|
// no leading or trailing slashes, only single slashes inside
|
||||||
|
func regularSlashes(path string) (res string) {
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
if (path[i] != '/') || ((i > 0) && (path[i-1] != '/')) {
|
||||||
|
res = res + path[i:i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (len(res) > 0) && (res[len(res)-1] == '/') {
|
||||||
|
res = res[:len(res)-1]
|
||||||
|
}
|
||||||
|
//fmt.Printf("%v -> %v\n", path, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) {
|
||||||
|
path := regularSlashes(spath)
|
||||||
|
var pos int
|
||||||
|
entry, pos = self.findPrefixOf(path)
|
||||||
|
/*if (pos > 0) && (pos < len(path)) && (path[pos] != '/') {
|
||||||
|
return nil, ""
|
||||||
|
}*/
|
||||||
|
return entry, path[:pos]
|
||||||
|
}
|
||||||
58
bzz/manifest_test.go
Normal file
58
bzz/manifest_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func manifest(paths ...string) (manifestReader SectionReader) {
|
||||||
|
var entries []string
|
||||||
|
for _, path := range paths {
|
||||||
|
entry := fmt.Sprintf(`{"path":"%s"}`, path)
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
manifest := fmt.Sprintf(`{"entries":[%s]}`, strings.Join(entries, ","))
|
||||||
|
return io.NewSectionReader(strings.NewReader(manifest), 0, int64(len(manifest)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetEntry(t *testing.T, path, match string, paths ...string) *manifestTrie {
|
||||||
|
trie, err := readManifest(manifest(paths...), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error making manifest: %v", err)
|
||||||
|
}
|
||||||
|
checkEntry(t, path, match, trie)
|
||||||
|
return trie
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEntry(t *testing.T, path, match string, trie *manifestTrie) {
|
||||||
|
entry, fullpath := trie.getEntry(path)
|
||||||
|
if match == "-" && entry != nil {
|
||||||
|
t.Errorf("expected no match for '%s', got '%s'", path, fullpath)
|
||||||
|
} else if entry == nil {
|
||||||
|
if match != "-" {
|
||||||
|
t.Errorf("expected entry '%s' to match '%s', got no match", match, path)
|
||||||
|
}
|
||||||
|
} else if fullpath != match {
|
||||||
|
t.Errorf("incorrect entry retrieved for '%s'. expected path '%v', got '%s'", path, match, fullpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEntry(t *testing.T) {
|
||||||
|
// file system manifest always contains regularized paths
|
||||||
|
testGetEntry(t, "a", "a", "a")
|
||||||
|
testGetEntry(t, "b", "-", "a")
|
||||||
|
testGetEntry(t, "/a//", "a", "a")
|
||||||
|
// fallback
|
||||||
|
testGetEntry(t, "/a", "", "")
|
||||||
|
testGetEntry(t, "/a/b", "a/b", "a/b")
|
||||||
|
// longest/deepest math
|
||||||
|
testGetEntry(t, "a/b", "a/b", "a", "a/b", "a/bb", "a/b/c")
|
||||||
|
testGetEntry(t, "//a//b//", "a/b", "a", "a/b", "a/bb", "a/b/c")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteEntry(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
348
bzz/memstore.go
Normal file
348
bzz/memstore.go
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
// memory storage layer for the package blockhash
|
||||||
|
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
memTreeLW = 2 // log2(subtree count) of the subtrees
|
||||||
|
memTreeFLW = 14 // log2(subtree count) of the root layer
|
||||||
|
dbForceUpdateAccessCnt = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
type memStore struct {
|
||||||
|
memtree *memTree
|
||||||
|
entryCnt, capacity uint // stored entries
|
||||||
|
accessCnt uint64 // access counter; oldest is thrown away when full
|
||||||
|
dbAccessCnt uint64
|
||||||
|
dbStore *dbStore
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
a hash prefix subtree containing subtrees or one storage entry (but never both)
|
||||||
|
|
||||||
|
- access[0] stores the smallest (oldest) access count value in this subtree
|
||||||
|
- if it contains more subtrees and its subtree count is at least 4, access[1:2]
|
||||||
|
stores the smallest access count in the first and second halves of subtrees
|
||||||
|
(so that access[0] = min(access[1], access[2])
|
||||||
|
- likewise, if subtree count is at least 8,
|
||||||
|
access[1] = min(access[3], access[4])
|
||||||
|
access[2] = min(access[5], access[6])
|
||||||
|
(access[] is a binary tree inside the multi-bit leveled hash tree)
|
||||||
|
*/
|
||||||
|
|
||||||
|
func newMemStore(d *dbStore) (m *memStore) {
|
||||||
|
m = &memStore{}
|
||||||
|
m.memtree = newMemTree(memTreeFLW, nil, 0)
|
||||||
|
m.dbStore = d
|
||||||
|
m.setCapacity(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Key) Size() uint {
|
||||||
|
return uint(len(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Key) isEqual(y Key) bool {
|
||||||
|
return bytes.Compare(x, y) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Key) bits(i, j uint) uint {
|
||||||
|
|
||||||
|
ii := i >> 3
|
||||||
|
jj := i & 7
|
||||||
|
if ii >= h.Size() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if jj+j <= 8 {
|
||||||
|
return uint((h[ii] >> jj) & ((1 << j) - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := uint(h[ii] >> jj)
|
||||||
|
jj = 8 - jj
|
||||||
|
j -= jj
|
||||||
|
for j != 0 {
|
||||||
|
ii++
|
||||||
|
if j < 8 {
|
||||||
|
res += uint(h[ii]&((1<<j)-1)) << jj
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res += uint(h[ii]) << jj
|
||||||
|
jj += 8
|
||||||
|
j -= 8
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type memTree struct {
|
||||||
|
subtree []*memTree
|
||||||
|
parent *memTree
|
||||||
|
parentIdx uint
|
||||||
|
|
||||||
|
bits uint // log2(subtree count)
|
||||||
|
width uint // subtree count
|
||||||
|
|
||||||
|
entry *Chunk // if subtrees are present, entry should be nil
|
||||||
|
lastDBaccess uint64
|
||||||
|
access []uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemTree(b uint, parent *memTree, pidx uint) (node *memTree) {
|
||||||
|
|
||||||
|
node = new(memTree)
|
||||||
|
node.bits = b
|
||||||
|
node.width = 1 << uint(b)
|
||||||
|
node.subtree = make([]*memTree, node.width)
|
||||||
|
node.access = make([]uint64, node.width-1)
|
||||||
|
node.parent = parent
|
||||||
|
node.parentIdx = pidx
|
||||||
|
if parent != nil {
|
||||||
|
parent.subtree[pidx] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *memTree) updateAccess(a uint64) {
|
||||||
|
|
||||||
|
aidx := uint(0)
|
||||||
|
var aa uint64
|
||||||
|
oa := node.access[0]
|
||||||
|
for node.access[aidx] == oa {
|
||||||
|
node.access[aidx] = a
|
||||||
|
if aidx > 0 {
|
||||||
|
aa = node.access[((aidx-1)^1)+1]
|
||||||
|
aidx = (aidx - 1) >> 1
|
||||||
|
} else {
|
||||||
|
pidx := node.parentIdx
|
||||||
|
node = node.parent
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nn := node.subtree[pidx^1]
|
||||||
|
if nn != nil {
|
||||||
|
aa = nn.access[0]
|
||||||
|
} else {
|
||||||
|
aa = 0
|
||||||
|
}
|
||||||
|
aidx = (node.width + pidx - 2) >> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aa != 0) && (aa < a) {
|
||||||
|
a = aa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStore) setCapacity(c uint) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
for c < s.entryCnt {
|
||||||
|
s.removeOldest()
|
||||||
|
}
|
||||||
|
s.capacity = c
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStore) getEntryCnt() uint {
|
||||||
|
|
||||||
|
return s.entryCnt
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry (not its copy) is going to be in memStore
|
||||||
|
func (s *memStore) Put(entry *Chunk) {
|
||||||
|
|
||||||
|
if s.capacity == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
if s.entryCnt >= s.capacity {
|
||||||
|
s.removeOldest()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.accessCnt++
|
||||||
|
|
||||||
|
node := s.memtree
|
||||||
|
bitpos := uint(0)
|
||||||
|
for node.entry == nil {
|
||||||
|
l := entry.Key.bits(bitpos, node.bits)
|
||||||
|
st := node.subtree[l]
|
||||||
|
if st == nil {
|
||||||
|
st = newMemTree(memTreeLW, node, l)
|
||||||
|
bitpos += node.bits
|
||||||
|
node = st
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bitpos += node.bits
|
||||||
|
node = st
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.entry != nil {
|
||||||
|
|
||||||
|
if node.entry.Key.isEqual(entry.Key) {
|
||||||
|
node.updateAccess(s.accessCnt)
|
||||||
|
if entry.SData == nil {
|
||||||
|
entry.Size = node.entry.Size
|
||||||
|
entry.SData = node.entry.SData
|
||||||
|
}
|
||||||
|
if entry.req == nil {
|
||||||
|
entry.req = node.entry.req
|
||||||
|
}
|
||||||
|
entry.C = node.entry.C
|
||||||
|
node.entry = entry
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for node.entry != nil {
|
||||||
|
|
||||||
|
l := node.entry.Key.bits(bitpos, node.bits)
|
||||||
|
st := node.subtree[l]
|
||||||
|
if st == nil {
|
||||||
|
st = newMemTree(memTreeLW, node, l)
|
||||||
|
}
|
||||||
|
st.entry = node.entry
|
||||||
|
node.entry = nil
|
||||||
|
st.updateAccess(node.access[0])
|
||||||
|
|
||||||
|
l = entry.Key.bits(bitpos, node.bits)
|
||||||
|
st = node.subtree[l]
|
||||||
|
if st == nil {
|
||||||
|
st = newMemTree(memTreeLW, node, l)
|
||||||
|
}
|
||||||
|
bitpos += node.bits
|
||||||
|
node = st
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.entry = entry
|
||||||
|
node.lastDBaccess = s.dbAccessCnt
|
||||||
|
node.updateAccess(s.accessCnt)
|
||||||
|
s.entryCnt++
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStore) Get(hash Key) (chunk *Chunk, err error) {
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
node := s.memtree
|
||||||
|
bitpos := uint(0)
|
||||||
|
for node.entry == nil {
|
||||||
|
l := hash.bits(bitpos, node.bits)
|
||||||
|
st := node.subtree[l]
|
||||||
|
if st == nil {
|
||||||
|
return nil, notFound
|
||||||
|
}
|
||||||
|
bitpos += node.bits
|
||||||
|
node = st
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.entry.Key.isEqual(hash) {
|
||||||
|
s.accessCnt++
|
||||||
|
node.updateAccess(s.accessCnt)
|
||||||
|
chunk = node.entry
|
||||||
|
if s.dbAccessCnt-node.lastDBaccess > dbForceUpdateAccessCnt {
|
||||||
|
s.dbAccessCnt++
|
||||||
|
node.lastDBaccess = s.dbAccessCnt
|
||||||
|
if s.dbStore != nil {
|
||||||
|
s.dbStore.updateAccessCnt(hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = notFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *memStore) removeOldest() {
|
||||||
|
|
||||||
|
node := s.memtree
|
||||||
|
|
||||||
|
for node.entry == nil {
|
||||||
|
|
||||||
|
aidx := uint(0)
|
||||||
|
av := node.access[aidx]
|
||||||
|
|
||||||
|
for aidx < node.width/2-1 {
|
||||||
|
if av == node.access[aidx*2+1] {
|
||||||
|
node.access[aidx] = node.access[aidx*2+2]
|
||||||
|
aidx = aidx*2 + 1
|
||||||
|
} else if av == node.access[aidx*2+2] {
|
||||||
|
node.access[aidx] = node.access[aidx*2+1]
|
||||||
|
aidx = aidx*2 + 2
|
||||||
|
} else {
|
||||||
|
panic(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pidx := aidx*2 + 2 - node.width
|
||||||
|
if (node.subtree[pidx] != nil) && (av == node.subtree[pidx].access[0]) {
|
||||||
|
if node.subtree[pidx+1] != nil {
|
||||||
|
node.access[aidx] = node.subtree[pidx+1].access[0]
|
||||||
|
} else {
|
||||||
|
node.access[aidx] = 0
|
||||||
|
}
|
||||||
|
} else if (node.subtree[pidx+1] != nil) && (av == node.subtree[pidx+1].access[0]) {
|
||||||
|
if node.subtree[pidx] != nil {
|
||||||
|
node.access[aidx] = node.subtree[pidx].access[0]
|
||||||
|
} else {
|
||||||
|
node.access[aidx] = 0
|
||||||
|
}
|
||||||
|
pidx++
|
||||||
|
} else {
|
||||||
|
panic(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Println(pidx)
|
||||||
|
node = node.subtree[pidx]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.entry.dbStored != nil {
|
||||||
|
<-node.entry.dbStored
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.entry.SData != nil {
|
||||||
|
node.entry = nil
|
||||||
|
s.entryCnt--
|
||||||
|
}
|
||||||
|
|
||||||
|
node.access[0] = 0
|
||||||
|
|
||||||
|
//---
|
||||||
|
|
||||||
|
aidx := uint(0)
|
||||||
|
for {
|
||||||
|
aa := node.access[aidx]
|
||||||
|
if aidx > 0 {
|
||||||
|
aidx = (aidx - 1) >> 1
|
||||||
|
} else {
|
||||||
|
pidx := node.parentIdx
|
||||||
|
node = node.parent
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aidx = (node.width + pidx - 2) >> 1
|
||||||
|
}
|
||||||
|
if (aa != 0) && ((aa < node.access[aidx]) || (node.access[aidx] == 0)) {
|
||||||
|
node.access[aidx] = aa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
bzz/memstore_test.go
Normal file
35
bzz/memstore_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testMemStore(l int64, branches int64, t *testing.T) {
|
||||||
|
m := newMemStore(nil)
|
||||||
|
testStore(m, l, branches, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemStore128_10000(t *testing.T) {
|
||||||
|
testMemStore(10000, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemStore128_1000(t *testing.T) {
|
||||||
|
testMemStore(1000, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemStore128_100(t *testing.T) {
|
||||||
|
testMemStore(100, 128, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemStore2_100(t *testing.T) {
|
||||||
|
testMemStore(100, 2, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemStoreNotFound(t *testing.T) {
|
||||||
|
m := newMemStore(nil)
|
||||||
|
zeroKey := make([]byte, 32)
|
||||||
|
_, err := m.Get(zeroKey)
|
||||||
|
if err != notFound {
|
||||||
|
t.Errorf("Expected notFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
343
bzz/netstore.go
Normal file
343
bzz/netstore.go
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/kademlia"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
netStore is a network storage for chunks (a dht = distributed hash table of sorts)
|
||||||
|
it is the entrypoint for chunk store/retrieval requests
|
||||||
|
both local (coming from DPA api) and network (coming from peers via bzz protocol)
|
||||||
|
it implements the ChunkStore interface and embeds local storage
|
||||||
|
*/
|
||||||
|
type netStore struct {
|
||||||
|
localStore *localStore
|
||||||
|
lock sync.Mutex
|
||||||
|
hive *hive
|
||||||
|
self *discover.Node
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
request status values:
|
||||||
|
- started searching
|
||||||
|
- found
|
||||||
|
*/
|
||||||
|
|
||||||
|
const (
|
||||||
|
reqSearching = iota // after search for chunk started until found or timed out
|
||||||
|
reqFound // chunk found search terminated
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
requesterCount = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
searchTimeout = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestStatus struct {
|
||||||
|
key Key
|
||||||
|
status int
|
||||||
|
requesters map[int64][]*retrieveRequestMsgData
|
||||||
|
C chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNetStore(path, hivepath string) (netstore *netStore, err error) {
|
||||||
|
dbStore, err := newDbStore(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hive := newHive(hivepath)
|
||||||
|
netstore = &netStore{
|
||||||
|
localStore: &localStore{
|
||||||
|
memStore: newMemStore(dbStore),
|
||||||
|
dbStore: dbStore,
|
||||||
|
},
|
||||||
|
path: path,
|
||||||
|
hive: hive,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *netStore) start(node *discover.Node, connectPeer func(string) error) (err error) {
|
||||||
|
self.self = node
|
||||||
|
err = self.hive.start(kademlia.Address(node.Sha()), connectPeer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *netStore) stop() (err error) {
|
||||||
|
return self.hive.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// called from dpa, entrypoint for *local* chunk store requests
|
||||||
|
func (self *netStore) Put(entry *Chunk) {
|
||||||
|
chunk, err := self.localStore.Get(entry.Key)
|
||||||
|
dpaLogger.Debugf("netStore.Pszut: localStore.Get returned with %v.", err)
|
||||||
|
if err != nil {
|
||||||
|
chunk = entry
|
||||||
|
} else if chunk.SData == nil {
|
||||||
|
chunk.SData = entry.SData
|
||||||
|
chunk.Size = entry.Size
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.put(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store logic common to local and network chunk store requests
|
||||||
|
func (self *netStore) put(entry *Chunk) {
|
||||||
|
self.localStore.Put(entry)
|
||||||
|
dpaLogger.Debugf("netStore.put: localStore.Put of %064x completed, %d bytes (%p).", entry.Key, len(entry.SData), entry)
|
||||||
|
if entry.req != nil {
|
||||||
|
if entry.req.status == reqSearching {
|
||||||
|
entry.req.status = reqFound
|
||||||
|
close(entry.req.C)
|
||||||
|
self.propagateResponse(entry)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
go self.store(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store propagates store requests to specific peers given by the kademlia hive
|
||||||
|
// except for peers that the store request came from (if any)
|
||||||
|
func (self *netStore) store(chunk *Chunk) {
|
||||||
|
for _, peer := range self.hive.getPeers(chunk.Key, 0) {
|
||||||
|
if chunk.source == nil || peer.Addr() != chunk.source.Addr() {
|
||||||
|
peer.storeRequest(chunk.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the entrypoint for network store requests
|
||||||
|
func (self *netStore) addStoreRequest(req *storeRequestMsgData) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
dpaLogger.Debugf("netStore.addStoreRequest: req = %v", req)
|
||||||
|
chunk, err := self.localStore.Get(req.Key)
|
||||||
|
dpaLogger.Debugf("netStore.addStoreRequest: chunk reference %p", chunk)
|
||||||
|
// we assume that a returned chunk is the one stored in the memory cache
|
||||||
|
if err != nil {
|
||||||
|
chunk = &Chunk{
|
||||||
|
Key: req.Key,
|
||||||
|
SData: req.SData,
|
||||||
|
Size: int64(binary.LittleEndian.Uint64(req.SData[0:8])),
|
||||||
|
}
|
||||||
|
} else if chunk.SData == nil {
|
||||||
|
chunk.SData = req.SData
|
||||||
|
chunk.Size = int64(binary.LittleEndian.Uint64(req.SData[0:8]))
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunk.source = req.peer
|
||||||
|
self.put(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get is the entrypoint for local retrieve requests
|
||||||
|
// waits for response or times out
|
||||||
|
func (self *netStore) Get(key Key) (chunk *Chunk, err error) {
|
||||||
|
chunk = self.get(key)
|
||||||
|
id := generateId()
|
||||||
|
timeout := time.Now().Add(searchTimeout)
|
||||||
|
if chunk.SData == nil {
|
||||||
|
self.startSearch(chunk, id, &timeout)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: use self.timer time.Timer and reset with defer disableTimer
|
||||||
|
timer := time.After(searchTimeout)
|
||||||
|
select {
|
||||||
|
case <-timer:
|
||||||
|
dpaLogger.Debugf("netStore.Get: %064x request time out ", key)
|
||||||
|
err = notFound
|
||||||
|
case <-chunk.req.C:
|
||||||
|
dpaLogger.Debugf("netStore.Get: %064x retrieved, %d bytes (%p)", key, len(chunk.SData), chunk)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve logic common for local and network chunk retrieval
|
||||||
|
func (self *netStore) get(key Key) (chunk *Chunk) {
|
||||||
|
var err error
|
||||||
|
chunk, err = self.localStore.Get(key)
|
||||||
|
dpaLogger.Debugf("netStore.get: localStore.Get of %064x returned with %v.", key, err)
|
||||||
|
// we assume that a returned chunk is the one stored in the memory cache
|
||||||
|
if err != nil {
|
||||||
|
// no data and no request status
|
||||||
|
chunk = &Chunk{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
self.localStore.memStore.Put(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunk.req == nil {
|
||||||
|
chunk.req = newRequestStatus()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestStatus() *requestStatus {
|
||||||
|
return &requestStatus{
|
||||||
|
requesters: make(map[int64][]*retrieveRequestMsgData),
|
||||||
|
C: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// entrypoint for network retrieve requests
|
||||||
|
func (self *netStore) addRetrieveRequest(req *retrieveRequestMsgData) {
|
||||||
|
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
|
||||||
|
chunk := self.get(req.Key)
|
||||||
|
if chunk.SData == nil {
|
||||||
|
t := time.Now().Add(10 * time.Second)
|
||||||
|
req.timeout = &t
|
||||||
|
} else {
|
||||||
|
chunk.req.status = reqFound
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := self.strategyUpdateRequest(chunk.req, req) // may change req status
|
||||||
|
|
||||||
|
if timeout == nil {
|
||||||
|
dpaLogger.Debugf("netStore.addRetrieveRequest: %064x - content found, delivering...", req.Key)
|
||||||
|
self.deliver(req, chunk)
|
||||||
|
} else {
|
||||||
|
// we might need chunk.req to cache relevant peers response, or would it expire?
|
||||||
|
self.peers(req, chunk, timeout)
|
||||||
|
dpaLogger.Debugf("netStore.addRetrieveRequest: %064x - searching.... responding with peers...", req.Key)
|
||||||
|
self.startSearch(chunk, int64(req.Id), timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logic propagating retrieve requests to peers given by the kademlia hive
|
||||||
|
// it's assumed that caller holds the lock
|
||||||
|
func (self *netStore) startSearch(chunk *Chunk, id int64, timeout *time.Time) {
|
||||||
|
chunk.req.status = reqSearching
|
||||||
|
peers := self.hive.getPeers(chunk.Key, 0)
|
||||||
|
dpaLogger.Debugf("netStore.startSearch: %064x - received %d peers from KΛÐΞMLIΛ...", chunk.Key, len(peers))
|
||||||
|
req := &retrieveRequestMsgData{
|
||||||
|
Key: chunk.Key,
|
||||||
|
Id: uint64(id),
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
for _, peer := range peers {
|
||||||
|
dpaLogger.Debugf("netStore.startSearch: sending retrieveRequests to peer [%064x]", req.Key)
|
||||||
|
dpaLogger.Debugf("req.requesters: %v", chunk.req.requesters)
|
||||||
|
var requester bool
|
||||||
|
OUT:
|
||||||
|
for _, recipients := range chunk.req.requesters {
|
||||||
|
for _, recipient := range recipients {
|
||||||
|
if recipient.peer.Addr() == peer.Addr() {
|
||||||
|
requester = true
|
||||||
|
break OUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !requester {
|
||||||
|
peer.retrieve(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateId() int64 {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
return r.Int63()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
adds a new peer to an existing open request
|
||||||
|
only add if less than requesterCount peers forwarded the same request id so far
|
||||||
|
note this is done irrespective of status (searching or found)
|
||||||
|
*/
|
||||||
|
func (self *netStore) addRequester(rs *requestStatus, req *retrieveRequestMsgData) {
|
||||||
|
dpaLogger.Debugf("netStore.addRequester: key %064x - add peer [%v] to req.Id %064x", req.Key, req.peer, req.Id)
|
||||||
|
list := rs.requesters[int64(req.Id)]
|
||||||
|
rs.requesters[int64(req.Id)] = append(list, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add peer request the chunk and decides the timeout for the response if still searching
|
||||||
|
func (self *netStore) strategyUpdateRequest(rs *requestStatus, req *retrieveRequestMsgData) (timeout *time.Time) {
|
||||||
|
dpaLogger.Debugf("netStore.strategyUpdateRequest: key %064x", req.Key)
|
||||||
|
self.addRequester(rs, req)
|
||||||
|
if rs.status == reqSearching {
|
||||||
|
timeout = self.searchTimeout(rs, req)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// once a chunk is found propagate it its requesters unless timed out
|
||||||
|
func (self *netStore) propagateResponse(chunk *Chunk) {
|
||||||
|
dpaLogger.Debugf("netStore.propagateResponse: key %064x", chunk.Key)
|
||||||
|
for id, requesters := range chunk.req.requesters {
|
||||||
|
counter := requesterCount
|
||||||
|
dpaLogger.Debugf("netStore.propagateResponse id %064x", id)
|
||||||
|
msg := &storeRequestMsgData{
|
||||||
|
Key: chunk.Key,
|
||||||
|
SData: chunk.SData,
|
||||||
|
Id: uint64(id),
|
||||||
|
}
|
||||||
|
for _, req := range requesters {
|
||||||
|
if req.timeout.After(time.Now()) {
|
||||||
|
dpaLogger.Debugf("netStore.propagateResponse store -> %064x with %v", req.Id, req.peer)
|
||||||
|
go req.peer.store(msg)
|
||||||
|
counter--
|
||||||
|
if counter <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// called on each request when a chunk is found,
|
||||||
|
// delivery is done by sending a request to the requesting peer
|
||||||
|
func (self *netStore) deliver(req *retrieveRequestMsgData, chunk *Chunk) {
|
||||||
|
storeReq := &storeRequestMsgData{
|
||||||
|
Key: req.Key,
|
||||||
|
Id: req.Id,
|
||||||
|
SData: chunk.SData,
|
||||||
|
requestTimeout: req.timeout, //
|
||||||
|
// StorageTimeout *time.Time // expiry of content
|
||||||
|
// Metadata metaData
|
||||||
|
}
|
||||||
|
req.peer.store(storeReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the immediate response to a retrieve request,
|
||||||
|
// sends relevant peer data given by the kademlia hive to the requester
|
||||||
|
func (self *netStore) peers(req *retrieveRequestMsgData, chunk *Chunk, timeout *time.Time) {
|
||||||
|
var addrs []*peerAddr
|
||||||
|
for _, peer := range self.hive.getPeers(req.Key, int(req.MaxPeers)) {
|
||||||
|
addrs = append(addrs, peer.peerAddr())
|
||||||
|
}
|
||||||
|
peersData := &peersMsgData{
|
||||||
|
Peers: addrs,
|
||||||
|
Key: req.Key,
|
||||||
|
Id: req.Id,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
req.peer.peers(peersData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decides the timeout promise sent with the immediate peers response to a retrieve request
|
||||||
|
func (self *netStore) searchTimeout(rs *requestStatus, req *retrieveRequestMsgData) (timeout *time.Time) {
|
||||||
|
t := time.Now().Add(searchTimeout)
|
||||||
|
if req.timeout != nil && req.timeout.Before(t) {
|
||||||
|
return req.timeout
|
||||||
|
} else {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
}
|
||||||
528
bzz/protocol.go
Normal file
528
bzz/protocol.go
Normal file
|
|
@ -0,0 +1,528 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
/*
|
||||||
|
BZZ implements the bzz wire protocol of swarm
|
||||||
|
routing decoded storage and retrieval requests
|
||||||
|
registering peers with the KAD DHT via hive
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/kademlia"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/errs"
|
||||||
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version = 0
|
||||||
|
ProtocolLength = uint64(8)
|
||||||
|
ProtocolMaxMsgSize = 10 * 1024 * 1024
|
||||||
|
NetworkId = 0
|
||||||
|
strategy = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// bzz protocol message codes
|
||||||
|
const (
|
||||||
|
statusMsg = iota // 0x01
|
||||||
|
storeRequestMsg // 0x02
|
||||||
|
retrieveRequestMsg // 0x03
|
||||||
|
peersMsg // 0x04
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrMsgTooLarge = iota
|
||||||
|
ErrDecode
|
||||||
|
ErrInvalidMsgCode
|
||||||
|
ErrVersionMismatch
|
||||||
|
ErrNetworkIdMismatch
|
||||||
|
ErrNoStatusMsg
|
||||||
|
ErrExtraStatusMsg
|
||||||
|
)
|
||||||
|
|
||||||
|
var errorToString = map[int]string{
|
||||||
|
ErrMsgTooLarge: "Message too long",
|
||||||
|
ErrDecode: "Invalid message",
|
||||||
|
ErrInvalidMsgCode: "Invalid message code",
|
||||||
|
ErrVersionMismatch: "Protocol version mismatch",
|
||||||
|
ErrNetworkIdMismatch: "NetworkId mismatch",
|
||||||
|
ErrNoStatusMsg: "No status message",
|
||||||
|
ErrExtraStatusMsg: "Extra status message",
|
||||||
|
}
|
||||||
|
|
||||||
|
// bzzProtocol represents the swarm wire protocol
|
||||||
|
// instance is running on each peer
|
||||||
|
type bzzProtocol struct {
|
||||||
|
node *discover.Node
|
||||||
|
netStore *netStore
|
||||||
|
peer *p2p.Peer
|
||||||
|
key Key
|
||||||
|
rw p2p.MsgReadWriter
|
||||||
|
errors *errs.Errors
|
||||||
|
requestDb *LDBDatabase
|
||||||
|
quitC chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
message structs used for rlp decoding
|
||||||
|
Handshake
|
||||||
|
|
||||||
|
[0x01, Version: B_32, strategy: B_32, capacity: B_64, peers: B_8]
|
||||||
|
|
||||||
|
Storing
|
||||||
|
|
||||||
|
[+0x02, key: B_256, metadata: [], data: B_4k]: the data chunk to be stored, preceded by its key.
|
||||||
|
|
||||||
|
Retrieving
|
||||||
|
|
||||||
|
[0x03, key: B_256, timeout: B_64, metadata: []]: key of the data chunk to be retrieved, timeout in milliseconds. Note that zero timeout retrievals serve also as messages to retrieve peers.
|
||||||
|
|
||||||
|
Peers
|
||||||
|
|
||||||
|
[0x04, key: B_256, timeout: B_64, peers: [[peer], [peer], .... ]] the encoding of a peer is identical to that in the devp2p base protocol peers messages: [IP, Port, NodeID] note that a node's DPA address is not the NodeID but the hash of the NodeID. Timeout serves to indicate whether the responder is forwarding the query within the timeout or not.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
type statusMsgData struct {
|
||||||
|
Version uint64
|
||||||
|
ID string
|
||||||
|
NodeID []byte
|
||||||
|
Addr *peerAddr
|
||||||
|
NetworkId uint64
|
||||||
|
Caps []p2p.Cap
|
||||||
|
// Strategy uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *statusMsgData) String() string {
|
||||||
|
return fmt.Sprintf("Status: Version: %v, ID: %v, NodeID: %v, Addr: %v, NetworkId: %v, Caps: %v", self.Version, self.ID, self.NodeID, self.Addr, self.NetworkId, self.Caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given the chunker I see absolutely no reason why not allow storage and delivery of larger data . See my discussion on flexible chunking.
|
||||||
|
store requests are forwarded to the peers in their cademlia proximity bin if they are distant
|
||||||
|
if they are within our storage radius or have any incentive to store it then attach your nodeID to the metadata
|
||||||
|
if the storage request is sufficiently close (within our proximity range (the last row of the routing table), then sending it to all peers will not guarantee convergence, so there needs to be an absolute expiry of the request too. Maybe the protocol should specify a forward probability exponentially declining with age.
|
||||||
|
*/
|
||||||
|
type storeRequestMsgData struct {
|
||||||
|
Key Key // hash of datasize | data
|
||||||
|
SData []byte // is this needed?
|
||||||
|
// optional
|
||||||
|
Id uint64 //
|
||||||
|
requestTimeout *time.Time // expiry for forwarding
|
||||||
|
storageTimeout *time.Time // expiry of content
|
||||||
|
Metadata metaData //
|
||||||
|
//
|
||||||
|
peer *peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self storeRequestMsgData) String() string {
|
||||||
|
var from string
|
||||||
|
if self.peer == nil {
|
||||||
|
from = "self"
|
||||||
|
} else {
|
||||||
|
from = self.peer.Addr().String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("From: %v, Key: %x; ID: %v, requestTimeout: %v, storageTimeout: %v, SData %x", from, self.Key[:4], self.Id, self.requestTimeout, self.storageTimeout, self.SData[:10])
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Root key retrieve request
|
||||||
|
Timeout in milliseconds. Note that zero timeout retrieval requests do not request forwarding, but prompt for a peers message response. therefore they also serve also as messages to retrieve peers.
|
||||||
|
MaxSize specifies the maximum size that the peer will accept. This is useful in particular if we allow storage and delivery of multichunk payload representing the entire or partial subtree unfolding from the requested root key. So when only interested in limited part of a stream (infinite trees) or only testing chunk availability etc etc, we can indicate it by limiting the size here.
|
||||||
|
In the special case that the key is identical to the peers own address (hash of NodeID) the message is to be handled as a self lookup. The response is a PeersMsg with the peers in the cademlia proximity bin corresponding to the address.
|
||||||
|
It is unclear if a retrieval request with an empty target is the same as a self lookup
|
||||||
|
*/
|
||||||
|
type retrieveRequestMsgData struct {
|
||||||
|
Key Key
|
||||||
|
// optional
|
||||||
|
Id uint64 // request id
|
||||||
|
MaxSize uint64 // maximum size of delivery accepted
|
||||||
|
MaxPeers uint64 // maximum number of peers returned
|
||||||
|
timeout *time.Time //
|
||||||
|
//Metadata metaData //
|
||||||
|
//
|
||||||
|
peer *peer // protocol registers the requester
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self retrieveRequestMsgData) String() string {
|
||||||
|
var from string
|
||||||
|
if self.peer == nil {
|
||||||
|
from = "self"
|
||||||
|
} else {
|
||||||
|
from = self.peer.Addr().String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("From: %v, Key: %x; ID: %v, MaxSize: %v, MaxPeers: %v", from, self.Key[:4], self.Id, self.MaxSize, self.MaxPeers)
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerAddr struct {
|
||||||
|
IP net.IP
|
||||||
|
Port uint16
|
||||||
|
ID []byte
|
||||||
|
n *discover.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerAddr) node() *discover.Node {
|
||||||
|
if self.n == nil {
|
||||||
|
var nodeid discover.NodeID
|
||||||
|
copy(nodeid[:], self.ID)
|
||||||
|
self.n = discover.NewNode(nodeid, self.IP, self.Port, self.Port)
|
||||||
|
}
|
||||||
|
return self.n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerAddr) addr() kademlia.Address {
|
||||||
|
return kademlia.Address(self.node().Sha())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerAddr) url() string {
|
||||||
|
return self.node().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
one response to retrieval, always encouraged after a retrieval request to respond with a list of peers in the same cademlia proximity bin.
|
||||||
|
The encoding of a peer is identical to that in the devp2p base protocol peers messages: [IP, Port, NodeID]
|
||||||
|
note that a node's DPA address is not the NodeID but the hash of the NodeID.
|
||||||
|
Timeout serves to indicate whether the responder is forwarding the query within the timeout or not.
|
||||||
|
The Key is the target (if response to a retrieval request) or peers address (hash of NodeID) if retrieval request was a self lookup.
|
||||||
|
It is unclear if PeersMsg with an empty Key has a special meaning or just mean the same as with the peers address as Key (cademlia bin)
|
||||||
|
*/
|
||||||
|
type peersMsgData struct {
|
||||||
|
Peers []*peerAddr //
|
||||||
|
timeout *time.Time // indicate whether responder is expected to deliver content
|
||||||
|
Key Key // if a response to a retrieval request
|
||||||
|
Id uint64 // if a response to a retrieval request
|
||||||
|
//
|
||||||
|
peer *peer
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
metadata is as yet a placeholder
|
||||||
|
it will likely contain info about hops or the entire forward chain of node IDs
|
||||||
|
this may allow some interesting schemes to evolve optimal routing strategies
|
||||||
|
metadata for storage and retrieval requests could specify format parameters relevant for the (blockhashing) chunking scheme used (for chunks corresponding to a treenode). For instance all runtime params for the chunker (hashing algorithm used, branching etc.)
|
||||||
|
Finally metadata can hold info relevant to some reward or compensation scheme that may be used to incentivise peers.
|
||||||
|
*/
|
||||||
|
type metaData struct{}
|
||||||
|
|
||||||
|
/*
|
||||||
|
main entrypoint, wrappers starting a server running the bzz protocol
|
||||||
|
use this constructor to attach the protocol ("class") to server caps
|
||||||
|
the Dev p2p layer then runs the protocol instance on each peer
|
||||||
|
*/
|
||||||
|
func BzzProtocol(netstore *netStore) (p2p.Protocol, error) {
|
||||||
|
|
||||||
|
db, err := NewLDBDatabase(path.Join(netstore.path, "requests"))
|
||||||
|
if err != nil {
|
||||||
|
return p2p.Protocol{}, err
|
||||||
|
}
|
||||||
|
return p2p.Protocol{
|
||||||
|
Name: "bzz",
|
||||||
|
Version: Version,
|
||||||
|
Length: ProtocolLength,
|
||||||
|
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||||
|
return runBzzProtocol(db, netstore, p, rw)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// the main loop that handles incoming messages
|
||||||
|
// note RemovePeer in the post-disconnect hook
|
||||||
|
func runBzzProtocol(db *LDBDatabase, netstore *netStore, p *p2p.Peer, rw p2p.MsgReadWriter) (err error) {
|
||||||
|
|
||||||
|
self := &bzzProtocol{
|
||||||
|
netStore: netstore,
|
||||||
|
rw: rw,
|
||||||
|
peer: p,
|
||||||
|
errors: &errs.Errors{
|
||||||
|
Package: "BZZ",
|
||||||
|
Errors: errorToString,
|
||||||
|
},
|
||||||
|
requestDb: db,
|
||||||
|
quitC: make(chan bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
go self.storeRequestLoop()
|
||||||
|
|
||||||
|
err = self.handleStatus()
|
||||||
|
if err == nil {
|
||||||
|
for {
|
||||||
|
err = self.handle()
|
||||||
|
if err != nil {
|
||||||
|
self.netStore.hive.removePeer(peer{bzzProtocol: self})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(self.quitC)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) handle() error {
|
||||||
|
msg, err := self.rw.ReadMsg()
|
||||||
|
dpaLogger.Debugf("Incoming MSG: %v", msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if msg.Size > ProtocolMaxMsgSize {
|
||||||
|
return self.protoError(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize)
|
||||||
|
}
|
||||||
|
// make sure that the payload has been fully consumed
|
||||||
|
defer msg.Discard()
|
||||||
|
/*
|
||||||
|
statusMsg = iota // 0x01
|
||||||
|
storeRequestMsg // 0x02
|
||||||
|
retrieveRequestMsg // 0x03
|
||||||
|
peersMsg // 0x04
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch msg.Code {
|
||||||
|
case statusMsg:
|
||||||
|
dpaLogger.Debugf("Status message: %v", msg)
|
||||||
|
return self.protoError(ErrExtraStatusMsg, "")
|
||||||
|
|
||||||
|
case storeRequestMsg:
|
||||||
|
var req storeRequestMsgData
|
||||||
|
if err := msg.Decode(&req); err != nil {
|
||||||
|
return self.protoError(ErrDecode, "msg %v: %v", msg, err)
|
||||||
|
}
|
||||||
|
req.peer = &peer{bzzProtocol: self}
|
||||||
|
self.netStore.addStoreRequest(&req)
|
||||||
|
|
||||||
|
case retrieveRequestMsg:
|
||||||
|
var req retrieveRequestMsgData
|
||||||
|
if err := msg.Decode(&req); err != nil {
|
||||||
|
return self.protoError(ErrDecode, "->msg %v: %v", msg, err)
|
||||||
|
}
|
||||||
|
dpaLogger.Debugf("Receiving retrieve request: %v", req)
|
||||||
|
if req.Key == nil {
|
||||||
|
return self.protoError(ErrDecode, "protocol handler: req.Key == nil || req.Timeout == nil")
|
||||||
|
}
|
||||||
|
req.peer = &peer{bzzProtocol: self}
|
||||||
|
self.netStore.addRetrieveRequest(&req)
|
||||||
|
|
||||||
|
case peersMsg:
|
||||||
|
var req peersMsgData
|
||||||
|
if err := msg.Decode(&req); err != nil {
|
||||||
|
return self.protoError(ErrDecode, "->msg %v: %v", msg, err)
|
||||||
|
}
|
||||||
|
req.peer = &peer{bzzProtocol: self}
|
||||||
|
self.netStore.hive.addPeerEntries(&req)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return self.protoError(ErrInvalidMsgCode, "%v", msg.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) handleStatus() (err error) {
|
||||||
|
// send precanned status message
|
||||||
|
sliceNodeID := self.netStore.self.ID
|
||||||
|
handshake := &statusMsgData{
|
||||||
|
Version: uint64(Version),
|
||||||
|
ID: "honey",
|
||||||
|
NodeID: sliceNodeID[:],
|
||||||
|
Addr: &peerAddr{
|
||||||
|
ID: sliceNodeID[:],
|
||||||
|
IP: self.netStore.self.IP,
|
||||||
|
Port: self.netStore.self.TCP,
|
||||||
|
},
|
||||||
|
NetworkId: uint64(NetworkId),
|
||||||
|
Caps: []p2p.Cap{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p2p.Send(self.rw, statusMsg, handshake); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read and handle remote status
|
||||||
|
var msg p2p.Msg
|
||||||
|
msg, err = self.rw.ReadMsg()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Code != statusMsg {
|
||||||
|
return self.protoError(ErrNoStatusMsg, "first msg has code %x (!= %x)", msg.Code, statusMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Size > ProtocolMaxMsgSize {
|
||||||
|
return self.protoError(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status statusMsgData
|
||||||
|
if err := msg.Decode(&status); err != nil {
|
||||||
|
return self.protoError(ErrDecode, "msg %v: %v", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.NetworkId != NetworkId {
|
||||||
|
return self.protoError(ErrNetworkIdMismatch, "%d (!= %d)", status.NetworkId, NetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Version != status.Version {
|
||||||
|
return self.protoError(ErrVersionMismatch, "%d (!= %d)", status.Version, Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.V(logger.Info).Infof("Peer is [bzz] capable (%d/%d)\n", status.Version, status.NetworkId)
|
||||||
|
|
||||||
|
self.node = status.Addr.node()
|
||||||
|
|
||||||
|
self.netStore.hive.addPeer(peer{bzzProtocol: self})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// protocol instance implements kademlia.Node interface (embedded hive.peer)
|
||||||
|
func (self *bzzProtocol) Addr() (a kademlia.Address) {
|
||||||
|
return kademlia.Address(self.node.Sha())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) Url() string {
|
||||||
|
return self.node.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) LastActive() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) Drop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) String() string {
|
||||||
|
return fmt.Sprintf("%4x: %v\n", self.node.Sha().Bytes()[:4], self.Url())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) peerAddr() *peerAddr {
|
||||||
|
p := self.peer
|
||||||
|
id := p.ID()
|
||||||
|
host, port, _ := net.SplitHostPort(p.RemoteAddr().String())
|
||||||
|
intport, _ := strconv.Atoi(port)
|
||||||
|
return &peerAddr{
|
||||||
|
ID: id[:],
|
||||||
|
IP: net.ParseIP(host),
|
||||||
|
Port: uint16(intport),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// outgoing messages
|
||||||
|
func (self *bzzProtocol) retrieve(req *retrieveRequestMsgData) {
|
||||||
|
dpaLogger.Debugf("Sending retrieve request: %v", req)
|
||||||
|
err := p2p.Send(self.rw, retrieveRequestMsg, req)
|
||||||
|
if err != nil {
|
||||||
|
dpaLogger.Errorf("EncodeMsg error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) addrKey() []byte {
|
||||||
|
id := self.peer.ID()
|
||||||
|
if self.key == nil {
|
||||||
|
self.key = Key(crypto.Sha3(id[:]))
|
||||||
|
}
|
||||||
|
return self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) storeRequestLoop() {
|
||||||
|
|
||||||
|
start := make([]byte, 64)
|
||||||
|
copy(start, self.addrKey())
|
||||||
|
|
||||||
|
key := make([]byte, 64)
|
||||||
|
copy(key, start)
|
||||||
|
var n int
|
||||||
|
var it iterator.Iterator
|
||||||
|
LOOP:
|
||||||
|
for {
|
||||||
|
if n == 0 {
|
||||||
|
it = self.requestDb.NewIterator()
|
||||||
|
// dpaLogger.Debugf("seek iterator: %x", key)
|
||||||
|
it.Seek(key)
|
||||||
|
if !it.Valid() {
|
||||||
|
// dpaLogger.Debugf("not valid, sleep, continue: %x", key)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = it.Key()
|
||||||
|
// dpaLogger.Debugf("found db key: %x", key)
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
// dpaLogger.Debugf("checking key: %x <> %x ", key, self.key())
|
||||||
|
|
||||||
|
// reached the end of this peers range
|
||||||
|
if !bytes.Equal(key[:32], self.addrKey()) {
|
||||||
|
// dpaLogger.Debugf("reached the end of this peers range: %x", key)
|
||||||
|
n = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk, err := self.netStore.localStore.dbStore.Get(key[32:])
|
||||||
|
if err != nil {
|
||||||
|
self.requestDb.Delete(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// dpaLogger.Debugf("sending chunk: %x", chunk.Key)
|
||||||
|
|
||||||
|
id := generateId()
|
||||||
|
req := &storeRequestMsgData{
|
||||||
|
Key: chunk.Key,
|
||||||
|
SData: chunk.SData,
|
||||||
|
Id: uint64(id),
|
||||||
|
}
|
||||||
|
self.store(req)
|
||||||
|
|
||||||
|
n--
|
||||||
|
self.requestDb.Delete(key)
|
||||||
|
it.Next()
|
||||||
|
key = it.Key()
|
||||||
|
if len(key) == 0 {
|
||||||
|
key = start
|
||||||
|
if n == 0 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-self.quitC:
|
||||||
|
break LOOP
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) store(req *storeRequestMsgData) {
|
||||||
|
p2p.Send(self.rw, storeRequestMsg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) storeRequest(key Key) {
|
||||||
|
peerKey := make([]byte, 64)
|
||||||
|
copy(peerKey, self.addrKey())
|
||||||
|
copy(peerKey[32:], key[:])
|
||||||
|
dpaLogger.Debugf("enter store request %x into db", peerKey)
|
||||||
|
self.requestDb.Put(peerKey, []byte{0})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) peers(req *peersMsgData) {
|
||||||
|
p2p.Send(self.rw, peersMsg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) protoError(code int, format string, params ...interface{}) (err *errs.Error) {
|
||||||
|
err = self.errors.New(code, format, params...)
|
||||||
|
err.Log(glog.V(logger.Info))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *bzzProtocol) protoErrorDisconnect(err *errs.Error) {
|
||||||
|
err.Log(glog.V(logger.Info))
|
||||||
|
if err.Fatal() {
|
||||||
|
self.peer.Disconnect(p2p.DiscSubprotocolError)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
bzz/protocol_test.go
Normal file
51
bzz/protocol_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peerId struct {
|
||||||
|
pubkey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerId) String() string {
|
||||||
|
return fmt.Sprintf("test peer %x", self.Pubkey()[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerId) Pubkey() (pubkey []byte) {
|
||||||
|
pubkey = self.pubkey
|
||||||
|
if len(pubkey) == 0 {
|
||||||
|
pubkey = crypto.GenerateNewKeyPair().PublicKey
|
||||||
|
self.pubkey = pubkey
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestPeer() (peer *p2p.Peer) {
|
||||||
|
// peer = NewPeer(&peerId{}, []p2p.Cap{})
|
||||||
|
// peer.pubkeyHook = func(*peerAddr) error { return nil }
|
||||||
|
// peer.ourID = &peerId{}
|
||||||
|
// peer.listenAddr = &peerAddr{}
|
||||||
|
// peer.otherPeers = func() []*Peer { return nil }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectMsg(r p2p.MsgReader, code uint64) error {
|
||||||
|
msg, err := r.ReadMsg()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := msg.Discard(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if msg.Code != code {
|
||||||
|
return fmt.Errorf("wrong message code: got %d, expected %d", msg.Code, code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test(t *testing.T) {}
|
||||||
19
bzz/roundtripper.go
Normal file
19
bzz/roundtripper.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
// "github.com/ethereum/go-ethereum/common/docserver"
|
||||||
|
// "github.com/ethereum/go-ethereum/jsre"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoundTripper struct {
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *RoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
|
url := fmt.Sprintf("http://localhost:%s/%s/%s", self.Port, req.URL.Host, req.URL.Path)
|
||||||
|
dpaLogger.Infof("roundtripper: proxying request '%s' to '%s'", req.RequestURI, url)
|
||||||
|
return http.Get(url)
|
||||||
|
}
|
||||||
51
bzz/roundtripper_test.go
Normal file
51
bzz/roundtripper_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package bzz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/docserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const port = "8500"
|
||||||
|
|
||||||
|
func TestRoundTripper(t *testing.T) {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
http.ServeContent(w, r, "", time.Unix(0, 0), strings.NewReader(r.RequestURI))
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go http.ListenAndServe(":"+port, nil)
|
||||||
|
|
||||||
|
rt := &RoundTripper{port}
|
||||||
|
ds := docserver.New("/")
|
||||||
|
ds.RegisterProtocol("bzz", rt)
|
||||||
|
|
||||||
|
resp, err := ds.Client().Get("bzz://test.com/path")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(content) != "/test.com/path" {
|
||||||
|
t.Errorf("incorrect response from http server: expected '%v', got '%v'", "/test.com/path", string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/compiler"
|
"github.com/ethereum/go-ethereum/common/compiler"
|
||||||
"github.com/ethereum/go-ethereum/common/natspec"
|
"github.com/ethereum/go-ethereum/common/natspec"
|
||||||
"github.com/ethereum/go-ethereum/common/resolver"
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
|
@ -27,10 +27,7 @@ import (
|
||||||
"gopkg.in/fatih/set.v0"
|
"gopkg.in/fatih/set.v0"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
// node admin bindings
|
||||||
node admin bindings
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (js *jsre) adminBindings() {
|
func (js *jsre) adminBindings() {
|
||||||
ethO, _ := js.re.Get("eth")
|
ethO, _ := js.re.Get("eth")
|
||||||
eth := ethO.Object()
|
eth := ethO.Object()
|
||||||
|
|
@ -53,6 +50,9 @@ func (js *jsre) adminBindings() {
|
||||||
admin.Set("verbosity", js.verbosity)
|
admin.Set("verbosity", js.verbosity)
|
||||||
admin.Set("progress", js.downloadProgress)
|
admin.Set("progress", js.downloadProgress)
|
||||||
admin.Set("setSolc", js.setSolc)
|
admin.Set("setSolc", js.setSolc)
|
||||||
|
admin.Set("setGlobalRegistrar", js.setGlobalRegistrar)
|
||||||
|
admin.Set("setHashReg", js.setHashReg)
|
||||||
|
admin.Set("setUrlHint", js.setUrlHint)
|
||||||
|
|
||||||
admin.Set("contractInfo", struct{}{})
|
admin.Set("contractInfo", struct{}{})
|
||||||
t, _ = admin.Get("contractInfo")
|
t, _ = admin.Get("contractInfo")
|
||||||
|
|
@ -60,8 +60,8 @@ func (js *jsre) adminBindings() {
|
||||||
// newRegistry officially not documented temporary option
|
// newRegistry officially not documented temporary option
|
||||||
cinfo.Set("start", js.startNatSpec)
|
cinfo.Set("start", js.startNatSpec)
|
||||||
cinfo.Set("stop", js.stopNatSpec)
|
cinfo.Set("stop", js.stopNatSpec)
|
||||||
cinfo.Set("newRegistry", js.newRegistry)
|
|
||||||
cinfo.Set("get", js.getContractInfo)
|
cinfo.Set("get", js.getContractInfo)
|
||||||
|
cinfo.Set("saveInfo", js.saveInfo)
|
||||||
cinfo.Set("register", js.register)
|
cinfo.Set("register", js.register)
|
||||||
cinfo.Set("registerUrl", js.registerUrl)
|
cinfo.Set("registerUrl", js.registerUrl)
|
||||||
// cinfo.Set("verify", js.verify)
|
// cinfo.Set("verify", js.verify)
|
||||||
|
|
@ -98,6 +98,12 @@ func (js *jsre) adminBindings() {
|
||||||
debug.Set("insertBlock", js.insertBlockRlp)
|
debug.Set("insertBlock", js.insertBlockRlp)
|
||||||
// undocumented temporary
|
// undocumented temporary
|
||||||
debug.Set("waitForBlocks", js.waitForBlocks)
|
debug.Set("waitForBlocks", js.waitForBlocks)
|
||||||
|
|
||||||
|
js.re.Set("http", struct{}{})
|
||||||
|
t, _ = js.re.Get("http")
|
||||||
|
http := t.Object()
|
||||||
|
http.Set("get", js.httpGet)
|
||||||
|
http.Set("loadScript", js.httpLoadScript)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generic helper to getBlock by Number/Height or Hex depending on autodetected input
|
// generic helper to getBlock by Number/Height or Hex depending on autodetected input
|
||||||
|
|
@ -202,6 +208,112 @@ func (js *jsre) pendingTransactions(call otto.FunctionCall) otto.Value {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (js *jsre) setGlobalRegistrar(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) == 0 {
|
||||||
|
fmt.Println("requires 1 or 2 arguments: admin.setGlobalRegistrar(sender[, contractaddress])")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var namereg, addr string
|
||||||
|
var sender common.Address
|
||||||
|
var err error
|
||||||
|
if len(call.ArgumentList) > 0 {
|
||||||
|
namereg, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
addr, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
sender = common.HexToAddress(addr)
|
||||||
|
}
|
||||||
|
reg := registrar.New(js.xeth)
|
||||||
|
err = reg.SetGlobalRegistrar(namereg, sender)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(registrar.GlobalRegistrarAddr)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *jsre) setHashReg(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) > 2 {
|
||||||
|
fmt.Println("requires 0, 1 or 2 arguments: admin.setHashReg(sender[, contractaddress])")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var hashreg, addr string
|
||||||
|
var sender common.Address
|
||||||
|
var err error
|
||||||
|
if len(call.ArgumentList) > 0 {
|
||||||
|
hashreg, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
addr, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
sender = common.HexToAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := registrar.New(js.xeth)
|
||||||
|
sender = common.HexToAddress(addr)
|
||||||
|
err = reg.SetHashReg(hashreg, sender)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(registrar.HashRegAddr)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *jsre) setUrlHint(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) > 2 {
|
||||||
|
fmt.Println("requires 0, 1 or 2 arguments: admin.setHashReg(sender[, contractaddress])")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var urlhint, addr string
|
||||||
|
var sender common.Address
|
||||||
|
var err error
|
||||||
|
if len(call.ArgumentList) > 0 {
|
||||||
|
urlhint, err = call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
addr, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
sender = common.HexToAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := registrar.New(js.xeth)
|
||||||
|
sender = common.HexToAddress(addr)
|
||||||
|
err = reg.SetUrlHint(urlhint, sender)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := call.Otto.ToValue(registrar.UrlHintAddr)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (js *jsre) resend(call otto.FunctionCall) otto.Value {
|
func (js *jsre) resend(call otto.FunctionCall) otto.Value {
|
||||||
if len(call.ArgumentList) == 0 {
|
if len(call.ArgumentList) == 0 {
|
||||||
fmt.Println("first argument must be a transaction")
|
fmt.Println("first argument must be a transaction")
|
||||||
|
|
@ -238,6 +350,56 @@ func (js *jsre) resend(call otto.FunctionCall) otto.Value {
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (js *jsre) httpLoadScript(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 1 {
|
||||||
|
fmt.Println("requires 1 argument: http.httpLoadScript(url)")
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
uri, err := call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
script, err := js.ds.Get(uri, "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
_, err = call.Otto.Run(script)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
return otto.TrueValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *jsre) httpGet(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) > 2 {
|
||||||
|
fmt.Println("requires 1 or 2 arguments: http.get(url[, filepath])")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
uri, err := call.Argument(0).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
var path string
|
||||||
|
if len(call.ArgumentList) > 1 {
|
||||||
|
path, err = call.Argument(1).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := js.ds.Get(uri, path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
v, _ := call.Otto.ToValue(string(resp))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (js *jsre) sign(call otto.FunctionCall) otto.Value {
|
func (js *jsre) sign(call otto.FunctionCall) otto.Value {
|
||||||
if len(call.ArgumentList) != 2 {
|
if len(call.ArgumentList) != 2 {
|
||||||
fmt.Println("requires 2 arguments: eth.sign(signer, data)")
|
fmt.Println("requires 2 arguments: eth.sign(signer, data)")
|
||||||
|
|
@ -688,12 +850,18 @@ func (js *jsre) waitForBlocks(call otto.FunctionCall) otto.Value {
|
||||||
}
|
}
|
||||||
|
|
||||||
if args == 0 {
|
if args == 0 {
|
||||||
height = js.xeth.CurrentBlock().Number()
|
height = new(big.Int).Add(js.xeth.CurrentBlock().Number(), common.Big1)
|
||||||
height.Add(height, common.Big1)
|
|
||||||
}
|
}
|
||||||
|
height, err = waitForBlocks(js.wait, height, timer)
|
||||||
|
if err != nil {
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
v, _ := call.Otto.ToValue(height.Uint64())
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
wait := js.wait
|
func waitForBlocks(wait chan *big.Int, height *big.Int, timer <-chan time.Time) (newHeight *big.Int, err error) {
|
||||||
js.wait <- height
|
wait <- height
|
||||||
select {
|
select {
|
||||||
case <-timer:
|
case <-timer:
|
||||||
// if times out make sure the xeth loop does not block
|
// if times out make sure the xeth loop does not block
|
||||||
|
|
@ -703,11 +871,10 @@ func (js *jsre) waitForBlocks(call otto.FunctionCall) otto.Value {
|
||||||
case <-wait:
|
case <-wait:
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return otto.UndefinedValue()
|
return nil, fmt.Errorf("timeout")
|
||||||
case height = <-wait:
|
case newHeight = <-wait:
|
||||||
}
|
}
|
||||||
v, _ := call.Otto.ToValue(height.Uint64())
|
return
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (js *jsre) sleep(call otto.FunctionCall) otto.Value {
|
func (js *jsre) sleep(call otto.FunctionCall) otto.Value {
|
||||||
|
|
@ -739,23 +906,49 @@ func (js *jsre) setSolc(call otto.FunctionCall) otto.Value {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (js *jsre) register(call otto.FunctionCall) otto.Value {
|
func (js *jsre) register(call otto.FunctionCall) otto.Value {
|
||||||
if len(call.ArgumentList) != 4 {
|
if len(call.ArgumentList) != 3 {
|
||||||
fmt.Println("requires 4 arguments: admin.contractInfo.register(fromaddress, contractaddress, contract, filename)")
|
fmt.Println("requires 3 arguments: admin.contractInfo.register(fromaddress, contractaddress, contenthash)")
|
||||||
return otto.UndefinedValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
sender, err := call.Argument(0).ToString()
|
sender, err := call.Argument(0).ToString()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
address, err := call.Argument(1).ToString()
|
address, err := call.Argument(1).ToString()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, err := call.Argument(2).Export()
|
contentHashHex, err := call.Argument(2).ToString()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sender and contract address are passed as hex strings
|
||||||
|
codeb := js.xeth.CodeAtBytes(address)
|
||||||
|
codeHash := common.BytesToHash(crypto.Sha3(codeb))
|
||||||
|
contentHash := common.HexToHash(contentHashHex)
|
||||||
|
registry := registrar.New(js.xeth)
|
||||||
|
|
||||||
|
_, err = registry.SetHashToHash(common.HexToAddress(sender), codeHash, contentHash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return otto.FalseValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return otto.TrueValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *jsre) saveInfo(call otto.FunctionCall) otto.Value {
|
||||||
|
if len(call.ArgumentList) != 2 {
|
||||||
|
fmt.Println("requires 2 arguments: admin.contractInfo.saveInfo(contract.info, filename)")
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
}
|
||||||
|
raw, err := call.Argument(0).Export()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
|
|
@ -765,36 +958,20 @@ func (js *jsre) register(call otto.FunctionCall) otto.Value {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
}
|
}
|
||||||
var contract compiler.Contract
|
var contractInfo compiler.ContractInfo
|
||||||
err = json.Unmarshal(jsonraw, &contract)
|
err = json.Unmarshal(jsonraw, &contractInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
filename, err := call.Argument(3).ToString()
|
filename, err := call.Argument(1).ToString()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
contenthash, err := compiler.ExtractInfo(&contract, filename)
|
contenthash, err := compiler.SaveInfo(&contractInfo, filename)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return otto.UndefinedValue()
|
|
||||||
}
|
|
||||||
// sender and contract address are passed as hex strings
|
|
||||||
codeb := js.xeth.CodeAtBytes(address)
|
|
||||||
codehash := common.BytesToHash(crypto.Sha3(codeb))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return otto.UndefinedValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := resolver.New(js.xeth)
|
|
||||||
|
|
||||||
_, err = registry.RegisterContentHash(common.HexToAddress(sender), codehash, contenthash)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
|
|
@ -806,7 +983,7 @@ func (js *jsre) register(call otto.FunctionCall) otto.Value {
|
||||||
|
|
||||||
func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value {
|
func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value {
|
||||||
if len(call.ArgumentList) != 3 {
|
if len(call.ArgumentList) != 3 {
|
||||||
fmt.Println("requires 3 arguments: admin.contractInfo.register(fromaddress, contenthash, filename)")
|
fmt.Println("requires 3 arguments: admin.contractInfo.registerUrl(fromaddress, contenthash, url)")
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
sender, err := call.Argument(0).ToString()
|
sender, err := call.Argument(0).ToString()
|
||||||
|
|
@ -827,9 +1004,8 @@ func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value {
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
registry := resolver.New(js.xeth)
|
registry := registrar.New(js.xeth)
|
||||||
|
_, err = registry.SetUrlToHash(common.HexToAddress(sender), common.HexToHash(contenthash), url)
|
||||||
_, err = registry.RegisterUrl(common.HexToAddress(sender), common.HexToHash(contenthash), url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
|
|
@ -840,7 +1016,7 @@ func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value {
|
||||||
|
|
||||||
func (js *jsre) getContractInfo(call otto.FunctionCall) otto.Value {
|
func (js *jsre) getContractInfo(call otto.FunctionCall) otto.Value {
|
||||||
if len(call.ArgumentList) != 1 {
|
if len(call.ArgumentList) != 1 {
|
||||||
fmt.Println("requires 1 argument: admin.contractInfo.register(contractaddress)")
|
fmt.Println("requires 1 argument: admin.contractInfo.get(contractaddress)")
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
addr, err := call.Argument(0).ToString()
|
addr, err := call.Argument(0).ToString()
|
||||||
|
|
@ -849,12 +1025,12 @@ func (js *jsre) getContractInfo(call otto.FunctionCall) otto.Value {
|
||||||
return otto.FalseValue()
|
return otto.FalseValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
infoDoc, err := natspec.FetchDocsForContract(addr, js.xeth, ds)
|
infoDoc, err := natspec.FetchDocsForContract(addr, js.xeth, js.ds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return otto.UndefinedValue()
|
return otto.UndefinedValue()
|
||||||
}
|
}
|
||||||
var info compiler.ContractInfo
|
var info interface{}
|
||||||
err = json.Unmarshal(infoDoc, &info)
|
err = json.Unmarshal(infoDoc, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
@ -874,28 +1050,6 @@ func (js *jsre) stopNatSpec(call otto.FunctionCall) otto.Value {
|
||||||
return otto.TrueValue()
|
return otto.TrueValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (js *jsre) newRegistry(call otto.FunctionCall) otto.Value {
|
|
||||||
|
|
||||||
if len(call.ArgumentList) != 1 {
|
|
||||||
fmt.Println("requires 1 argument: admin.contractInfo.newRegistry(adminaddress)")
|
|
||||||
return otto.FalseValue()
|
|
||||||
}
|
|
||||||
addr, err := call.Argument(0).ToString()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return otto.FalseValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := resolver.New(js.xeth)
|
|
||||||
err = registry.CreateContracts(common.HexToAddress(addr))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return otto.FalseValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
return otto.TrueValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal transaction type which will allow us to resend transactions using `eth.resend`
|
// internal transaction type which will allow us to resend transactions using `eth.resend`
|
||||||
type tx struct {
|
type tx struct {
|
||||||
tx *types.Transaction
|
tx *types.Transaction
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
var (
|
|
||||||
globalRegistrar = `var GlobalRegistrar = web3.eth.contract([{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"name","outputs":[{"name":"o_name","type":"bytes32"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"owner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"content","outputs":[{"name":"","type":"bytes32"}],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"addr","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"reserve","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"subRegistrar","outputs":[{"name":"o_subRegistrar","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_newOwner","type":"address"}],"name":"transfer","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_registrar","type":"address"}],"name":"setSubRegistrar","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"Registrar","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_a","type":"address"},{"name":"_primary","type":"bool"}],"name":"setAddress","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"},{"name":"_content","type":"bytes32"}],"name":"setContent","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"bytes32"}],"name":"disown","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"register","outputs":[{"name":"","type":"address"}],"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"}],"name":"Changed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"addr","type":"address"}],"name":"PrimaryChanged","type":"event"}]);`
|
|
||||||
globalRegistrarAddr = "0xc6d9d2cd449a754c494264e1809c50e34d64562b"
|
|
||||||
)
|
|
||||||
|
|
@ -26,10 +26,13 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/bzz"
|
||||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/docserver"
|
"github.com/ethereum/go-ethereum/common/docserver"
|
||||||
"github.com/ethereum/go-ethereum/common/natspec"
|
"github.com/ethereum/go-ethereum/common/natspec"
|
||||||
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
|
"github.com/ethereum/go-ethereum/common/registrar/ethreg"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
re "github.com/ethereum/go-ethereum/jsre"
|
re "github.com/ethereum/go-ethereum/jsre"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
|
@ -63,6 +66,7 @@ func (r dumbterm) PasswordPrompt(p string) (string, error) {
|
||||||
func (r dumbterm) AppendHistory(string) {}
|
func (r dumbterm) AppendHistory(string) {}
|
||||||
|
|
||||||
type jsre struct {
|
type jsre struct {
|
||||||
|
ds *docserver.DocServer
|
||||||
re *re.JSRE
|
re *re.JSRE
|
||||||
ethereum *eth.Ethereum
|
ethereum *eth.Ethereum
|
||||||
xeth *xeth.XEth
|
xeth *xeth.XEth
|
||||||
|
|
@ -73,19 +77,27 @@ type jsre struct {
|
||||||
prompter
|
prompter
|
||||||
}
|
}
|
||||||
|
|
||||||
func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, interactive bool, f xeth.Frontend) *jsre {
|
func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, bzzEnabled bool, bzzPort string, interactive bool, f xeth.Frontend) *jsre {
|
||||||
js := &jsre{ethereum: ethereum, ps1: "> "}
|
js := &jsre{ethereum: ethereum, ps1: "> "}
|
||||||
// set default cors domain used by startRpc from CLI flag
|
// set default cors domain used by startRpc from CLI flag
|
||||||
js.corsDomain = corsDomain
|
js.corsDomain = corsDomain
|
||||||
if f == nil {
|
if f == nil {
|
||||||
f = js
|
f = js
|
||||||
}
|
}
|
||||||
|
js.ds = docserver.New("/")
|
||||||
js.xeth = xeth.New(ethereum, f)
|
js.xeth = xeth.New(ethereum, f)
|
||||||
js.wait = js.xeth.UpdateState()
|
js.wait = js.xeth.UpdateState()
|
||||||
// update state in separare forever blocks
|
|
||||||
js.re = re.New(libPath)
|
js.re = re.New(libPath)
|
||||||
|
// js.apiBindings(js.xeth)
|
||||||
js.apiBindings(f)
|
js.apiBindings(f)
|
||||||
js.adminBindings()
|
js.adminBindings()
|
||||||
|
if bzzEnabled {
|
||||||
|
// register the swarm rountripper with the bzz scheme on the docserver
|
||||||
|
js.ds.RegisterScheme("bzz", &bzz.RoundTripper{Port: bzzPort})
|
||||||
|
// swarm js bindings
|
||||||
|
bzz.NewJSApi(js.re, js.ethereum.Swarm)
|
||||||
|
js.ethereum.Swarm.Registrar = ethreg.New(js.xeth)
|
||||||
|
}
|
||||||
|
|
||||||
if !liner.TerminalSupported() || !interactive {
|
if !liner.TerminalSupported() || !interactive {
|
||||||
js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
|
js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
|
||||||
|
|
@ -105,6 +117,7 @@ func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, interactive boo
|
||||||
|
|
||||||
func (js *jsre) apiBindings(f xeth.Frontend) {
|
func (js *jsre) apiBindings(f xeth.Frontend) {
|
||||||
xe := xeth.New(js.ethereum, f)
|
xe := xeth.New(js.ethereum, f)
|
||||||
|
// func (js *jsre) apiBindings(xe *xeth.XEth) {
|
||||||
ethApi := rpc.NewEthereumApi(xe)
|
ethApi := rpc.NewEthereumApi(xe)
|
||||||
jeth := rpc.NewJeth(ethApi, js.re)
|
jeth := rpc.NewJeth(ethApi, js.re)
|
||||||
|
|
||||||
|
|
@ -143,14 +156,12 @@ var net = web3.net;
|
||||||
utils.Fatalf("Error setting namespaces: %v", err)
|
utils.Fatalf("Error setting namespaces: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
js.re.Eval(globalRegistrar + "registrar = GlobalRegistrar.at(\"" + globalRegistrarAddr + "\");")
|
js.re.Eval(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `); registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ds, _ = docserver.New("/")
|
|
||||||
|
|
||||||
func (self *jsre) ConfirmTransaction(tx string) bool {
|
func (self *jsre) ConfirmTransaction(tx string) bool {
|
||||||
if self.ethereum.NatSpec {
|
if self.ethereum.NatSpec {
|
||||||
notice := natspec.GetNotice(self.xeth, tx, ds)
|
notice := natspec.GetNotice(self.xeth, tx, self.ds)
|
||||||
fmt.Println(notice)
|
fmt.Println(notice)
|
||||||
answer, _ := self.Prompt("Confirm Transaction [y/n]")
|
answer, _ := self.Prompt("Confirm Transaction [y/n]")
|
||||||
return strings.HasPrefix(strings.Trim(answer, " "), "y")
|
return strings.HasPrefix(strings.Trim(answer, " "), "y")
|
||||||
|
|
|
||||||
67
cmd/geth/js_bzz_test.go
Normal file
67
cmd/geth/js_bzz_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bzzREPL(t *testing.T, port string) (string, *testjethre, *eth.Ethereum) {
|
||||||
|
return testREPL(t, func(c *eth.Config) {
|
||||||
|
c.Bzz = true
|
||||||
|
c.BzzPort = port
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBzzUploadDownload(t *testing.T) {
|
||||||
|
tmp, repl, ethereum := bzzREPL(t, "")
|
||||||
|
if err := ethereum.Start(); err != nil {
|
||||||
|
t.Fatalf("error starting ethereum: %v", err)
|
||||||
|
}
|
||||||
|
defer ethereum.Stop()
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
_ = repl
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBzzPutGet(t *testing.T) {
|
||||||
|
tmp, repl, ethereum := bzzREPL(t, "")
|
||||||
|
if err := ethereum.Start(); err != nil {
|
||||||
|
t.Fatalf("error starting ethereum: %v", err)
|
||||||
|
}
|
||||||
|
defer ethereum.Stop()
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
if checkEvalJSON(t, repl, `hash = bzz.put("console.log(\"hello from console\")", "application/javascript")`, `"97f1b7c7ea12468fd37c262383b9aa862d0cfbc4fc7218652374679fc5cf40cd"`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := `{"content":"console.log(\"hello from console\")","contentType":"application/javascript","size":"33","status":"0"}`
|
||||||
|
if checkEvalJSON(t, repl, `bzz.get(hash)`, want) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the server can be initialized only once per test session !
|
||||||
|
// until we implement a stoppable http server
|
||||||
|
// further http tests will need to make sure the correct server is running
|
||||||
|
func TestHTTP(t *testing.T) {
|
||||||
|
tmp, repl, ethereum := bzzREPL(t, "8500")
|
||||||
|
if err := ethereum.Start(); err != nil {
|
||||||
|
t.Fatalf("error starting ethereum: %v", err)
|
||||||
|
}
|
||||||
|
defer ethereum.Stop()
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
if checkEvalJSON(t, repl, `hash = bzz.put("f42 = function() { return 42 }", "application/javascript")`, `"e6847876f00102441f850b2d438a06d10e3bf24e6a0a76d47b073a86c3c2f9ac"`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkEvalJSON(t, repl, `http.get("bzz://"+hash)`, `"f42 = function() { return 42 }"`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkEvalJSON(t, repl, `http.loadScript("bzz://"+hash)`, `true`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkEvalJSON(t, repl, `f42()`, `42`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,21 +3,22 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/compiler"
|
"github.com/ethereum/go-ethereum/common/compiler"
|
||||||
"github.com/ethereum/go-ethereum/common/docserver"
|
"github.com/ethereum/go-ethereum/common/docserver"
|
||||||
"github.com/ethereum/go-ethereum/common/natspec"
|
"github.com/ethereum/go-ethereum/common/natspec"
|
||||||
"github.com/ethereum/go-ethereum/common/resolver"
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
"github.com/ethereum/go-ethereum/eth"
|
||||||
)
|
)
|
||||||
|
|
@ -41,7 +42,6 @@ var (
|
||||||
|
|
||||||
type testjethre struct {
|
type testjethre struct {
|
||||||
*jsre
|
*jsre
|
||||||
stateDb *state.StateDB
|
|
||||||
lastConfirm string
|
lastConfirm string
|
||||||
ds *docserver.DocServer
|
ds *docserver.DocServer
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +62,10 @@ func (self *testjethre) ConfirmTransaction(tx string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) {
|
func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) {
|
||||||
|
return testREPL(t, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *eth.Ethereum) {
|
||||||
tmp, err := ioutil.TempDir("", "geth-test")
|
tmp, err := ioutil.TempDir("", "geth-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -72,14 +76,19 @@ func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) {
|
||||||
|
|
||||||
ks := crypto.NewKeyStorePlain(filepath.Join(tmp, "keystore"))
|
ks := crypto.NewKeyStorePlain(filepath.Join(tmp, "keystore"))
|
||||||
am := accounts.NewManager(ks)
|
am := accounts.NewManager(ks)
|
||||||
ethereum, err := eth.New(ð.Config{
|
conf := ð.Config{
|
||||||
NodeKey: testNodeKey,
|
NodeKey: testNodeKey,
|
||||||
DataDir: tmp,
|
DataDir: tmp,
|
||||||
AccountManager: am,
|
AccountManager: am,
|
||||||
MaxPeers: 0,
|
MaxPeers: 0,
|
||||||
Name: "test",
|
Name: "test",
|
||||||
SolcPath: testSolcPath,
|
SolcPath: testSolcPath,
|
||||||
})
|
PowTest: true,
|
||||||
|
}
|
||||||
|
if config != nil {
|
||||||
|
config(conf)
|
||||||
|
}
|
||||||
|
ethereum, err := eth.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("%v", err)
|
t.Fatal("%v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -100,24 +109,15 @@ func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assetPath := filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "ethereum", "go-ethereum", "cmd", "mist", "assets", "ext")
|
assetPath := filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "ethereum", "go-ethereum", "cmd", "mist", "assets", "ext")
|
||||||
ds, err := docserver.New("/")
|
ds := docserver.New("/")
|
||||||
if err != nil {
|
tf := &testjethre{ds: ds}
|
||||||
t.Errorf("Error creating DocServer: %v", err)
|
repl := newJSRE(ethereum, assetPath, "", conf.Bzz, conf.BzzPort, false, tf)
|
||||||
}
|
|
||||||
tf := &testjethre{ds: ds, stateDb: ethereum.ChainManager().State().Copy()}
|
|
||||||
repl := newJSRE(ethereum, assetPath, "", false, tf)
|
|
||||||
tf.jsre = repl
|
tf.jsre = repl
|
||||||
return tmp, tf, ethereum
|
return tmp, tf, ethereum
|
||||||
}
|
}
|
||||||
|
|
||||||
// this line below is needed for transaction to be applied to the state in testing
|
|
||||||
// the heavy lifing is done in XEth.ApplyTestTxs
|
|
||||||
// this is fragile, overwriting xeth will result in
|
|
||||||
// process leaking since xeth loops cannot quit safely
|
|
||||||
// should be replaced by proper mining with testDAG for easy full integration tests
|
|
||||||
// txc, self.xeth = self.xeth.ApplyTestTxs(self.xeth.repl.stateDb, coinbase, txc)
|
|
||||||
|
|
||||||
func TestNodeInfo(t *testing.T) {
|
func TestNodeInfo(t *testing.T) {
|
||||||
|
t.Skip("broken after p2p update")
|
||||||
tmp, repl, ethereum := testJEthRE(t)
|
tmp, repl, ethereum := testJEthRE(t)
|
||||||
if err := ethereum.Start(); err != nil {
|
if err := ethereum.Start(); err != nil {
|
||||||
t.Fatalf("error starting ethereum: %v", err)
|
t.Fatalf("error starting ethereum: %v", err)
|
||||||
|
|
@ -259,12 +259,21 @@ func TestContract(t *testing.T) {
|
||||||
defer ethereum.Stop()
|
defer ethereum.Stop()
|
||||||
defer os.RemoveAll(tmp)
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
var txc uint64
|
|
||||||
coinbase := common.HexToAddress(testAddress)
|
coinbase := common.HexToAddress(testAddress)
|
||||||
resolver.New(repl.xeth).CreateContracts(coinbase)
|
reg := registrar.New(repl.xeth)
|
||||||
// time.Sleep(1000 * time.Millisecond)
|
err := reg.SetGlobalRegistrar("", coinbase)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error setting HashReg: %v", err)
|
||||||
|
}
|
||||||
|
err = reg.SetHashReg("", coinbase)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error setting HashReg: %v", err)
|
||||||
|
}
|
||||||
|
err = reg.SetUrlHint("", coinbase)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error setting HashReg: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// checkEvalJSON(t, repl, `eth.getBlock("pending", true).transactions.length`, `2`)
|
|
||||||
source := `contract test {\n` +
|
source := `contract test {\n` +
|
||||||
" /// @notice Will multiply `a` by 7." + `\n` +
|
" /// @notice Will multiply `a` by 7." + `\n` +
|
||||||
` function multiply(uint a) returns(uint d) {\n` +
|
` function multiply(uint a) returns(uint d) {\n` +
|
||||||
|
|
@ -272,14 +281,20 @@ func TestContract(t *testing.T) {
|
||||||
` }\n` +
|
` }\n` +
|
||||||
`}\n`
|
`}\n`
|
||||||
|
|
||||||
checkEvalJSON(t, repl, `admin.contractInfo.stop()`, `true`)
|
if checkEvalJSON(t, repl, `admin.contractInfo.stop()`, `true`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
contractInfo, err := ioutil.ReadFile("info_test.json")
|
contractInfo, err := ioutil.ReadFile("info_test.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
checkEvalJSON(t, repl, `primary = eth.accounts[0]`, `"`+testAddress+`"`)
|
if checkEvalJSON(t, repl, `primary = eth.accounts[0]`, `"`+testAddress+`"`) != nil {
|
||||||
checkEvalJSON(t, repl, `source = "`+source+`"`, `"`+source+`"`)
|
return
|
||||||
|
}
|
||||||
|
if checkEvalJSON(t, repl, `source = "`+source+`"`, `"`+source+`"`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if solc is found with right version, test it, otherwise read from file
|
// if solc is found with right version, test it, otherwise read from file
|
||||||
sol, err := compiler.New("")
|
sol, err := compiler.New("")
|
||||||
|
|
@ -299,71 +314,157 @@ func TestContract(t *testing.T) {
|
||||||
t.Errorf("%v", err)
|
t.Errorf("%v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
checkEvalJSON(t, repl, `contract = eth.compile.solidity(source).test`, string(contractInfo))
|
if checkEvalJSON(t, repl, `contract = eth.compile.solidity(source).test`, string(contractInfo)) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEvalJSON(t, repl, `contract.code`, `"0x605880600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b603d6004803590602001506047565b8060005260206000f35b60006007820290506053565b91905056"`)
|
if checkEvalJSON(t, repl, `contract.code`, `"0x605880600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b603d6004803590602001506047565b8060005260206000f35b60006007820290506053565b91905056"`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checkEvalJSON(
|
if checkEvalJSON(
|
||||||
t, repl,
|
t, repl,
|
||||||
`contractaddress = eth.sendTransaction({from: primary, data: contract.code })`,
|
`contractaddress = eth.sendTransaction({from: primary, data: contract.code})`,
|
||||||
`"0x5dcaace5982778b409c524873b319667eba5d074"`,
|
`"0x46d69d55c3c4b86a924a92c9fc4720bb7bce1d74"`,
|
||||||
)
|
// `"0x291293d57e0a0ab47effe97c02577f90d9211567"`,
|
||||||
|
) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !processTxs(repl, t, 8) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
callSetup := `abiDef = JSON.parse('[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}]');
|
callSetup := `abiDef = JSON.parse('[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}]');
|
||||||
Multiply7 = eth.contract(abiDef);
|
Multiply7 = eth.contract(abiDef);
|
||||||
multiply7 = Multiply7.at(contractaddress);
|
multiply7 = Multiply7.at(contractaddress);
|
||||||
`
|
`
|
||||||
// time.Sleep(1500 * time.Millisecond)
|
|
||||||
_, err = repl.re.Run(callSetup)
|
_, err = repl.re.Run(callSetup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error setting up contract, got %v", err)
|
t.Errorf("unexpected error setting up contract, got %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkEvalJSON(t, repl, `eth.getBlock("pending", true).transactions.length`, `3`)
|
|
||||||
|
|
||||||
// why is this sometimes failing?
|
|
||||||
// checkEvalJSON(t, repl, `multiply7.multiply.call(6)`, `42`)
|
|
||||||
expNotice := ""
|
expNotice := ""
|
||||||
if repl.lastConfirm != expNotice {
|
if repl.lastConfirm != expNotice {
|
||||||
t.Errorf("incorrect confirmation message: expected %v, got %v", expNotice, repl.lastConfirm)
|
t.Errorf("incorrect confirmation message: expected %v, got %v", expNotice, repl.lastConfirm)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
txc, repl.xeth = repl.xeth.ApplyTestTxs(repl.stateDb, coinbase, txc)
|
if checkEvalJSON(t, repl, `admin.contractInfo.start()`, `true`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary })`, `undefined`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checkEvalJSON(t, repl, `admin.contractInfo.start()`, `true`)
|
if !processTxs(repl, t, 1) {
|
||||||
checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary, gas: "1000000", gasPrice: "100000" })`, `undefined`)
|
return
|
||||||
expNotice = `About to submit transaction (no NatSpec info found for contract: content hash not found for '0x87e2802265838c7f14bb69eecd2112911af6767907a702eeaa445239fb20711b'): {"params":[{"to":"0x5dcaace5982778b409c524873b319667eba5d074","data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}]}`
|
}
|
||||||
|
|
||||||
|
expNotice = `About to submit transaction (no NatSpec info found for contract: content hash not found for '0x87e2802265838c7f14bb69eecd2112911af6767907a702eeaa445239fb20711b'): {"params":[{"to":"0x46d69d55c3c4b86a924a92c9fc4720bb7bce1d74","data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}]}`
|
||||||
if repl.lastConfirm != expNotice {
|
if repl.lastConfirm != expNotice {
|
||||||
t.Errorf("incorrect confirmation message: expected %v, got %v", expNotice, repl.lastConfirm)
|
t.Errorf("incorrect confirmation message: expected\n%v, got\n%v", expNotice, repl.lastConfirm)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var contenthash = `"0x86d2b7cf1e72e9a7a3f8d96601f0151742a2f780f1526414304fbe413dc7f9bd"`
|
var contentHash = `"0x86d2b7cf1e72e9a7a3f8d96601f0151742a2f780f1526414304fbe413dc7f9bd"`
|
||||||
if sol != nil {
|
if sol != nil && solcVersion != sol.Version() {
|
||||||
modContractInfo := versionRE.ReplaceAll(contractInfo, []byte(`"compilerVersion":"`+sol.Version()+`"`))
|
modContractInfo := versionRE.ReplaceAll(contractInfo, []byte(`"compilerVersion":"`+sol.Version()+`"`))
|
||||||
_ = modContractInfo
|
fmt.Printf("modified contractinfo:\n%s\n", modContractInfo)
|
||||||
// contenthash = crypto.Sha3(modContractInfo)
|
contentHash = `"` + common.ToHex(crypto.Sha3([]byte(modContractInfo))) + `"`
|
||||||
}
|
}
|
||||||
checkEvalJSON(t, repl, `filename = "/tmp/info.json"`, `"/tmp/info.json"`)
|
if checkEvalJSON(t, repl, `filename = "/tmp/info.json"`, `"/tmp/info.json"`) != nil {
|
||||||
checkEvalJSON(t, repl, `contenthash = admin.contractInfo.register(primary, contractaddress, contract, filename)`, contenthash)
|
return
|
||||||
checkEvalJSON(t, repl, `admin.contractInfo.registerUrl(primary, contenthash, "file://"+filename)`, `true`)
|
}
|
||||||
if err != nil {
|
if checkEvalJSON(t, repl, `contentHash = admin.contractInfo.saveInfo(contract.info, filename)`, contentHash) != nil {
|
||||||
t.Errorf("unexpected error registering, got %v", err)
|
return
|
||||||
|
}
|
||||||
|
if checkEvalJSON(t, repl, `admin.contractInfo.register(primary, contractaddress, contentHash)`, `true`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkEvalJSON(t, repl, `admin.contractInfo.registerUrl(primary, contentHash, "file://"+filename)`, `true`) != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEvalJSON(t, repl, `admin.contractInfo.start()`, `true`)
|
if checkEvalJSON(t, repl, `admin.contractInfo.start()`, `true`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// update state
|
if !processTxs(repl, t, 3) {
|
||||||
txc, repl.xeth = repl.xeth.ApplyTestTxs(repl.stateDb, coinbase, txc)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary })`, `undefined`) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !processTxs(repl, t, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary, gas: "1000000", gasPrice: "100000" })`, `undefined`)
|
|
||||||
expNotice = "Will multiply 6 by 7."
|
expNotice = "Will multiply 6 by 7."
|
||||||
if repl.lastConfirm != expNotice {
|
if repl.lastConfirm != expNotice {
|
||||||
t.Errorf("incorrect confirmation message: expected %v, got %v", expNotice, repl.lastConfirm)
|
t.Errorf("incorrect confirmation message: expected\n%v, got\n%v", expNotice, repl.lastConfirm)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pendingTransactions(repl *testjethre, t *testing.T) (txc int64, err error) {
|
||||||
|
txs := repl.ethereum.TxPool().GetTransactions()
|
||||||
|
return int64(len(txs)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTxs(repl *testjethre, t *testing.T, expTxc int) bool {
|
||||||
|
var txc int64
|
||||||
|
var err error
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
txc, err = pendingTransactions(repl, t)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error checking pending transactions: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if expTxc < int(txc) {
|
||||||
|
t.Errorf("too many pending transactions: expected %v, got %v", expTxc, txc)
|
||||||
|
return false
|
||||||
|
} else if expTxc == int(txc) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if int(txc) != expTxc {
|
||||||
|
t.Errorf("incorrect number of pending transactions, expected %v, got %v", expTxc, txc)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repl.ethereum.StartMining(runtime.NumCPU())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error mining: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer repl.ethereum.StopMining()
|
||||||
|
|
||||||
|
timer := time.NewTimer(100 * time.Second)
|
||||||
|
height := new(big.Int).Add(repl.xeth.CurrentBlock().Number(), big.NewInt(1))
|
||||||
|
_, err = waitForBlocks(repl.wait, height, timer.C)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error mining transactions: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
txc, err = pendingTransactions(repl, t)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error checking pending transactions: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if txc != 0 {
|
||||||
|
t.Errorf("%d trasactions were not mined", txc)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func checkEvalJSON(t *testing.T, re *testjethre, expr, want string) error {
|
func checkEvalJSON(t *testing.T, re *testjethre, expr, want string) error {
|
||||||
val, err := re.re.Run("JSON.stringify(" + expr + ")")
|
val, err := re.re.Run("JSON.stringify(" + expr + ")")
|
||||||
if err == nil && val.String() != want {
|
if err == nil && val.String() != want {
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,8 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
|
||||||
utils.RPCListenAddrFlag,
|
utils.RPCListenAddrFlag,
|
||||||
utils.RPCPortFlag,
|
utils.RPCPortFlag,
|
||||||
utils.WhisperEnabledFlag,
|
utils.WhisperEnabledFlag,
|
||||||
|
utils.SwarmEnabledFlag,
|
||||||
|
utils.SwarmProxyPortFlag,
|
||||||
utils.VMDebugFlag,
|
utils.VMDebugFlag,
|
||||||
utils.ProtocolVersionFlag,
|
utils.ProtocolVersionFlag,
|
||||||
utils.NetworkIdFlag,
|
utils.NetworkIdFlag,
|
||||||
|
|
@ -304,6 +306,8 @@ func console(ctx *cli.Context) {
|
||||||
ethereum,
|
ethereum,
|
||||||
ctx.String(utils.JSpathFlag.Name),
|
ctx.String(utils.JSpathFlag.Name),
|
||||||
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
||||||
|
ctx.GlobalBool(utils.SwarmEnabledFlag.Name),
|
||||||
|
ctx.GlobalString(utils.SwarmProxyPortFlag.Name),
|
||||||
true,
|
true,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
@ -325,6 +329,8 @@ func execJSFiles(ctx *cli.Context) {
|
||||||
ethereum,
|
ethereum,
|
||||||
ctx.String(utils.JSpathFlag.Name),
|
ctx.String(utils.JSpathFlag.Name),
|
||||||
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
ctx.GlobalString(utils.RPCCORSDomainFlag.Name),
|
||||||
|
ctx.GlobalBool(utils.SwarmEnabledFlag.Name),
|
||||||
|
ctx.GlobalString(utils.SwarmProxyPortFlag.Name),
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func (self *DirectoryString) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *DirectoryString) Set(value string) error {
|
func (self *DirectoryString) Set(value string) error {
|
||||||
self.Value = expandPath(value)
|
self.Value = ExpandPath(value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ func (self *DirectoryFlag) Set(value string) {
|
||||||
// 2. expands embedded environment variables
|
// 2. expands embedded environment variables
|
||||||
// 3. cleans the path, e.g. /a/b/../c -> /a/c
|
// 3. cleans the path, e.g. /a/b/../c -> /a/c
|
||||||
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
|
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
|
||||||
func expandPath(p string) string {
|
func ExpandPath(p string) string {
|
||||||
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
|
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
|
||||||
if user, err := user.Current(); err == nil {
|
if user, err := user.Current(); err == nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func TestPathExpansion(t *testing.T) {
|
||||||
os.Setenv("DDDXXX", "/tmp")
|
os.Setenv("DDDXXX", "/tmp")
|
||||||
|
|
||||||
for test, expected := range tests {
|
for test, expected := range tests {
|
||||||
got := expandPath(test)
|
got := ExpandPath(test)
|
||||||
if got != expected {
|
if got != expected {
|
||||||
t.Errorf("test %s, got %s, expected %s\n", test, got, expected)
|
t.Errorf("test %s, got %s, expected %s\n", test, got, expected)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,15 @@ var (
|
||||||
Name: "shh",
|
Name: "shh",
|
||||||
Usage: "Enable whisper",
|
Usage: "Enable whisper",
|
||||||
}
|
}
|
||||||
|
SwarmEnabledFlag = cli.BoolFlag{
|
||||||
|
Name: "bzz",
|
||||||
|
Usage: "Enable swarm",
|
||||||
|
}
|
||||||
|
SwarmProxyPortFlag = cli.StringFlag{
|
||||||
|
Name: "bzzport",
|
||||||
|
Usage: "Swarm HTTP Proxy Port on localhost",
|
||||||
|
Value: "8500",
|
||||||
|
}
|
||||||
// ATM the url is left to the user and deployment to
|
// ATM the url is left to the user and deployment to
|
||||||
JSpathFlag = cli.StringFlag{
|
JSpathFlag = cli.StringFlag{
|
||||||
Name: "jspath",
|
Name: "jspath",
|
||||||
|
|
@ -312,6 +321,8 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config {
|
||||||
Discovery: !ctx.GlobalBool(NoDiscoverFlag.Name),
|
Discovery: !ctx.GlobalBool(NoDiscoverFlag.Name),
|
||||||
NodeKey: MakeNodeKey(ctx),
|
NodeKey: MakeNodeKey(ctx),
|
||||||
Shh: ctx.GlobalBool(WhisperEnabledFlag.Name),
|
Shh: ctx.GlobalBool(WhisperEnabledFlag.Name),
|
||||||
|
Bzz: ctx.GlobalBool(SwarmEnabledFlag.Name),
|
||||||
|
BzzPort: ctx.GlobalString(SwarmProxyPortFlag.Name),
|
||||||
Dial: true,
|
Dial: true,
|
||||||
BootNodes: ctx.GlobalString(BootnodesFlag.Name),
|
BootNodes: ctx.GlobalString(BootnodesFlag.Name),
|
||||||
GasPrice: common.String2Big(ctx.GlobalString(GasPriceFlag.Name)),
|
GasPrice: common.String2Big(ctx.GlobalString(GasPriceFlag.Name)),
|
||||||
|
|
|
||||||
|
|
@ -185,12 +185,12 @@ func (sol *Solidity) Compile(source string) (contracts map[string]*Contract, err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractInfo(contract *Contract, filename string) (contenthash common.Hash, err error) {
|
func SaveInfo(info *ContractInfo, filename string) (contenthash common.Hash, err error) {
|
||||||
contractInfo, err := json.Marshal(contract.Info)
|
infojson, err := json.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contenthash = common.BytesToHash(crypto.Sha3(contractInfo))
|
contenthash = common.BytesToHash(crypto.Sha3(infojson))
|
||||||
err = ioutil.WriteFile(filename, contractInfo, 0600)
|
err = ioutil.WriteFile(filename, infojson, 0600)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,19 +72,15 @@ func TestNoCompiler(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractInfo(t *testing.T) {
|
func TestSaveInfo(t *testing.T) {
|
||||||
var cinfo ContractInfo
|
var cinfo ContractInfo
|
||||||
err := json.Unmarshal([]byte(info), &cinfo)
|
err := json.Unmarshal([]byte(info), &cinfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%v", err)
|
t.Errorf("%v", err)
|
||||||
}
|
}
|
||||||
contract := &Contract{
|
|
||||||
Code: "",
|
|
||||||
Info: cinfo,
|
|
||||||
}
|
|
||||||
filename := "/tmp/solctest.info.json"
|
filename := "/tmp/solctest.info.json"
|
||||||
os.Remove(filename)
|
os.Remove(filename)
|
||||||
cinfohash, err := ExtractInfo(contract, filename)
|
cinfohash, err := SaveInfo(&cinfo, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error extracting info: %v", err)
|
t.Errorf("error extracting info: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// http://golang.org/pkg/net/http/#RoundTripper
|
|
||||||
var (
|
|
||||||
schemes = map[string]func(*DocServer) http.RoundTripper{
|
|
||||||
// Simple File server from local disk file:///etc/passwd :)
|
|
||||||
"file": fileServerOnDocRoot,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func fileServerOnDocRoot(ds *DocServer) http.RoundTripper {
|
|
||||||
return http.NewFileTransport(http.Dir(ds.DocRoot))
|
|
||||||
}
|
|
||||||
|
|
||||||
type DocServer struct {
|
type DocServer struct {
|
||||||
*http.Transport
|
*http.Transport
|
||||||
DocRoot string
|
DocRoot string
|
||||||
|
schemes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(docRoot string) (self *DocServer, err error) {
|
func New(docRoot string) (self *DocServer) {
|
||||||
self = &DocServer{
|
self = &DocServer{
|
||||||
Transport: &http.Transport{},
|
Transport: &http.Transport{},
|
||||||
DocRoot: docRoot,
|
DocRoot: docRoot,
|
||||||
|
schemes: []string{"file"},
|
||||||
}
|
}
|
||||||
err = self.RegisterProtocols(schemes)
|
self.RegisterProtocol("file", http.NewFileTransport(http.Dir(self.DocRoot)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,25 +36,24 @@ func (self *DocServer) Client() *http.Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *DocServer) RegisterProtocols(schemes map[string]func(*DocServer) http.RoundTripper) (err error) {
|
func (self *DocServer) RegisterScheme(scheme string, rt http.RoundTripper) {
|
||||||
for scheme, rtf := range schemes {
|
self.schemes = append(self.schemes, scheme)
|
||||||
self.RegisterProtocol(scheme, rtf(self))
|
self.RegisterProtocol(scheme, rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DocServer) HasScheme(scheme string) bool {
|
||||||
|
for _, s := range self.schemes {
|
||||||
|
if s == scheme {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *DocServer) GetAuthContent(uri string, hash common.Hash) (content []byte, err error) {
|
func (self *DocServer) GetAuthContent(uri string, hash common.Hash) (content []byte, err error) {
|
||||||
// retrieve content
|
// retrieve content
|
||||||
resp, err := self.Client().Get(uri)
|
|
||||||
defer func() {
|
content, err = self.Get(uri, "")
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
content, err = ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -80,3 +70,31 @@ func (self *DocServer) GetAuthContent(uri string, hash common.Hash) (content []b
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get(uri, path) downloads the document at uri, if path is non-empty it
|
||||||
|
// is interpreted as a filepath to which the contents are saved
|
||||||
|
func (self *DocServer) Get(uri, path string) (content []byte, err error) {
|
||||||
|
// retrieve content
|
||||||
|
resp, err := self.Client().Get(uri)
|
||||||
|
defer func() {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if path != "" {
|
||||||
|
var abspath string
|
||||||
|
abspath, err = filepath.Abs(path)
|
||||||
|
ioutil.WriteFile(abspath, content, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package docserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ func TestGetAuthContent(t *testing.T) {
|
||||||
copy(hash[:], crypto.Sha3([]byte(text)))
|
copy(hash[:], crypto.Sha3([]byte(text)))
|
||||||
ioutil.WriteFile("/tmp/test.content", []byte(text), os.ModePerm)
|
ioutil.WriteFile("/tmp/test.content", []byte(text), os.ModePerm)
|
||||||
|
|
||||||
ds, err := New("/tmp/")
|
ds := New("/tmp/")
|
||||||
content, err := ds.GetAuthContent("file:///test.content", hash)
|
content, err := ds.GetAuthContent("file:///test.content", hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("no error expected, got %v", err)
|
t.Errorf("no error expected, got %v", err)
|
||||||
|
|
@ -36,3 +37,18 @@ func TestGetAuthContent(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rt struct{}
|
||||||
|
|
||||||
|
func (rt) RoundTrip(req *http.Request) (resp *http.Response, err error) { return }
|
||||||
|
|
||||||
|
func TestRegisterScheme(t *testing.T) {
|
||||||
|
ds := New("/tmp/")
|
||||||
|
if ds.HasScheme("scheme") {
|
||||||
|
t.Errorf("expected scheme not to be registered")
|
||||||
|
}
|
||||||
|
ds.RegisterScheme("scheme", rt{})
|
||||||
|
if !ds.HasScheme("scheme") {
|
||||||
|
t.Errorf("expected scheme to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
576
common/kademlia/kademlia.go
Normal file
576
common/kademlia/kademlia.go
Normal file
|
|
@ -0,0 +1,576 @@
|
||||||
|
package kademlia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kadlogger = logger.NewLogger("KΛÐ")
|
||||||
|
|
||||||
|
const (
|
||||||
|
minBucketSize = 1
|
||||||
|
bucketSize = 20
|
||||||
|
maxProx = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kademlia struct {
|
||||||
|
// immutable baseparam
|
||||||
|
addr Address
|
||||||
|
|
||||||
|
// adjustable parameters
|
||||||
|
MaxProx int
|
||||||
|
ProxBinSize int
|
||||||
|
BucketSize int
|
||||||
|
MinBucketSize int
|
||||||
|
nodeDB [][]*NodeRecord
|
||||||
|
nodeIndex map[Address]*NodeRecord
|
||||||
|
|
||||||
|
GetNode func(int)
|
||||||
|
|
||||||
|
// state
|
||||||
|
proxLimit int
|
||||||
|
proxSize int
|
||||||
|
|
||||||
|
//
|
||||||
|
count int
|
||||||
|
buckets []*bucket
|
||||||
|
|
||||||
|
dblock sync.RWMutex
|
||||||
|
lock sync.RWMutex
|
||||||
|
quitC chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Address common.Hash
|
||||||
|
|
||||||
|
func (a Address) String() string {
|
||||||
|
return fmt.Sprintf("%x", a[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node interface {
|
||||||
|
Addr() Address
|
||||||
|
Url() string
|
||||||
|
LastActive() time.Time
|
||||||
|
Drop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeRecord struct {
|
||||||
|
Address Address `json:address`
|
||||||
|
Url string `json:url`
|
||||||
|
Active int64 `json:active`
|
||||||
|
node Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *NodeRecord) setActive() {
|
||||||
|
if self.node != nil {
|
||||||
|
self.Active = self.node.LastActive().UnixNano()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type kadDB struct {
|
||||||
|
Address Address `json:address`
|
||||||
|
Nodes [][]*NodeRecord `json:nodes`
|
||||||
|
}
|
||||||
|
|
||||||
|
// public constructor
|
||||||
|
// hash is a byte slice of length equal to self.HashBytes
|
||||||
|
func New() *Kademlia {
|
||||||
|
return &Kademlia{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessor for KAD self address
|
||||||
|
func (self *Kademlia) Addr() Address {
|
||||||
|
return self.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessor for KAD self count
|
||||||
|
func (self *Kademlia) Count() int {
|
||||||
|
return self.count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start brings up a pool of entries potentially from an offline persisted source
|
||||||
|
// and sets default values for optional parameters
|
||||||
|
func (self *Kademlia) Start(addr Address) error {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if self.quitC != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.addr = addr
|
||||||
|
if self.MaxProx == 0 {
|
||||||
|
self.MaxProx = maxProx
|
||||||
|
}
|
||||||
|
if self.BucketSize == 0 {
|
||||||
|
self.BucketSize = bucketSize
|
||||||
|
}
|
||||||
|
if self.MinBucketSize == 0 {
|
||||||
|
self.MinBucketSize = minBucketSize
|
||||||
|
}
|
||||||
|
// runtime parameters
|
||||||
|
if self.ProxBinSize == 0 {
|
||||||
|
self.ProxBinSize = self.BucketSize
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buckets = make([]*bucket, self.MaxProx+1)
|
||||||
|
for i, _ := range self.buckets {
|
||||||
|
self.buckets[i] = &bucket{size: self.BucketSize} // will initialise bucket{int(0),[]Node(nil),sync.Mutex}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nodeDB = make([][]*NodeRecord, 8*len(self.addr))
|
||||||
|
self.nodeIndex = make(map[Address]*NodeRecord)
|
||||||
|
|
||||||
|
self.quitC = make(chan bool)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop saves the routing table into a persistant form
|
||||||
|
func (self *Kademlia) Stop(path string) (err error) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if self.quitC == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(self.quitC)
|
||||||
|
self.quitC = nil
|
||||||
|
|
||||||
|
if len(path) > 0 {
|
||||||
|
err = self.Save(path)
|
||||||
|
if err != nil {
|
||||||
|
kadlogger.Warnf("unable to save node records: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNode is the entrypoint where nodes are taken offline
|
||||||
|
func (self *Kademlia) RemoveNode(node Node) (err error) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
index := self.proximityBin(node.Addr())
|
||||||
|
bucket := self.buckets[index]
|
||||||
|
for i := 0; i < len(bucket.nodes); i++ {
|
||||||
|
if node.Addr() == bucket.nodes[i].Addr() {
|
||||||
|
bucket.nodes = append(bucket.nodes[:i], bucket.nodes[(i+1):]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.count--
|
||||||
|
if len(bucket.nodes) < bucket.size {
|
||||||
|
err = fmt.Errorf("insufficient nodes (%v) in bucket %v", len(bucket.nodes), index)
|
||||||
|
}
|
||||||
|
if len(bucket.nodes) == 0 || index >= self.proxLimit {
|
||||||
|
self.adjustProx(index, -1)
|
||||||
|
}
|
||||||
|
// async callback to notify user that bucket needs filling
|
||||||
|
// action is left to the user
|
||||||
|
if self.GetNode != nil {
|
||||||
|
go self.GetNode(index)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNode is the entry point where new nodes are registered
|
||||||
|
func (self *Kademlia) AddNode(node Node) (err error) {
|
||||||
|
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
|
||||||
|
index := self.proximityBin(node.Addr())
|
||||||
|
kadlogger.Debugf("bin %d, len: %d\n", index, len(self.buckets))
|
||||||
|
|
||||||
|
bucket := self.buckets[index]
|
||||||
|
err = bucket.insert(node)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.count++
|
||||||
|
if index >= self.proxLimit {
|
||||||
|
self.adjustProx(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
self.dblock.Lock()
|
||||||
|
defer self.dblock.Unlock()
|
||||||
|
record, found := self.nodeIndex[node.Addr()]
|
||||||
|
if found {
|
||||||
|
record.node = node
|
||||||
|
} else {
|
||||||
|
record = &NodeRecord{
|
||||||
|
Address: node.Addr(),
|
||||||
|
Url: node.Url(),
|
||||||
|
Active: node.LastActive().UnixNano(),
|
||||||
|
node: node,
|
||||||
|
}
|
||||||
|
self.nodeIndex[node.Addr()] = record
|
||||||
|
self.nodeDB[index] = append(self.nodeDB[index], record)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
kadlogger.Infof("add peer %v...", node)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust Prox (proxLimit and proxSize after an insertion of add nodes into bucket r)
|
||||||
|
func (self *Kademlia) adjustProx(r int, add int) {
|
||||||
|
var i int
|
||||||
|
switch {
|
||||||
|
case add > 0 && r >= self.proxLimit:
|
||||||
|
self.proxSize += add
|
||||||
|
for i = self.proxLimit; i < self.MaxProx && len(self.buckets[i].nodes) > 0 && self.proxSize > self.ProxBinSize; i++ {
|
||||||
|
self.proxSize -= len(self.buckets[i].nodes)
|
||||||
|
}
|
||||||
|
self.proxLimit = i
|
||||||
|
case add < 0 && r < self.proxLimit && len(self.buckets[r].nodes) == 0:
|
||||||
|
for i = self.proxLimit - 1; i > r; i-- {
|
||||||
|
self.proxSize += len(self.buckets[i].nodes)
|
||||||
|
}
|
||||||
|
self.proxLimit = r
|
||||||
|
case add < 0 && self.proxLimit > 0 && r >= self.proxLimit-1:
|
||||||
|
for i = self.proxLimit - 1; len(self.buckets[i].nodes)+self.proxSize <= self.ProxBinSize; i-- {
|
||||||
|
self.proxSize += len(self.buckets[i].nodes)
|
||||||
|
}
|
||||||
|
self.proxLimit = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetNodes(target) returns the list of nodes belonging to the same proximity bin
|
||||||
|
as the target. The most proximate bin will be the union of the bins between
|
||||||
|
proxLimit and MaxProx. proxLimit is dynamically adjusted so that 1) there is no
|
||||||
|
empty buckets in bin < proxLimit and 2) the sum of all items are the maximum
|
||||||
|
possible but lower than ProxBinSize
|
||||||
|
*/
|
||||||
|
func (self *Kademlia) GetNodes(target Address, max int) []Node {
|
||||||
|
return self.getNodes(target, max).nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kademlia) getNodes(target Address, max int) (r nodesByDistance) {
|
||||||
|
self.lock.RLock()
|
||||||
|
defer self.lock.RUnlock()
|
||||||
|
r.target = target
|
||||||
|
index := self.proximityBin(target)
|
||||||
|
start := index
|
||||||
|
var down bool
|
||||||
|
if index >= self.proxLimit {
|
||||||
|
index = self.proxLimit
|
||||||
|
start = self.MaxProx
|
||||||
|
down = true
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
limit := max
|
||||||
|
if max == 0 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
bucket := self.buckets[start].nodes
|
||||||
|
for i := 0; i < len(bucket); i++ {
|
||||||
|
r.push(bucket[i], limit)
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if max == 0 && start <= index && (n > 0 || start == 0) ||
|
||||||
|
max > 0 && down && start <= index && (n >= limit || n == self.Count() || start == 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if down {
|
||||||
|
start--
|
||||||
|
} else {
|
||||||
|
if start == self.MaxProx {
|
||||||
|
if index == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start = index - 1
|
||||||
|
down = true
|
||||||
|
} else {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is used to add node records to the persisted db
|
||||||
|
// TODO: maybe db needs to be purged occasionally (reputation will take care of
|
||||||
|
// that)
|
||||||
|
func (self *Kademlia) AddNodeRecords(nrs []*NodeRecord) {
|
||||||
|
self.dblock.Lock()
|
||||||
|
defer self.dblock.Unlock()
|
||||||
|
for _, node := range nrs {
|
||||||
|
_, found := self.nodeIndex[node.Address]
|
||||||
|
if !found {
|
||||||
|
self.nodeIndex[node.Address] = node
|
||||||
|
index := proximity(self.addr, node.Address)
|
||||||
|
if index < len(self.nodeDB) {
|
||||||
|
self.nodeDB[index] = append(self.nodeDB[index], node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetNodeRecord gives back a node record with the highest priority for desired
|
||||||
|
connection
|
||||||
|
Used to pick candidates for live nodes to satisfy Kademlia network for Swarm
|
||||||
|
|
||||||
|
if len(nodes) < MinBucketSize, then take ith element in corresponding
|
||||||
|
db row ordered by reputation (active time?)
|
||||||
|
node record a is more favoured to b a > b iff
|
||||||
|
|proxBin(a)| < |proxBin(b)|
|
||||||
|
|| proxBin(a) < proxBin(b) && |proxBin(a)| < MinBucketSize
|
||||||
|
|| lastActive(a) < lastActive(b)
|
||||||
|
|
||||||
|
This has double role. Starting as naive node with empty db, this implements
|
||||||
|
Kademlia bootstrapping
|
||||||
|
As a mature node, it manages quickly fill in blanks or short lines
|
||||||
|
All on demand
|
||||||
|
*/
|
||||||
|
func (self *Kademlia) GetNodeRecord() (*NodeRecord, bool) {
|
||||||
|
full := true
|
||||||
|
for i, b := range self.nodeDB {
|
||||||
|
if i >= self.MaxProx {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(self.buckets[i].nodes) < self.MinBucketSize {
|
||||||
|
full = false
|
||||||
|
for _, node := range b {
|
||||||
|
if node.node == nil {
|
||||||
|
return node, full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, b := range self.nodeDB {
|
||||||
|
if i > self.MaxProx {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(self.buckets[i].nodes) < self.BucketSize {
|
||||||
|
full = false
|
||||||
|
for _, node := range b {
|
||||||
|
if node.node == nil {
|
||||||
|
return node, full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, full
|
||||||
|
}
|
||||||
|
|
||||||
|
// in situ mutable bucket
|
||||||
|
type bucket struct {
|
||||||
|
size int
|
||||||
|
nodes []Node
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Address) Bin() string {
|
||||||
|
var bs []string
|
||||||
|
for _, b := range a[:] {
|
||||||
|
bs = append(bs, fmt.Sprintf("%08b", b))
|
||||||
|
}
|
||||||
|
return strings.Join(bs, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodesByDistance is a list of nodes, ordered by distance to target.
|
||||||
|
type nodesByDistance struct {
|
||||||
|
nodes []Node
|
||||||
|
target Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedByDistanceTo(target Address, slice []Node) bool {
|
||||||
|
var last Address
|
||||||
|
for i, node := range slice {
|
||||||
|
if i > 0 {
|
||||||
|
if proxCmp(target, node.Addr(), last) < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last = node.Addr()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// push(node, max) adds the given node to the list, keeping the total size
|
||||||
|
// below max elements.
|
||||||
|
func (h *nodesByDistance) push(node Node, max int) {
|
||||||
|
// returns the firt index ix such that func(i) returns true
|
||||||
|
ix := sort.Search(len(h.nodes), func(i int) bool {
|
||||||
|
return proxCmp(h.target, h.nodes[i].Addr(), node.Addr()) >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(h.nodes) < max {
|
||||||
|
h.nodes = append(h.nodes, node)
|
||||||
|
}
|
||||||
|
if ix < len(h.nodes) {
|
||||||
|
copy(h.nodes[ix+1:], h.nodes[ix:])
|
||||||
|
h.nodes[ix] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert adds a peer to a bucket either by appending to existing items if
|
||||||
|
// bucket length does not exceed bucketLength, or by replacing the worst
|
||||||
|
// Node in the bucket
|
||||||
|
func (self *bucket) insert(node Node) (err error) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if len(self.nodes) >= self.size { // >= allows us to add peers beyond the bucketsize limitation
|
||||||
|
self.worstNode().Drop() // assumes self.size > 0
|
||||||
|
}
|
||||||
|
self.nodes = append(self.nodes, node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// worst expunges the single worst entry in a row, where worst entry is with a peer that has not been active the longests
|
||||||
|
func (self *bucket) worstNode() (node Node) {
|
||||||
|
var oldest time.Time
|
||||||
|
for _, n := range self.nodes {
|
||||||
|
if (oldest == time.Time{}) || node.LastActive().Before(oldest) {
|
||||||
|
oldest = n.LastActive()
|
||||||
|
node = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Taking the proximity value relative to a fix point x classifies the points in
|
||||||
|
the space (n byte long byte sequences) into bins the items in which are each at
|
||||||
|
most half as distant from x as items in the previous bin. Given a sample of
|
||||||
|
uniformly distrbuted items (a hash function over arbitrary sequence) the
|
||||||
|
proximity scale maps onto series of subsets with cardinalities on a negative
|
||||||
|
exponential scale.
|
||||||
|
|
||||||
|
It also has the property that any two item belonging to the same bin are at
|
||||||
|
most half as distant from each other as they are from x.
|
||||||
|
|
||||||
|
If we think of random sample of items in the bins as connections in a network of interconnected nodes than relative proximity can serve as the basis for local
|
||||||
|
decisions for graph traversal where the task is to find a route between two
|
||||||
|
points. Since in every step of forwarding, the finite distance halves, there is
|
||||||
|
a guaranteed constant maximum limit on the number of hops needed to reach one
|
||||||
|
node from the other.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (self *Kademlia) proximityBin(other Address) (ret int) {
|
||||||
|
ret = proximity(self.addr, other)
|
||||||
|
if ret > self.MaxProx {
|
||||||
|
ret = self.MaxProx
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The distance metric MSB(x, y) of two equal length byte sequences x an y is the
|
||||||
|
value of the binary integer cast of the xor-ed byte sequence (most significant
|
||||||
|
bit first).
|
||||||
|
proximity(x, y) counts the common zeros in the front of this distance measure.
|
||||||
|
which is equivalent to the reverse rank of the integer part of the base 2
|
||||||
|
logarithm of the distance
|
||||||
|
called proximity belt (0 farthest, 255 closest, 256 self)
|
||||||
|
*/
|
||||||
|
func proximity(one, other Address) (ret int) {
|
||||||
|
for i := 0; i < len(one); i++ {
|
||||||
|
oxo := one[i] ^ other[i]
|
||||||
|
for j := 0; j < 8; j++ {
|
||||||
|
if (uint8(oxo)>>uint8(7-j))&0x1 != 0 {
|
||||||
|
return i*8 + j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(one) * 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxCmp compares the distances a->target and b->target.
|
||||||
|
// Returns -1 if a is closer to target, 1 if b is closer to target
|
||||||
|
// and 0 if they are equal.
|
||||||
|
func proxCmp(target, a, b Address) int {
|
||||||
|
for i := range target {
|
||||||
|
da := a[i] ^ target[i]
|
||||||
|
db := b[i] ^ target[i]
|
||||||
|
if da > db {
|
||||||
|
return 1
|
||||||
|
} else if da < db {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kademlia) DB() [][]*NodeRecord {
|
||||||
|
return self.nodeDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// save persists all peers encountered
|
||||||
|
func (self *Kademlia) Save(path string) error {
|
||||||
|
|
||||||
|
kad := kadDB{
|
||||||
|
Address: self.addr,
|
||||||
|
Nodes: self.nodeDB,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b := range kad.Nodes {
|
||||||
|
for _, node := range b {
|
||||||
|
node.setActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(&kad, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(path, data, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loading the idle node record from disk
|
||||||
|
func (self *Kademlia) Load(path string) (err error) {
|
||||||
|
var data []byte
|
||||||
|
data, err = ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var kad kadDB
|
||||||
|
err = json.Unmarshal(data, &kad)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.addr != kad.Address {
|
||||||
|
return fmt.Errorf("invalid kad db: address mismatch, expected %v, got %v", self.addr, kad.Address)
|
||||||
|
}
|
||||||
|
self.nodeDB = kad.Nodes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomAddressAt(address, prox) generates a random address
|
||||||
|
// at proximity order prox relative to address
|
||||||
|
// if prox is negative a random address is generated
|
||||||
|
func RandomAddressAt(self Address, prox int) (addr Address) {
|
||||||
|
addr = self
|
||||||
|
var pos int
|
||||||
|
if prox >= 0 {
|
||||||
|
pos = prox / 8
|
||||||
|
trans := prox % 8
|
||||||
|
transbytea := byte(0)
|
||||||
|
for j := 0; j <= trans; j++ {
|
||||||
|
transbytea |= 1 << uint8(7-j)
|
||||||
|
}
|
||||||
|
flipbyte := byte(1 << uint8(7-trans))
|
||||||
|
transbyteb := transbytea ^ byte(255)
|
||||||
|
randbyte := byte(rand.Intn(255))
|
||||||
|
addr[pos] = ((addr[pos] & transbytea) ^ flipbyte) | randbyte&transbyteb
|
||||||
|
}
|
||||||
|
for i := pos + 1; i < len(addr); i++ {
|
||||||
|
addr[i] = byte(rand.Intn(255))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomAddressAt() generates a random address
|
||||||
|
func RandomAddress() Address {
|
||||||
|
return RandomAddressAt(Address{}, -1)
|
||||||
|
}
|
||||||
479
common/kademlia/kademlia_test.go
Normal file
479
common/kademlia/kademlia_test.go
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
package kademlia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quickrand = rand.New(rand.NewSource(time.Now().Unix()))
|
||||||
|
quickcfgGetNodes = &quick.Config{MaxCount: 5000, Rand: quickrand}
|
||||||
|
quickcfgBootStrap = &quick.Config{MaxCount: 1000, Rand: quickrand}
|
||||||
|
)
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func LogInit(l logger.LogLevel) {
|
||||||
|
once.Do(func() {
|
||||||
|
logger.NewStdLogSystem(os.Stderr, log.LstdFlags, l)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type testNode struct {
|
||||||
|
addr Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) String() string {
|
||||||
|
return fmt.Sprintf("%x", n.addr[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) Addr() Address {
|
||||||
|
return n.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) Drop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) Url() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) LastActive() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNode) Add(a Address) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddNode(t *testing.T) {
|
||||||
|
LogInit(logger.DebugLevel)
|
||||||
|
addr, ok := gen(Address{}, quickrand).(Address)
|
||||||
|
other, ok := gen(Address{}, quickrand).(Address)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("oops")
|
||||||
|
}
|
||||||
|
kad := New()
|
||||||
|
kad.Start(addr)
|
||||||
|
err := kad.AddNode(&testNode{addr: other})
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
LogInit(logger.DebugLevel)
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
test := func(test *bootstrapTest) bool {
|
||||||
|
// for any node kad.le, Target and N
|
||||||
|
kad := New()
|
||||||
|
kad.MaxProx = test.MaxProx
|
||||||
|
kad.MinBucketSize = test.MinBucketSize
|
||||||
|
kad.BucketSize = test.BucketSize
|
||||||
|
kad.Start(test.Self)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
t.Logf("bootstapTest MaxProx: %v MinBucketSize: %v BucketSize: %v\n", test.MaxProx, test.MinBucketSize, test.BucketSize)
|
||||||
|
|
||||||
|
addr := gen(Address{}, r).(Address)
|
||||||
|
prox := proximity(addr, test.Self)
|
||||||
|
|
||||||
|
for p := 0; p <= prox; p++ {
|
||||||
|
var nrs []*NodeRecord
|
||||||
|
for i := 0; i < test.BucketSize; i++ {
|
||||||
|
nrs = append(nrs, &NodeRecord{
|
||||||
|
Address: RandomAddressAt(test.Self, p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
kad.AddNodeRecords(nrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
node := &testNode{addr}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for n < 100 {
|
||||||
|
err = kad.AddNode(node)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var nrs []*NodeRecord
|
||||||
|
prox := proximity(node.addr, test.Self)
|
||||||
|
for i := 0; i < test.BucketSize; i++ {
|
||||||
|
nrs = append(nrs, &NodeRecord{
|
||||||
|
Address: RandomAddressAt(test.Self, prox+1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
kad.AddNodeRecords(nrs)
|
||||||
|
|
||||||
|
var lens []int
|
||||||
|
for i := 0; i <= test.MaxProx; i++ {
|
||||||
|
lens = append(lens, len(kad.buckets[i].nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
record, _ := kad.GetNodeRecord()
|
||||||
|
if record == nil {
|
||||||
|
t.Logf("after round %d, no more node records needed", n)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
node = &testNode{record.Address}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
exp := test.BucketSize * (test.MaxProx + 1)
|
||||||
|
if kad.Count() != exp {
|
||||||
|
t.Errorf("incorrect number of peers, expceted %d, got %d", exp, kad.Count())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
if n < 1 {
|
||||||
|
t.Errorf("incorrect number of rounds, expceted %d, got %d", 0, n)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := quick.Check(test, quickcfgBootStrap); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNodes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
LogInit(logger.DebugLevel)
|
||||||
|
|
||||||
|
test := func(test *getNodesTest) bool {
|
||||||
|
// for any node kad.le, Target and N
|
||||||
|
kad := New()
|
||||||
|
kad.MaxProx = 10
|
||||||
|
kad.Start(test.Self)
|
||||||
|
var err error
|
||||||
|
t.Logf("getNodesTest %v: %v\n", len(test.All), test)
|
||||||
|
for _, node := range test.All {
|
||||||
|
err = kad.AddNode(node)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.All) == 0 || test.N == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
nodes := kad.GetNodes(test.Target, test.N)
|
||||||
|
|
||||||
|
// check that the number of results is min(N, kad.len)
|
||||||
|
wantN := test.N
|
||||||
|
if tlen := kad.Count(); tlen < test.N {
|
||||||
|
wantN = tlen
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodes) != wantN {
|
||||||
|
t.Errorf("wrong number of nodes: got %d, want %d", len(nodes), wantN)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasDuplicates(nodes) {
|
||||||
|
t.Errorf("result contains duplicates")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sortedByDistanceTo(test.Target, nodes) {
|
||||||
|
t.Errorf("result is not sorted by distance to target")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the result nodes have minimum distance to target.
|
||||||
|
farthestResult := nodes[len(nodes)-1].Addr()
|
||||||
|
for i, b := range kad.buckets {
|
||||||
|
for j, n := range b.nodes {
|
||||||
|
if contains(nodes, n.Addr()) {
|
||||||
|
continue // don't run the check below for nodes in result
|
||||||
|
}
|
||||||
|
if proxCmp(test.Target, n.Addr(), farthestResult) < 0 {
|
||||||
|
t.Errorf("kad.le contains node that is closer to target but it's not in result")
|
||||||
|
t.Logf("bucket %v, item %v\n", i, j)
|
||||||
|
t.Logf(" Target: %x", test.Target)
|
||||||
|
t.Logf(" Farthest Result: %x", farthestResult)
|
||||||
|
t.Logf(" ID: %x (%d)", n.Addr(), kad.proximityBin(n.Addr()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := quick.Check(test, quickcfgGetNodes); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxTest struct {
|
||||||
|
add bool
|
||||||
|
index int
|
||||||
|
address Address
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
addresses []Address
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProxAdjust(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
LogInit(logger.DebugLevel)
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
self := gen(Address{}, r).(Address)
|
||||||
|
|
||||||
|
kad := New()
|
||||||
|
kad.MaxProx = 10
|
||||||
|
kad.Start(self)
|
||||||
|
var err error
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
a := gen(Address{}, r).(Address)
|
||||||
|
addresses = append(addresses, a)
|
||||||
|
err = kad.AddNode(&testNode{addr: a})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !kad.proxCheck(t) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test := func(test *proxTest) bool {
|
||||||
|
node := &testNode{test.address}
|
||||||
|
if test.add {
|
||||||
|
kad.AddNode(node)
|
||||||
|
} else {
|
||||||
|
kad.RemoveNode(node)
|
||||||
|
}
|
||||||
|
return kad.proxCheck(t)
|
||||||
|
}
|
||||||
|
if err := quick.Check(test, quickcfgGetNodes); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallback(t *testing.T) {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
self := gen(Address{}, r).(Address)
|
||||||
|
kad := New()
|
||||||
|
var bucket int
|
||||||
|
var called chan bool
|
||||||
|
kad.MaxProx = 5
|
||||||
|
kad.BucketSize = 2
|
||||||
|
kad.GetNode = func(b int) {
|
||||||
|
bucket = b
|
||||||
|
close(called)
|
||||||
|
}
|
||||||
|
kad.Start(self)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
a := gen(Address{}, r).(Address)
|
||||||
|
addresses = append(addresses, a)
|
||||||
|
err = kad.AddNode(&testNode{addr: a})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, a := range addresses {
|
||||||
|
called = make(chan bool)
|
||||||
|
kad.RemoveNode(&testNode{a})
|
||||||
|
b := kad.proximityBin(a)
|
||||||
|
select {
|
||||||
|
case <-called:
|
||||||
|
if bucket != b {
|
||||||
|
t.Errorf("GetNode callback called with incorrect bucket, expected %v, got %v", b, bucket)
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
l := len(kad.buckets[b].nodes)
|
||||||
|
if l < kad.BucketSize {
|
||||||
|
t.Errorf("GetNode not called on bucket %v although size is %v < %v", b, l, kad.BucketSize)
|
||||||
|
} else {
|
||||||
|
t.Logf("bucket callback ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveLoad(t *testing.T) {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
addresses := gen([]Address{}, r).([]Address)
|
||||||
|
self := addresses[0]
|
||||||
|
kad := New()
|
||||||
|
kad.MaxProx = 10
|
||||||
|
kad.Start(self)
|
||||||
|
var err error
|
||||||
|
for _, a := range addresses[1:] {
|
||||||
|
err = kad.AddNode(&testNode{addr: a})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes := kad.GetNodes(self, 100)
|
||||||
|
path := "/tmp/bzz.peers"
|
||||||
|
kad.Stop(path)
|
||||||
|
kad = New()
|
||||||
|
kad.Start(self)
|
||||||
|
kad.Load(path)
|
||||||
|
for _, b := range kad.DB() {
|
||||||
|
for _, node := range b {
|
||||||
|
node.node = &testNode{node.Address}
|
||||||
|
err = kad.AddNode(node.node)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("backend not accepting node")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadednodes := kad.GetNodes(self, 100)
|
||||||
|
for i, node := range loadednodes {
|
||||||
|
if nodes[i].Addr() != node.Addr() {
|
||||||
|
t.Errorf("node mismatch at %d/%d", i, len(nodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kademlia) proxCheck(t *testing.T) bool {
|
||||||
|
var sum, i int
|
||||||
|
var b *bucket
|
||||||
|
for i, b = range self.buckets {
|
||||||
|
l := len(b.nodes)
|
||||||
|
// if we are in the high prox multibucket
|
||||||
|
if i >= self.proxLimit {
|
||||||
|
// unless it starts with an empty bucket, count the size
|
||||||
|
if l > 0 || sum > 0 {
|
||||||
|
sum += l
|
||||||
|
}
|
||||||
|
} else if l == 0 {
|
||||||
|
t.Errorf("bucket %d empty, yet proxLimit is %d", len(b.nodes), self.proxLimit)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if merged high prox bucket does not exceed size
|
||||||
|
if sum > 0 {
|
||||||
|
if sum > self.ProxBinSize {
|
||||||
|
t.Errorf("bucket %d is empty, yet proxSize is %d", i, self.proxSize)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if sum != self.proxSize {
|
||||||
|
t.Errorf("proxSize incorrect, expected %v, got %v", sum, self.proxSize)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.proxLimit > 0 && sum+len(self.buckets[self.proxLimit-1].nodes) < self.ProxBinSize {
|
||||||
|
t.Errorf("proxBinSize incorrect, expected %v got %v", sum, self.proxSize)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type bootstrapTest struct {
|
||||||
|
MaxProx int
|
||||||
|
MinBucketSize int
|
||||||
|
BucketSize int
|
||||||
|
Self Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*bootstrapTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||||
|
m := rand.Intn(2) + 1
|
||||||
|
t := &bootstrapTest{
|
||||||
|
Self: gen(Address{}, rand).(Address),
|
||||||
|
MaxProx: 10 + rand.Intn(3),
|
||||||
|
MinBucketSize: m,
|
||||||
|
BucketSize: rand.Intn(3) + m,
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
type getNodesTest struct {
|
||||||
|
Self Address
|
||||||
|
Target Address
|
||||||
|
All []Node
|
||||||
|
N int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c getNodesTest) String() string {
|
||||||
|
return fmt.Sprintf("A: %x\nT: %x\n(%d)\n", c.Self, c.Target, c.N)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*getNodesTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||||
|
t := &getNodesTest{
|
||||||
|
Self: gen(Address{}, rand).(Address),
|
||||||
|
Target: gen(Address{}, rand).(Address),
|
||||||
|
N: rand.Intn(bucketSize),
|
||||||
|
}
|
||||||
|
for _, a := range gen([]Address{}, rand).([]Address) {
|
||||||
|
t.All = append(t.All, &testNode{addr: a})
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*proxTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||||
|
var add bool
|
||||||
|
if rand.Intn(1) == 0 {
|
||||||
|
add = true
|
||||||
|
}
|
||||||
|
var t *proxTest
|
||||||
|
if add {
|
||||||
|
t = &proxTest{
|
||||||
|
address: gen(Address{}, rand).(Address),
|
||||||
|
add: add,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t = &proxTest{
|
||||||
|
index: rand.Intn(len(addresses)),
|
||||||
|
add: add,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDuplicates(slice []Node) bool {
|
||||||
|
seen := make(map[Address]bool)
|
||||||
|
for _, node := range slice {
|
||||||
|
if seen[node.Addr()] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
seen[node.Addr()] = true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(nodes []Node, addr Address) bool {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n.Addr() == addr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen wraps quick.Value so it's easier to use.
|
||||||
|
// it generates a random value of the given value's type.
|
||||||
|
func gen(typ interface{}, rand *rand.Rand) interface{} {
|
||||||
|
v, ok := quick.Value(reflect.TypeOf(typ), rand)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("couldn't generate random value of type %T", typ))
|
||||||
|
}
|
||||||
|
return v.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Address) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||||
|
var id Address
|
||||||
|
// m := rand.Intn(len(id))
|
||||||
|
for i := 0; i < len(id); i++ {
|
||||||
|
id[i] = byte(rand.Uint32())
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(id)
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/docserver"
|
"github.com/ethereum/go-ethereum/common/docserver"
|
||||||
"github.com/ethereum/go-ethereum/common/resolver"
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/xeth"
|
"github.com/ethereum/go-ethereum/xeth"
|
||||||
)
|
)
|
||||||
|
|
@ -88,7 +88,7 @@ func New(xeth *xeth.XEth, jsontx string, http *docserver.DocServer) (self *NatSp
|
||||||
}
|
}
|
||||||
|
|
||||||
// also called by admin.contractInfo.get
|
// also called by admin.contractInfo.get
|
||||||
func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, http *docserver.DocServer) (content []byte, err error) {
|
func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, ds *docserver.DocServer) (content []byte, err error) {
|
||||||
// retrieve contract hash from state
|
// retrieve contract hash from state
|
||||||
codehex := xeth.CodeAt(contractAddress)
|
codehex := xeth.CodeAt(contractAddress)
|
||||||
codeb := xeth.CodeAtBytes(contractAddress)
|
codeb := xeth.CodeAtBytes(contractAddress)
|
||||||
|
|
@ -99,20 +99,32 @@ func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, http *docserv
|
||||||
}
|
}
|
||||||
codehash := common.BytesToHash(crypto.Sha3(codeb))
|
codehash := common.BytesToHash(crypto.Sha3(codeb))
|
||||||
// set up nameresolver with natspecreg + urlhint contract addresses
|
// set up nameresolver with natspecreg + urlhint contract addresses
|
||||||
res := resolver.New(xeth)
|
reg := registrar.New(xeth)
|
||||||
|
|
||||||
// resolve host via HashReg/UrlHint Resolver
|
// resolve host via HashReg/UrlHint Resolver
|
||||||
uri, hash, err := res.KeyToUrl(codehash)
|
hash, err := reg.HashToHash(codehash)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ds.HasScheme("bzz") {
|
||||||
|
content, err = ds.Get("bzz://"+hash.Hex()[2:], "")
|
||||||
|
if err == nil { // non-fatal
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = nil
|
||||||
|
//falling back to urlhint
|
||||||
|
}
|
||||||
|
|
||||||
|
uri, err := reg.HashToUrl(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get content via http client and authenticate content using hash
|
// get content via http client and authenticate content using hash
|
||||||
content, err = http.GetAuthContent(uri, hash)
|
content, err = ds.GetAuthContent(uri, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/docserver"
|
"github.com/ethereum/go-ethereum/common/docserver"
|
||||||
"github.com/ethereum/go-ethereum/common/resolver"
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
"github.com/ethereum/go-ethereum/core/state"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
|
@ -74,7 +74,6 @@ const (
|
||||||
|
|
||||||
type testFrontend struct {
|
type testFrontend struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
// resolver *resolver.Resolver
|
|
||||||
ethereum *eth.Ethereum
|
ethereum *eth.Ethereum
|
||||||
xeth *xe.XEth
|
xeth *xe.XEth
|
||||||
coinbase common.Address
|
coinbase common.Address
|
||||||
|
|
@ -91,10 +90,7 @@ func (self *testFrontend) UnlockAccount(acc []byte) bool {
|
||||||
|
|
||||||
func (self *testFrontend) ConfirmTransaction(tx string) bool {
|
func (self *testFrontend) ConfirmTransaction(tx string) bool {
|
||||||
if self.wantNatSpec {
|
if self.wantNatSpec {
|
||||||
ds, err := docserver.New("/tmp/")
|
ds := docserver.New("/tmp/")
|
||||||
if err != nil {
|
|
||||||
self.t.Errorf("Error creating DocServer: %v", err)
|
|
||||||
}
|
|
||||||
self.lastConfirm = GetNotice(self.xeth, tx, ds)
|
self.lastConfirm = GetNotice(self.xeth, tx, ds)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
@ -159,11 +155,16 @@ func testInit(t *testing.T) (self *testFrontend) {
|
||||||
self.stateDb = self.ethereum.ChainManager().State().Copy()
|
self.stateDb = self.ethereum.ChainManager().State().Copy()
|
||||||
|
|
||||||
// initialise the registry contracts
|
// initialise the registry contracts
|
||||||
// self.resolver.CreateContracts(addr)
|
reg := registrar.New(self.xeth)
|
||||||
resolver.New(self.xeth).CreateContracts(addr)
|
err = reg.SetHashReg("", addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating HashReg: %v", err)
|
||||||
|
}
|
||||||
|
err = reg.SetUrlHint("", addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating UrlHint: %v", err)
|
||||||
|
}
|
||||||
self.applyTxs()
|
self.applyTxs()
|
||||||
// t.Logf("HashReg contract registered at %v", resolver.HashRegContractAddress)
|
|
||||||
// t.Logf("URLHint contract registered at %v", resolver.UrlHintContractAddress)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -192,13 +193,20 @@ func TestNatspecE2E(t *testing.T) {
|
||||||
dochash := common.BytesToHash(crypto.Sha3([]byte(testContractInfo)))
|
dochash := common.BytesToHash(crypto.Sha3([]byte(testContractInfo)))
|
||||||
|
|
||||||
// take the codehash for the contract we wanna test
|
// take the codehash for the contract we wanna test
|
||||||
// codehex := tf.xeth.CodeAt(resolver.HashRegContractAddress)
|
// codehex := tf.xeth.CodeAt(registar.HashRegAddr)
|
||||||
codeb := tf.xeth.CodeAtBytes(resolver.HashRegContractAddress)
|
codeb := tf.xeth.CodeAtBytes(registrar.HashRegAddr)
|
||||||
codehash := common.BytesToHash(crypto.Sha3(codeb))
|
codehash := common.BytesToHash(crypto.Sha3(codeb))
|
||||||
|
|
||||||
// use resolver to register codehash->dochash->url
|
// use resolver to register codehash->dochash->url
|
||||||
registry := resolver.New(tf.xeth)
|
// test if globalregistry works
|
||||||
_, err := registry.Register(tf.coinbase, codehash, dochash, "file:///"+testFileName)
|
// registrar.HashRefAddr = "0x0"
|
||||||
|
// registrar.UrlHintAddr = "0x0"
|
||||||
|
reg := registrar.New(tf.xeth)
|
||||||
|
_, err := reg.SetHashToHash(tf.coinbase, codehash, dochash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error registering: %v", err)
|
||||||
|
}
|
||||||
|
_, err = reg.SetUrlToHash(tf.coinbase, dochash, "file:///"+testFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error registering: %v", err)
|
t.Errorf("error registering: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -209,18 +217,19 @@ func TestNatspecE2E(t *testing.T) {
|
||||||
// now using the same transactions to check confirm messages
|
// now using the same transactions to check confirm messages
|
||||||
|
|
||||||
tf.wantNatSpec = true // this is set so now the backend uses natspec confirmation
|
tf.wantNatSpec = true // this is set so now the backend uses natspec confirmation
|
||||||
_, err = registry.RegisterContentHash(tf.coinbase, codehash, dochash)
|
_, err = reg.SetHashToHash(tf.coinbase, codehash, dochash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error calling contract registry: %v", err)
|
t.Errorf("error calling contract registry: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("GlobalRegistrar: %v, HashReg: %v, UrlHint: %v\n", registrar.GlobalRegistrarAddr, registrar.HashRegAddr, registrar.UrlHintAddr)
|
||||||
if tf.lastConfirm != testExpNotice {
|
if tf.lastConfirm != testExpNotice {
|
||||||
t.Errorf("Wrong confirm message. expected '%v', got '%v'", testExpNotice, tf.lastConfirm)
|
t.Errorf("Wrong confirm message. expected '%v', got '%v'", testExpNotice, tf.lastConfirm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test unknown method
|
// test unknown method
|
||||||
exp := fmt.Sprintf(testExpNotice2, resolver.HashRegContractAddress)
|
exp := fmt.Sprintf(testExpNotice2, registrar.HashRegAddr)
|
||||||
_, err = registry.SetOwner(tf.coinbase)
|
_, err = reg.SetOwner(tf.coinbase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error setting owner: %v", err)
|
t.Errorf("error setting owner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -230,9 +239,9 @@ func TestNatspecE2E(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// test unknown contract
|
// test unknown contract
|
||||||
exp = fmt.Sprintf(testExpNotice3, resolver.UrlHintContractAddress)
|
exp = fmt.Sprintf(testExpNotice3, registrar.UrlHintAddr)
|
||||||
|
|
||||||
_, err = registry.RegisterUrl(tf.coinbase, dochash, "file:///test.content")
|
_, err = reg.SetUrlToHash(tf.coinbase, dochash, "file:///test.content")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error registering: %v", err)
|
t.Errorf("error registering: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
147
common/registrar/contracts.go
Normal file
147
common/registrar/contracts.go
Normal file
File diff suppressed because one or more lines are too long
32
common/registrar/ethreg/ethreg.go
Normal file
32
common/registrar/ethreg/ethreg.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ethreg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/registrar"
|
||||||
|
"github.com/ethereum/go-ethereum/xeth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// implements a versioned Registrar on an archiving full node
|
||||||
|
type EthReg struct {
|
||||||
|
backend *xeth.XEth
|
||||||
|
registry *registrar.Registrar
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(xe *xeth.XEth) (self *EthReg) {
|
||||||
|
self = &EthReg{backend: xe}
|
||||||
|
self.registry = registrar.New(xe)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EthReg) Registry() *registrar.Registrar {
|
||||||
|
return self.registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EthReg) Resolver(n *big.Int) *registrar.Registrar {
|
||||||
|
xe := self.backend
|
||||||
|
if n != nil {
|
||||||
|
xe = self.backend.AtStateNum(n.Int64())
|
||||||
|
}
|
||||||
|
return registrar.New(xe)
|
||||||
|
}
|
||||||
391
common/registrar/registrar.go
Normal file
391
common/registrar/registrar.go
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
package registrar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Registrar implements the Ethereum name registrar services mapping
|
||||||
|
- arbitrary strings to ethereum addresses
|
||||||
|
- hashes to hashes
|
||||||
|
- hashes to arbitrary strings
|
||||||
|
(likely will provide lookup service for all three)
|
||||||
|
|
||||||
|
The Registrar is used by
|
||||||
|
* the roundtripper transport implementation of
|
||||||
|
url schemes to resolve domain names and services that register these names
|
||||||
|
* contract info retrieval (NatSpec).
|
||||||
|
|
||||||
|
The Registrar uses 3 contracts on the blockchain:
|
||||||
|
* GlobalRegistrar: Name (string) -> Address (Owner)
|
||||||
|
* HashReg : Key Hash (hash of domain name or contract code) -> Content Hash
|
||||||
|
* UrlHint : Content Hash -> Url Hint
|
||||||
|
|
||||||
|
These contracts are (currently) not included in the genesis block.
|
||||||
|
Each Set<X> needs to be called once on each blockchain/network once.
|
||||||
|
|
||||||
|
Contract addresses need to be set (HashReg and UrlHint retrieved from the global
|
||||||
|
registrar the first time any Registrar method is called in a client session
|
||||||
|
|
||||||
|
So the caller needs to make sure the relevant environment initialised the desired
|
||||||
|
contracts
|
||||||
|
*/
|
||||||
|
var (
|
||||||
|
UrlHintAddr = "0x0"
|
||||||
|
HashRegAddr = "0x0"
|
||||||
|
GlobalRegistrarAddr = "0x0"
|
||||||
|
// GlobalRegistrarAddr = "0xc6d9d2cd449a754c494264e1809c50e34d64562b"
|
||||||
|
|
||||||
|
zero = regexp.MustCompile("^(0x)?0*$")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
trueHex = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
falseHex = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
)
|
||||||
|
|
||||||
|
func abiSignature(s string) string {
|
||||||
|
return common.ToHex(crypto.Sha3([]byte(s))[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
HashRegName = "HashReg"
|
||||||
|
UrlHintName = "UrlHint"
|
||||||
|
|
||||||
|
registerContentHashAbi = abiSignature("register(uint256,uint256)")
|
||||||
|
registerUrlAbi = abiSignature("register(uint256,uint8,uint256)")
|
||||||
|
setOwnerAbi = abiSignature("setowner()")
|
||||||
|
reserveAbi = abiSignature("reserve(bytes32)")
|
||||||
|
resolveAbi = abiSignature("addr(bytes32)")
|
||||||
|
registerAbi = abiSignature("setAddress(bytes32,address,bool)")
|
||||||
|
addressAbiPrefix = falseHex[:24]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Registrar's backend is defined as an interface (implemented by xeth, but could be remote)
|
||||||
|
type Backend interface {
|
||||||
|
StorageAt(string, string) string
|
||||||
|
Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error)
|
||||||
|
Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Registrar should also just implement The Resolver and Registry interfaces
|
||||||
|
// Simplify for now.
|
||||||
|
type VersionedRegistrar interface {
|
||||||
|
Resolver(*big.Int) *Registrar
|
||||||
|
Registry() *Registrar
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registrar struct {
|
||||||
|
backend Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(b Backend) (res *Registrar) {
|
||||||
|
res = &Registrar{b}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Registrar) SetGlobalRegistrar(namereg string, addr common.Address) (err error) {
|
||||||
|
if namereg != "" {
|
||||||
|
GlobalRegistrarAddr = namereg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if GlobalRegistrarAddr == "0x0" || GlobalRegistrarAddr == "0x" {
|
||||||
|
if (addr == common.Address{}) {
|
||||||
|
err = fmt.Errorf("GlobalRegistrar address not found and sender for creation not given")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
GlobalRegistrarAddr, err = self.backend.Transact(addr.Hex(), "", "", "", "800000", "", GlobalRegistrarCode)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("GlobalRegistrar address not found and sender for creation failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Registrar) SetHashReg(hashreg string, addr common.Address) (err error) {
|
||||||
|
if hashreg != "" {
|
||||||
|
HashRegAddr = hashreg
|
||||||
|
} else {
|
||||||
|
if !zero.MatchString(HashRegAddr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nameHex, extra := encodeName(HashRegName, 2)
|
||||||
|
hashRegAbi := resolveAbi + nameHex + extra
|
||||||
|
glog.V(logger.Detail).Infof("\ncall HashRegAddr %v with %v\n", GlobalRegistrarAddr, hashRegAbi)
|
||||||
|
HashRegAddr, _, err = self.backend.Call("", GlobalRegistrarAddr, "", "", "", hashRegAbi)
|
||||||
|
if err != nil || zero.MatchString(HashRegAddr) {
|
||||||
|
if (addr == common.Address{}) {
|
||||||
|
err = fmt.Errorf("HashReg address not found and sender for creation not given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
HashRegAddr, err = self.backend.Transact(addr.Hex(), "", "", "", "200000", "", HashRegCode)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("HashReg address not found and sender for creation failed: %v", err)
|
||||||
|
}
|
||||||
|
glog.V(logger.Detail).Infof("created HashRegAddr @ %v\n", HashRegAddr)
|
||||||
|
} else {
|
||||||
|
glog.V(logger.Detail).Infof("HashRegAddr found at @ %v\n", HashRegAddr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register as HashReg
|
||||||
|
self.ReserveName(addr, HashRegName)
|
||||||
|
self.SetAddressToName(addr, HashRegName, common.HexToAddress(HashRegAddr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Registrar) SetUrlHint(urlhint string, addr common.Address) (err error) {
|
||||||
|
if urlhint != "" {
|
||||||
|
UrlHintAddr = urlhint
|
||||||
|
} else {
|
||||||
|
if !zero.MatchString(UrlHintAddr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nameHex, extra := encodeName(UrlHintName, 2)
|
||||||
|
urlHintAbi := resolveAbi + nameHex + extra
|
||||||
|
glog.V(logger.Detail).Infof("UrlHint address query data: %s to %s", urlHintAbi, GlobalRegistrarAddr)
|
||||||
|
UrlHintAddr, _, err = self.backend.Call("", GlobalRegistrarAddr, "", "", "", urlHintAbi)
|
||||||
|
if err != nil || zero.MatchString(UrlHintAddr) {
|
||||||
|
if (addr == common.Address{}) {
|
||||||
|
err = fmt.Errorf("UrlHint address not found and sender for creation not given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UrlHintAddr, err = self.backend.Transact(addr.Hex(), "", "", "", "210000", "", UrlHintCode)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("UrlHint address not found and sender for creation failed: %v", err)
|
||||||
|
}
|
||||||
|
glog.V(logger.Detail).Infof("created UrlHint @ %v\n", HashRegAddr)
|
||||||
|
} else {
|
||||||
|
glog.V(logger.Detail).Infof("UrlHint found @ %v\n", HashRegAddr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register as UrlHint
|
||||||
|
self.ReserveName(addr, UrlHintName)
|
||||||
|
self.SetAddressToName(addr, UrlHintName, common.HexToAddress(UrlHintAddr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReserveName(from, name) reserves name for the sender address in the globalRegistrar
|
||||||
|
// the tx needs to be mined to take effect
|
||||||
|
func (self *Registrar) ReserveName(address common.Address, name string) (txh string, err error) {
|
||||||
|
nameHex, extra := encodeName(name, 2)
|
||||||
|
abi := reserveAbi + nameHex + extra
|
||||||
|
glog.V(logger.Detail).Infof("Reserve data: %s", abi)
|
||||||
|
return self.backend.Transact(
|
||||||
|
address.Hex(),
|
||||||
|
GlobalRegistrarAddr,
|
||||||
|
"", "", "", "",
|
||||||
|
abi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAddressToName(from, name, addr) will set the Address to address for name
|
||||||
|
// in the globalRegistrar using from as the sender of the transaction
|
||||||
|
// the tx needs to be mined to take effect
|
||||||
|
func (self *Registrar) SetAddressToName(from common.Address, name string, address common.Address) (txh string, err error) {
|
||||||
|
nameHex, extra := encodeName(name, 6)
|
||||||
|
addrHex := encodeAddress(address)
|
||||||
|
|
||||||
|
abi := registerAbi + nameHex + addrHex + trueHex + extra
|
||||||
|
glog.V(logger.Detail).Infof("SetAddressToName data: %s to %s ", abi, GlobalRegistrarAddr)
|
||||||
|
|
||||||
|
return self.backend.Transact(
|
||||||
|
from.Hex(),
|
||||||
|
GlobalRegistrarAddr,
|
||||||
|
"", "", "", "",
|
||||||
|
abi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameToAddr(from, name) queries the registrar for the address on name
|
||||||
|
func (self *Registrar) NameToAddr(from common.Address, name string) (address common.Address, err error) {
|
||||||
|
nameHex, extra := encodeName(name, 2)
|
||||||
|
abi := resolveAbi + nameHex + extra
|
||||||
|
glog.V(logger.Detail).Infof("NameToAddr data: %s", abi)
|
||||||
|
res, _, err := self.backend.Call(
|
||||||
|
from.Hex(),
|
||||||
|
GlobalRegistrarAddr,
|
||||||
|
"", "", "",
|
||||||
|
abi,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
address = common.HexToAddress(res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// called as first step in the registration process on HashReg
|
||||||
|
func (self *Registrar) SetOwner(address common.Address) (txh string, err error) {
|
||||||
|
return self.backend.Transact(
|
||||||
|
address.Hex(),
|
||||||
|
HashRegAddr,
|
||||||
|
"", "", "", "",
|
||||||
|
setOwnerAbi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registers some content hash to a key/code hash
|
||||||
|
// e.g., the contract Info combined Json Doc's ContentHash
|
||||||
|
// to CodeHash of a contract or hash of a domain
|
||||||
|
func (self *Registrar) SetHashToHash(address common.Address, codehash, dochash common.Hash) (txh string, err error) {
|
||||||
|
_, err = self.SetOwner(address)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
codehex := common.Bytes2Hex(codehash[:])
|
||||||
|
dochex := common.Bytes2Hex(dochash[:])
|
||||||
|
|
||||||
|
data := registerContentHashAbi + codehex + dochex
|
||||||
|
glog.V(logger.Detail).Infof("SetHashToHash data: %s sent to %v\n", data, HashRegAddr)
|
||||||
|
return self.backend.Transact(
|
||||||
|
address.Hex(),
|
||||||
|
HashRegAddr,
|
||||||
|
"", "", "", "",
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUrlToHash(from, hash, url) registers a url to a content hash so that the content can be fetched
|
||||||
|
// address is used as sender for the transaction and will be the owner of a new
|
||||||
|
// registry entry on first time use
|
||||||
|
// FIXME: silently doing nothing if sender is not the owner
|
||||||
|
// note that with content addressed storage, this step is no longer necessary
|
||||||
|
func (self *Registrar) SetUrlToHash(address common.Address, hash common.Hash, url string) (txh string, err error) {
|
||||||
|
hashHex := common.Bytes2Hex(hash[:])
|
||||||
|
var urlHex string
|
||||||
|
urlb := []byte(url)
|
||||||
|
var cnt byte
|
||||||
|
n := len(urlb)
|
||||||
|
|
||||||
|
for n > 0 {
|
||||||
|
if n > 32 {
|
||||||
|
n = 32
|
||||||
|
}
|
||||||
|
urlHex = common.Bytes2Hex(urlb[:n])
|
||||||
|
urlb = urlb[n:]
|
||||||
|
n = len(urlb)
|
||||||
|
bcnt := make([]byte, 32)
|
||||||
|
bcnt[31] = cnt
|
||||||
|
data := registerUrlAbi +
|
||||||
|
hashHex +
|
||||||
|
common.Bytes2Hex(bcnt) +
|
||||||
|
common.Bytes2Hex(common.Hex2BytesFixed(urlHex, 32))
|
||||||
|
txh, err = self.backend.Transact(
|
||||||
|
address.Hex(),
|
||||||
|
UrlHintAddr,
|
||||||
|
"", "", "", "",
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cnt++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashToHash(key) resolves contenthash for key (a hash) using HashReg
|
||||||
|
// resolution is costless non-transactional
|
||||||
|
// implemented as direct retrieval from db
|
||||||
|
func (self *Registrar) HashToHash(khash common.Hash) (chash common.Hash, err error) {
|
||||||
|
// look up in hashReg
|
||||||
|
at := HashRegAddr[2:]
|
||||||
|
key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:]))
|
||||||
|
hash := self.backend.StorageAt(at, key)
|
||||||
|
|
||||||
|
if hash == "0x0" || len(hash) < 3 {
|
||||||
|
err = fmt.Errorf("content hash not found for '%v'", khash.Hex())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(chash[:], common.Hex2BytesFixed(hash[2:], 32))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashToUrl(contenthash) resolves the url for contenthash using UrlHint
|
||||||
|
// resolution is costless non-transactional
|
||||||
|
// implemented as direct retrieval from db
|
||||||
|
// if we use content addressed storage, this step is no longer necessary
|
||||||
|
func (self *Registrar) HashToUrl(chash common.Hash) (uri string, err error) {
|
||||||
|
// look up in URL reg
|
||||||
|
var str string = " "
|
||||||
|
var idx uint32
|
||||||
|
for len(str) > 0 {
|
||||||
|
mapaddr := storageMapping(storageIdx2Addr(1), chash[:])
|
||||||
|
key := storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(idx)))
|
||||||
|
hex := self.backend.StorageAt(UrlHintAddr[2:], key)
|
||||||
|
str = string(common.Hex2Bytes(hex[2:]))
|
||||||
|
l := len(str)
|
||||||
|
for (l > 0) && (str[l-1] == 0) {
|
||||||
|
l--
|
||||||
|
}
|
||||||
|
str = str[:l]
|
||||||
|
uri = uri + str
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uri) == 0 {
|
||||||
|
err = fmt.Errorf("GetURLhint: URL hint not found for '%v'", chash.Hex())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageIdx2Addr(varidx uint32) []byte {
|
||||||
|
data := make([]byte, 32)
|
||||||
|
binary.BigEndian.PutUint32(data[28:32], varidx)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageMapping(addr, key []byte) []byte {
|
||||||
|
data := make([]byte, 64)
|
||||||
|
copy(data[0:32], key[0:32])
|
||||||
|
copy(data[32:64], addr[0:32])
|
||||||
|
sha := crypto.Sha3(data)
|
||||||
|
return sha
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageFixedArray(addr, idx []byte) []byte {
|
||||||
|
var carry byte
|
||||||
|
for i := 31; i >= 0; i-- {
|
||||||
|
var b byte = addr[i] + idx[i] + carry
|
||||||
|
if b < addr[i] {
|
||||||
|
carry = 1
|
||||||
|
} else {
|
||||||
|
carry = 0
|
||||||
|
}
|
||||||
|
addr[i] = b
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageAddress(addr []byte) string {
|
||||||
|
return common.ToHex(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeAddress(address common.Address) string {
|
||||||
|
return addressAbiPrefix + address.Hex()[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeName(name string, index uint8) (string, string) {
|
||||||
|
extra := common.Bytes2Hex([]byte(name))
|
||||||
|
if len(name) > 32 {
|
||||||
|
return fmt.Sprintf("%064x", index), extra
|
||||||
|
}
|
||||||
|
return extra + falseHex[len(extra):], ""
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package resolver
|
package registrar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -20,22 +20,22 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTestBackend() *testBackend {
|
func NewTestBackend() *testBackend {
|
||||||
HashRegContractAddress = common.BigToAddress(common.Big0).Hex()[2:]
|
HashRegAddr = common.BigToAddress(common.Big0).Hex() //[2:]
|
||||||
UrlHintContractAddress = common.BigToAddress(common.Big1).Hex()[2:]
|
UrlHintAddr = common.BigToAddress(common.Big1).Hex() //[2:]
|
||||||
self := &testBackend{}
|
self := &testBackend{}
|
||||||
self.contracts = make(map[string](map[string]string))
|
self.contracts = make(map[string](map[string]string))
|
||||||
|
|
||||||
self.contracts[HashRegContractAddress] = make(map[string]string)
|
self.contracts[HashRegAddr[2:]] = make(map[string]string)
|
||||||
key := storageAddress(storageMapping(storageIdx2Addr(1), codehash[:]))
|
key := storageAddress(storageMapping(storageIdx2Addr(1), codehash[:]))
|
||||||
self.contracts[HashRegContractAddress][key] = hash.Hex()
|
self.contracts[HashRegAddr[2:]][key] = hash.Hex()
|
||||||
|
|
||||||
self.contracts[UrlHintContractAddress] = make(map[string]string)
|
self.contracts[UrlHintAddr[2:]] = make(map[string]string)
|
||||||
mapaddr := storageMapping(storageIdx2Addr(1), hash[:])
|
mapaddr := storageMapping(storageIdx2Addr(1), hash[:])
|
||||||
|
|
||||||
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(0)))
|
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(0)))
|
||||||
self.contracts[UrlHintContractAddress][key] = common.ToHex([]byte(url))
|
self.contracts[UrlHintAddr[2:]][key] = common.ToHex([]byte(url))
|
||||||
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(1)))
|
key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(1)))
|
||||||
self.contracts[UrlHintContractAddress][key] = "0x00"
|
self.contracts[UrlHintAddr[2:]][key] = "0x0"
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,11 +52,25 @@ func (self *testBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, ga
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyToContentHash(t *testing.T) {
|
func (self *testBackend) Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, string, error) {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetGlobalRegistrar(t *testing.T) {
|
||||||
b := NewTestBackend()
|
b := NewTestBackend()
|
||||||
res := New(b)
|
res := New(b)
|
||||||
|
err := res.SetGlobalRegistrar("addresshex", common.BigToAddress(common.Big1))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v'", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
got, err := res.KeyToContentHash(codehash)
|
func TestHashToHash(t *testing.T) {
|
||||||
|
b := NewTestBackend()
|
||||||
|
res := New(b)
|
||||||
|
// res.SetHashReg()
|
||||||
|
|
||||||
|
got, err := res.HashToHash(codehash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error, got %v", err)
|
t.Errorf("expected no error, got %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -66,28 +80,17 @@ func TestKeyToContentHash(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContentHashToUrl(t *testing.T) {
|
func TestHashToUrl(t *testing.T) {
|
||||||
b := NewTestBackend()
|
b := NewTestBackend()
|
||||||
res := New(b)
|
res := New(b)
|
||||||
got, err := res.ContentHashToUrl(hash)
|
// res.SetUrlHint()
|
||||||
|
|
||||||
|
got, err := res.HashToUrl(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error, got %v", err)
|
t.Errorf("expected error, got %v", err)
|
||||||
} else {
|
} else {
|
||||||
if got != url {
|
if got != url {
|
||||||
t.Errorf("incorrect result, expected '%v', got '%s'", url, got)
|
t.Errorf("incorrect result, expected '%v', got '%s'", url, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyToUrl(t *testing.T) {
|
|
||||||
b := NewTestBackend()
|
|
||||||
res := New(b)
|
|
||||||
got, _, err := res.KeyToUrl(codehash)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error, got %v", err)
|
|
||||||
} else {
|
|
||||||
if got != url {
|
|
||||||
t.Errorf("incorrect result, expected \n'%s', got \n'%s'", url, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
const ( // built-in contracts address and code
|
|
||||||
ContractCodeURLhint = "0x60c180600c6000396000f30060003560e060020a90048063300a3bbf14601557005b6024600435602435604435602a565b60006000f35b6000600084815260200190815260200160002054600160a060020a0316600014806078575033600160a060020a03166000600085815260200190815260200160002054600160a060020a0316145b607f5760bc565b336000600085815260200190815260200160002081905550806001600085815260200190815260200160002083610100811060b657005b01819055505b50505056"
|
|
||||||
/*
|
|
||||||
contract URLhint {
|
|
||||||
function register(uint256 _hash, uint8 idx, uint256 _url) {
|
|
||||||
if (owner[_hash] == 0 || owner[_hash] == msg.sender) {
|
|
||||||
owner[_hash] = msg.sender;
|
|
||||||
url[_hash][idx] = _url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapping (uint256 => address) owner;
|
|
||||||
mapping (uint256 => uint256[256]) url;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
ContractCodeHashReg = "0x609880600c6000396000f30060003560e060020a9004806331e12c2014601f578063d66d6c1014602b57005b6025603d565b60006000f35b6037600435602435605d565b60006000f35b600054600160a060020a0316600014605357605b565b336000819055505b565b600054600160a060020a031633600160a060020a031614607b576094565b8060016000848152602001908152602001600020819055505b505056"
|
|
||||||
/*
|
|
||||||
contract HashReg {
|
|
||||||
function setowner() {
|
|
||||||
if (owner == 0) {
|
|
||||||
owner = msg.sender;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function register(uint256 _key, uint256 _content) {
|
|
||||||
if (msg.sender == owner) {
|
|
||||||
content[_key] = _content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
address owner;
|
|
||||||
mapping (uint256 => uint256) content;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/logger"
|
|
||||||
"github.com/ethereum/go-ethereum/logger/glog"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Resolver implements the Ethereum DNS mapping
|
|
||||||
HashReg : Key Hash (hash of domain name or contract code) -> Content Hash
|
|
||||||
UrlHint : Content Hash -> Url Hint
|
|
||||||
|
|
||||||
The resolver is meant to be called by the roundtripper transport implementation
|
|
||||||
of a url scheme
|
|
||||||
*/
|
|
||||||
|
|
||||||
// // contract addresses will be hardcoded after they're created
|
|
||||||
var UrlHintContractAddress, HashRegContractAddress string
|
|
||||||
|
|
||||||
const (
|
|
||||||
txValue = "0"
|
|
||||||
txGas = "100000"
|
|
||||||
txGasPrice = "1000000000000"
|
|
||||||
)
|
|
||||||
|
|
||||||
func abi(s string) string {
|
|
||||||
return common.ToHex(crypto.Sha3([]byte(s))[:4])
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
registerContentHashAbi = abi("register(uint256,uint256)")
|
|
||||||
registerUrlAbi = abi("register(uint256,uint8,uint256)")
|
|
||||||
setOwnerAbi = abi("setowner()")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Backend interface {
|
|
||||||
StorageAt(string, string) string
|
|
||||||
Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resolver struct {
|
|
||||||
backend Backend
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(eth Backend) *Resolver {
|
|
||||||
return &Resolver{eth}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for testing and play temporarily
|
|
||||||
// ideally the HashReg and UrlHint contracts should be in the genesis block
|
|
||||||
// if we got build-in support for natspec/contract info
|
|
||||||
// there should be only one of these officially endorsed
|
|
||||||
// addresses as constants
|
|
||||||
// TODO: could get around this with namereg, check
|
|
||||||
func (self *Resolver) CreateContracts(addr common.Address) (err error) {
|
|
||||||
HashRegContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeHashReg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
UrlHintContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeURLhint)
|
|
||||||
glog.V(logger.Detail).Infof("HashReg @ %v\nUrlHint @ %v\n", HashRegContractAddress, UrlHintContractAddress)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// called as first step in the registration process on HashReg
|
|
||||||
func (self *Resolver) SetOwner(address common.Address) (txh string, err error) {
|
|
||||||
return self.backend.Transact(
|
|
||||||
address.Hex(),
|
|
||||||
HashRegContractAddress,
|
|
||||||
"", txValue, txGas, txGasPrice,
|
|
||||||
setOwnerAbi,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// registers some content hash to a key/code hash
|
|
||||||
// e.g., the contract Info combined Json Doc's ContentHash
|
|
||||||
// to CodeHash of a contract or hash of a domain
|
|
||||||
// kept
|
|
||||||
func (self *Resolver) RegisterContentHash(address common.Address, codehash, dochash common.Hash) (txh string, err error) {
|
|
||||||
_, err = self.SetOwner(address)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
codehex := common.Bytes2Hex(codehash[:])
|
|
||||||
dochex := common.Bytes2Hex(dochash[:])
|
|
||||||
|
|
||||||
data := registerContentHashAbi + codehex + dochex
|
|
||||||
return self.backend.Transact(
|
|
||||||
address.Hex(),
|
|
||||||
HashRegContractAddress,
|
|
||||||
"", txValue, txGas, txGasPrice,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// registers a url to a content hash so that the content can be fetched
|
|
||||||
// address is used as sender for the transaction and will be the owner of a new
|
|
||||||
// registry entry on first time use
|
|
||||||
// FIXME: silently doing nothing if sender is not the owner
|
|
||||||
// note that with content addressed storage, this step is no longer necessary
|
|
||||||
// it could be purely
|
|
||||||
func (self *Resolver) RegisterUrl(address common.Address, hash common.Hash, url string) (txh string, err error) {
|
|
||||||
hashHex := common.Bytes2Hex(hash[:])
|
|
||||||
var urlHex string
|
|
||||||
urlb := []byte(url)
|
|
||||||
var cnt byte
|
|
||||||
n := len(urlb)
|
|
||||||
|
|
||||||
for n > 0 {
|
|
||||||
if n > 32 {
|
|
||||||
n = 32
|
|
||||||
}
|
|
||||||
urlHex = common.Bytes2Hex(urlb[:n])
|
|
||||||
urlb = urlb[n:]
|
|
||||||
n = len(urlb)
|
|
||||||
bcnt := make([]byte, 32)
|
|
||||||
bcnt[31] = cnt
|
|
||||||
data := registerUrlAbi +
|
|
||||||
hashHex +
|
|
||||||
common.Bytes2Hex(bcnt) +
|
|
||||||
common.Bytes2Hex(common.Hex2BytesFixed(urlHex, 32))
|
|
||||||
txh, err = self.backend.Transact(
|
|
||||||
address.Hex(),
|
|
||||||
UrlHintContractAddress,
|
|
||||||
"", txValue, txGas, txGasPrice,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cnt++
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Resolver) Register(address common.Address, codehash, dochash common.Hash, url string) (txh string, err error) {
|
|
||||||
|
|
||||||
_, err = self.RegisterContentHash(address, codehash, dochash)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return self.RegisterUrl(address, dochash, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolution is costless non-transactional
|
|
||||||
// implemented as direct retrieval from db
|
|
||||||
func (self *Resolver) KeyToContentHash(khash common.Hash) (chash common.Hash, err error) {
|
|
||||||
// look up in hashReg
|
|
||||||
at := common.Bytes2Hex(common.FromHex(HashRegContractAddress))
|
|
||||||
key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:]))
|
|
||||||
hash := self.backend.StorageAt(at, key)
|
|
||||||
|
|
||||||
if hash == "0x0" || len(hash) < 3 {
|
|
||||||
err = fmt.Errorf("content hash not found for '%v'", khash.Hex())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
copy(chash[:], common.Hex2BytesFixed(hash[2:], 32))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieves the url-hint for the content hash -
|
|
||||||
// if we use content addressed storage, this step is no longer necessary
|
|
||||||
func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error) {
|
|
||||||
// look up in URL reg
|
|
||||||
var str string = " "
|
|
||||||
var idx uint32
|
|
||||||
for len(str) > 0 {
|
|
||||||
mapaddr := storageMapping(storageIdx2Addr(1), chash[:])
|
|
||||||
key := storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(idx)))
|
|
||||||
hex := self.backend.StorageAt(UrlHintContractAddress, key)
|
|
||||||
str = string(common.Hex2Bytes(hex[2:]))
|
|
||||||
l := len(str)
|
|
||||||
for (l > 0) && (str[l-1] == 0) {
|
|
||||||
l--
|
|
||||||
}
|
|
||||||
str = str[:l]
|
|
||||||
uri = uri + str
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(uri) == 0 {
|
|
||||||
err = fmt.Errorf("GetURLhint: URL hint not found for '%v'", chash.Hex())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *Resolver) KeyToUrl(key common.Hash) (uri string, hash common.Hash, err error) {
|
|
||||||
// look up in urlHint
|
|
||||||
hash, err = self.KeyToContentHash(key)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uri, err = self.ContentHashToUrl(hash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageIdx2Addr(varidx uint32) []byte {
|
|
||||||
data := make([]byte, 32)
|
|
||||||
binary.BigEndian.PutUint32(data[28:32], varidx)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageMapping(addr, key []byte) []byte {
|
|
||||||
data := make([]byte, 64)
|
|
||||||
copy(data[0:32], key[0:32])
|
|
||||||
copy(data[32:64], addr[0:32])
|
|
||||||
sha := crypto.Sha3(data)
|
|
||||||
return sha
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageFixedArray(addr, idx []byte) []byte {
|
|
||||||
var carry byte
|
|
||||||
for i := 31; i >= 0; i-- {
|
|
||||||
var b byte = addr[i] + idx[i] + carry
|
|
||||||
if b < addr[i] {
|
|
||||||
carry = 1
|
|
||||||
} else {
|
|
||||||
carry = 0
|
|
||||||
}
|
|
||||||
addr[i] = b
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageAddress(addr []byte) string {
|
|
||||||
return common.ToHex(addr)
|
|
||||||
}
|
|
||||||
50
core/contracts.go
Normal file
50
core/contracts.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
const ( // built-in contracts address and code
|
||||||
|
ContractAddrURLhint = "0000000000000000000000000000000000000008"
|
||||||
|
//ContractCodeURLhint = "0x60b180600c6000396000f30060003560e060020a90048063d66d6c1014601557005b60216004356024356027565b60006000f35b6000600083815260200190815260200160002054600160a060020a0316600014806075575033600160a060020a03166000600084815260200190815260200160002054600160a060020a0316145b607c5760ad565b3360006000848152602001908152602001600020819055508060016000848152602001908152602001600020819055505b505056"
|
||||||
|
ContractCodeURLhint = "0x60003560e060020a90048063d66d6c1014601557005b60216004356024356027565b60006000f35b6000600083815260200190815260200160002054600160a060020a0316600014806075575033600160a060020a03166000600084815260200190815260200160002054600160a060020a0316145b607c5760ad565b3360006000848152602001908152602001600020819055508060016000848152602001908152602001600020819055505b505056"
|
||||||
|
/*
|
||||||
|
contract URLhint {
|
||||||
|
function register(uint256 _hash, uint256 _url) {
|
||||||
|
if (owner[_hash] == 0 || owner[_hash] == msg.sender) {
|
||||||
|
owner[_hash] = msg.sender;
|
||||||
|
url[_hash] = _url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapping (uint256 => address) owner;
|
||||||
|
mapping (uint256 => uint256) url;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
ContractAddrHashReg = "0000000000000000000000000000000000000009"
|
||||||
|
ContractCodeHashReg = "0x60003560e060020a9004806331e12c2014601f578063d66d6c1014602b57005b6025603d565b60006000f35b6037600435602435605d565b60006000f35b600054600160a060020a0316600014605357605b565b336000819055505b565b600054600160a060020a031633600160a060020a031614607b576094565b8060016000848152602001908152602001600020819055505b505056"
|
||||||
|
//ContractCodeHashReg = "0x609880600c6000396000f30060003560e060020a9004806331e12c2014601f578063d66d6c1014602b57005b6025603d565b60006000f35b6037600435602435605d565b60006000f35b600054600160a060020a0316600014605357605b565b336000819055505b565b600054600160a060020a031633600160a060020a031614607b576094565b8060016000848152602001908152602001600020819055505b505056"
|
||||||
|
/*
|
||||||
|
contract HashReg {
|
||||||
|
function setowner() {
|
||||||
|
if (owner == 0) {
|
||||||
|
owner = msg.sender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function register(uint256 _key, uint256 _content) {
|
||||||
|
if (msg.sender == owner) {
|
||||||
|
content[_key] = _content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address owner;
|
||||||
|
mapping (uint256 => uint256) content;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
ContractAddrSwarm = "000000000000000000000000000000000000000a"
|
||||||
|
ContractCodeSwarm = "0x60003560e060020a900480633ccfd60b1461002c5780636d5433e61461003a5780638843ffba1461005257005b6100346100f0565b60006000f35b61004860043560243561014e565b8060005260206000f35b61005d600435610063565b60006000f35b60006000600033600160a060020a0316815260200190815260200160002090504282420111610091576100aa565b6100a1816001015483420161014e565b81600101819055505b348181815401915081905550806000600033600160a060020a03168152602001908152602001600020600082810154828201555060018281015482820155509050505050565b60006000600033600160a060020a031681526020019081526020016000209050806001015442116101205761014b565b33600160a060020a0316600082546000600060006000848787f161014057005b505050600081819055505b50565b60008183101561015d57610165565b829050610169565b8190505b9291505056"
|
||||||
|
//"0x60003560e060020a900480633ccfd60b1461002c5780636d5433e61461003a5780638843ffba1461005257005b6100346100db565b60006000f35b610048600435602435610063565b8060005260206000f35b61005d600435610084565b60006000f35b6000818310156100725761007a565b82905061007e565b8190505b92915050565b60006000600033600160a060020a03168152602001908152602001600020905042824201116100b2576100cb565b6100c28160010154834201610063565b81600101819055505b3481818154019150819055505050565b60006000600033600160a060020a0316815260200190815260200160002090508060010154421161010b57610136565b33600160a060020a0316600082546000600060006000848787f161012b57005b505050600081819055505b5056"
|
||||||
|
// see bzz/bzzcontract/swarm.sol
|
||||||
|
|
||||||
|
BuiltInContracts = `
|
||||||
|
"` + ContractAddrURLhint + `": {"balance": "0", "code": "` + ContractCodeURLhint + `" },
|
||||||
|
"` + ContractAddrHashReg + `": {"balance": "0", "code": "` + ContractCodeHashReg + `" },
|
||||||
|
"` + ContractAddrSwarm + `": {"balance": "0", "code": "` + ContractCodeSwarm + `" },
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
@ -57,7 +57,14 @@ func GenesisBlock(db common.Database) *types.Block {
|
||||||
return genesis
|
return genesis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestAccount = "e273f01c99144c438695e10f24926dc1f9fbf62d"
|
||||||
|
TestBalance = "1000000000000"
|
||||||
|
)
|
||||||
|
|
||||||
var GenesisAccounts = []byte(`{
|
var GenesisAccounts = []byte(`{
|
||||||
|
"` + TestAccount + `": {"balance": "` + TestBalance + `"},
|
||||||
|
` + BuiltInContracts + `
|
||||||
"0000000000000000000000000000000000000001": {"balance": "1"},
|
"0000000000000000000000000000000000000001": {"balance": "1"},
|
||||||
"0000000000000000000000000000000000000002": {"balance": "1"},
|
"0000000000000000000000000000000000000002": {"balance": "1"},
|
||||||
"0000000000000000000000000000000000000003": {"balance": "1"},
|
"0000000000000000000000000000000000000003": {"balance": "1"},
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/ethash"
|
"github.com/ethereum/ethash"
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/bzz"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/compiler"
|
"github.com/ethereum/go-ethereum/common/compiler"
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
|
@ -85,6 +86,9 @@ type Config struct {
|
||||||
NAT nat.Interface
|
NAT nat.Interface
|
||||||
Shh bool
|
Shh bool
|
||||||
Dial bool
|
Dial bool
|
||||||
|
Bzz bool
|
||||||
|
BzzPort string
|
||||||
|
PowTest bool
|
||||||
|
|
||||||
Etherbase string
|
Etherbase string
|
||||||
GasPrice *big.Int
|
GasPrice *big.Int
|
||||||
|
|
@ -193,6 +197,7 @@ type Ethereum struct {
|
||||||
pow *ethash.Ethash
|
pow *ethash.Ethash
|
||||||
protocolManager *ProtocolManager
|
protocolManager *ProtocolManager
|
||||||
downloader *downloader.Downloader
|
downloader *downloader.Downloader
|
||||||
|
Swarm *bzz.Api
|
||||||
SolcPath string
|
SolcPath string
|
||||||
solc *compiler.Solidity
|
solc *compiler.Solidity
|
||||||
|
|
||||||
|
|
@ -283,7 +288,15 @@ func New(config *Config) (*Ethereum, error) {
|
||||||
AutoDAG: config.AutoDAG,
|
AutoDAG: config.AutoDAG,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.PowTest {
|
||||||
|
glog.V(logger.Info).Infof("ethash used in test mode")
|
||||||
|
eth.pow, err = ethash.NewForTesting()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
eth.pow = ethash.New()
|
eth.pow = ethash.New()
|
||||||
|
}
|
||||||
eth.chainManager = core.NewChainManager(blockDb, stateDb, eth.pow, eth.EventMux())
|
eth.chainManager = core.NewChainManager(blockDb, stateDb, eth.pow, eth.EventMux())
|
||||||
eth.downloader = downloader.New(eth.EventMux(), eth.chainManager.HasBlock, eth.chainManager.GetBlock)
|
eth.downloader = downloader.New(eth.EventMux(), eth.chainManager.HasBlock, eth.chainManager.GetBlock)
|
||||||
eth.txPool = core.NewTxPool(eth.EventMux(), eth.chainManager.State, eth.chainManager.GasLimit)
|
eth.txPool = core.NewTxPool(eth.EventMux(), eth.chainManager.State, eth.chainManager.GasLimit)
|
||||||
|
|
@ -302,7 +315,25 @@ func New(config *Config) (*Ethereum, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
protocols := []p2p.Protocol{eth.protocolManager.SubProtocol}
|
protocols := []p2p.Protocol{eth.protocolManager.SubProtocol}
|
||||||
|
|
||||||
|
if config.Bzz {
|
||||||
|
eth.Swarm, err = bzz.NewApi(config.DataDir, config.BzzPort)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(logger.Warn).Infof("BZZ: error creating swarm: %v. Protocol skipped", err)
|
||||||
|
} else {
|
||||||
|
var proto p2p.Protocol
|
||||||
|
proto, err = eth.Swarm.Bzz()
|
||||||
|
if err != nil {
|
||||||
|
glog.V(logger.Warn).Infof("BZZ: error creating swarm: %v. Protocol skipped", err)
|
||||||
|
eth.Swarm = nil
|
||||||
|
} else {
|
||||||
|
protocols = append(protocols, proto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if config.Shh {
|
if config.Shh {
|
||||||
protocols = append(protocols, eth.whisper.Protocol())
|
protocols = append(protocols, eth.whisper.Protocol())
|
||||||
}
|
}
|
||||||
|
|
@ -324,6 +355,8 @@ func New(config *Config) (*Ethereum, error) {
|
||||||
eth.net.ListenAddr = ":" + config.Port
|
eth.net.ListenAddr = ":" + config.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eth.net.Protocols = protocols
|
||||||
|
|
||||||
vm.Debug = config.VmDebug
|
vm.Debug = config.VmDebug
|
||||||
|
|
||||||
return eth, nil
|
return eth, nil
|
||||||
|
|
@ -450,6 +483,7 @@ func (s *Ethereum) Start() error {
|
||||||
ClientString: s.net.Name,
|
ClientString: s.net.Name,
|
||||||
ProtocolVersion: ProtocolVersion,
|
ProtocolVersion: ProtocolVersion,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := s.net.Start()
|
err := s.net.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -469,6 +503,10 @@ func (s *Ethereum) Start() error {
|
||||||
s.whisper.Start()
|
s.whisper.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Swarm != nil {
|
||||||
|
s.Swarm.Start(s.net.Self(), s.AddPeer)
|
||||||
|
}
|
||||||
|
|
||||||
glog.V(logger.Info).Infoln("Server started")
|
glog.V(logger.Info).Infoln("Server started")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -536,6 +574,11 @@ func (s *Ethereum) Stop() {
|
||||||
}
|
}
|
||||||
s.StopAutoDAG()
|
s.StopAutoDAG()
|
||||||
|
|
||||||
|
if s.Swarm != nil {
|
||||||
|
s.Swarm.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.V(logger.Info).Infoln("Server stopped")
|
||||||
close(s.shutdownChan)
|
close(s.shutdownChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ProtocolVersion = 60
|
ProtocolVersion = 60
|
||||||
NetworkId = 0
|
NetworkId = 100
|
||||||
ProtocolLength = uint64(8)
|
ProtocolLength = uint64(8)
|
||||||
ProtocolMaxMsgSize = 10 * 1024 * 1024
|
ProtocolMaxMsgSize = 10 * 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ func (self *LDBDatabase) Flush() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *LDBDatabase) Write(batch *leveldb.Batch) error {
|
||||||
|
return self.db.Write(batch, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (self *LDBDatabase) Close() {
|
func (self *LDBDatabase) Close() {
|
||||||
if err := self.Flush(); err != nil {
|
if err := self.Flush(); err != nil {
|
||||||
glog.V(logger.Error).Infof("error: flush '%s': %v\n", self.fn, err)
|
glog.V(logger.Error).Infof("error: flush '%s': %v\n", self.fn, err)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ func newNode(id NodeID, ip net.IP, udpPort, tcpPort uint16) *Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewNode(id NodeID, ip net.IP, udpPort, tcpPort uint16) *Node {
|
||||||
|
return newNode(id, ip, udpPort, tcpPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) Sha() common.Hash {
|
||||||
|
return n.sha
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Node) addr() *net.UDPAddr {
|
func (n *Node) addr() *net.UDPAddr {
|
||||||
return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)}
|
return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/huin/goupnp/dcps/internetgateway2"
|
"github.com/huin/goupnp/dcps/internetgateway2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const soapRequestTimeout = 3 * time.Second
|
||||||
|
|
||||||
type upnp struct {
|
type upnp struct {
|
||||||
dev *goupnp.RootDevice
|
dev *goupnp.RootDevice
|
||||||
service string
|
service string
|
||||||
|
|
@ -131,6 +133,7 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
|
||||||
}
|
}
|
||||||
// check for a matching IGD service
|
// check for a matching IGD service
|
||||||
sc := goupnp.ServiceClient{service.NewSOAPClient(), devs[i].Root, service}
|
sc := goupnp.ServiceClient{service.NewSOAPClient(), devs[i].Root, service}
|
||||||
|
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
|
||||||
upnp := matcher(devs[i].Root, sc)
|
upnp := matcher(devs[i].Root, sc)
|
||||||
if upnp == nil {
|
if upnp == nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@ func (e *encodableReader) Read(b []byte) (int, error) {
|
||||||
panic("called")
|
panic("called")
|
||||||
}
|
}
|
||||||
|
|
||||||
type namedByteType byte
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ = Encoder(&testEncoder{})
|
_ = Encoder(&testEncoder{})
|
||||||
_ = Encoder(byteEncoder(0))
|
_ = Encoder(byteEncoder(0))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue