Merge remote-tracking branch 'shpere/bzz' into bzz

This commit is contained in:
obscuren 2015-06-05 14:10:48 +02:00
commit 8d860bb2d4
62 changed files with 7555 additions and 509 deletions

2
Godeps/Godeps.json generated
View file

@ -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",

View file

@ -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
View 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
View 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)
}

View 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(&eth.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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 &lt; %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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}
}

View file

@ -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

View file

@ -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"
)

View file

@ -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
View 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
}
}

View file

@ -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(&eth.Config{ conf := &eth.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 {

View file

@ -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,
) )

View file

@ -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 {

View file

@ -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)
} }

View file

@ -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)),

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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)
} }
return
func (self *DocServer) HasScheme(scheme string) bool {
for _, s := range self.schemes {
if s == scheme {
return true
}
}
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
}

View file

@ -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
View 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)
}

View 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)
}

View file

@ -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
} }

View file

@ -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)
} }

File diff suppressed because one or more lines are too long

View 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)
}

View 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):], ""
}

View file

@ -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)
}
}
}

View file

@ -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;
}
*/
)

View file

@ -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
View 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 + `" },
`
)

View file

@ -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"},

View file

@ -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)
} }

View file

@ -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
) )

View file

@ -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)

View file

@ -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)}
} }

View file

@ -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

View file

@ -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))