diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index bc049d1aa6..4cbc73defe 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -31,7 +31,7 @@ }, { "ImportPath": "github.com/huin/goupnp", - "Rev": "c57ae84388ab59076fd547f1abeab71c2edb0a21" + "Rev": "5cff77a69fb22f5f1774c4451ea2aab63d4d2f20" }, { "ImportPath": "github.com/jackpal/go-nat-pmp", diff --git a/Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go b/Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go index 8cd20c2f44..7799a32ce7 100644 --- a/Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go +++ b/Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go @@ -19,7 +19,7 @@ import ( "fmt" "net/http" "net/url" - + "time" "golang.org/x/net/html/charset" "github.com/huin/goupnp/httpu" @@ -64,7 +64,6 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) { maybe := &results[i] loc, err := response.Location() if err != nil { - maybe.Err = ContextError{"unexpected bad location from search", err} continue } @@ -93,7 +92,11 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, 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 { return err } diff --git a/bzz/api.go b/bzz/api.go new file mode 100644 index 0000000000..f7d7e1e7fb --- /dev/null +++ b/bzz/api.go @@ -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 +} diff --git a/bzz/api_test.go b/bzz/api_test.go new file mode 100644 index 0000000000..a7bca204f1 --- /dev/null +++ b/bzz/api_test.go @@ -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) +} diff --git a/bzz/bzzcontract/bzzcontract_test.go b/bzz/bzzcontract/bzzcontract_test.go new file mode 100644 index 0000000000..6ea4452e9c --- /dev/null +++ b/bzz/bzzcontract/bzzcontract_test.go @@ -0,0 +1,191 @@ +package bzzcontract + +import ( + "encoding/binary" + "fmt" + "io/ioutil" + "math/big" + "os" + "testing" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + //"github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/rpc" + xe "github.com/ethereum/go-ethereum/xeth" +) + +type testFrontend struct { + t *testing.T + ethereum *eth.Ethereum + xeth *xe.XEth + api *rpc.EthereumApi + coinbase string +} + +func (f *testFrontend) UnlockAccount(acc []byte) bool { + f.t.Logf("Unlocking account %v\n", common.Bytes2Hex(acc)) + f.ethereum.AccountManager().Unlock(acc, "password") + return true +} + +func (f *testFrontend) ConfirmTransaction(tx *types.Transaction) bool { + return true +} + +var port = 30300 + +func testEth(t *testing.T) (ethereum *eth.Ethereum, err error) { + os.RemoveAll("/tmp/eth/") + err = os.MkdirAll("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/", os.ModePerm) + if err != nil { + t.Errorf("%v", err) + return + } + err = os.MkdirAll("/tmp/eth/data", os.ModePerm) + if err != nil { + t.Errorf("%v", err) + return + } + ks := crypto.NewKeyStorePlain("/tmp/eth/keys") + ioutil.WriteFile("/tmp/eth/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/e273f01c99144c438695e10f24926dc1f9fbf62d", + []byte(`{"Id":"RhRXD+fNRKS4jx+7ZfEsNA==","Address":"4nPwHJkUTEOGleEPJJJtwfn79i0=","PrivateKey":"h4ACVpe74uIvi5Cg/2tX/Yrm2xdr3J7QoMbMtNX2CNc="}`), os.ModePerm) + + port++ + ethereum, err = eth.New(ð.Config{ + DataDir: "/tmp/eth", + AccountManager: accounts.NewManager(ks), + Port: fmt.Sprintf("%d", port), + MaxPeers: 10, + Name: "test", + }) + + if err != nil { + t.Errorf("%v", err) + return + } + + return +} + +func testInit(t *testing.T) (self *testFrontend) { + + ethereum, err := testEth(t) + if err != nil { + t.Errorf("error creating jsre, got %v", err) + return + } + err = ethereum.Start() + if err != nil { + t.Errorf("error starting ethereum: %v", err) + return + } + + self = &testFrontend{t: t, ethereum: ethereum} + self.xeth = xe.New(ethereum, self) + self.api = rpc.NewEthereumApi(self.xeth) + + addr := self.xeth.Coinbase() + self.coinbase = addr + if addr != "0x"+core.TestAccount { + t.Errorf("CoinBase %v does not match TestAccount 0x%v", addr, core.TestAccount) + } + t.Logf("CoinBase is %v", addr) + + balance := self.xeth.BalanceAt(core.TestAccount) + t.Logf("Balance is %v", balance) + + return + +} + +func (self *testFrontend) insertTx(addr, contract, fnsig string, args []string) { + + //cb := common.HexToAddress(self.coinbase) + //coinbase := self.ethereum.ChainManager().State().GetStateObject(cb) + + hash := common.Bytes2Hex(crypto.Sha3([]byte(fnsig))) + data := "0x" + hash[0:8] + for _, arg := range args { + data = data + common.Bytes2Hex(common.Hex2BytesFixed(arg, 32)) + } + self.t.Logf("Tx data: %v", data) + + jsontx := ` +[{ + "from": "` + addr + `", + "to": "0x` + contract + `", + "value": "100000000000", + "gas": "100000", + "gasPrice": "100000", + "data": "` + data + `" +}] +` + req := &rpc.RpcRequest{ + Jsonrpc: "2.0", + Method: "eth_transact", + Params: []byte(jsontx), + Id: 6, + } + + var reply interface{} + err0 := self.api.GetRequestReply(req, &reply) + if err0 != nil { + self.t.Errorf("GetRequestReply error: %v", err0) + } + + //self.xeth.Transact(addr, contract, "100000000000", "100000", "100000", data) +} + +func (self *testFrontend) applyTxs() { + + cb := common.HexToAddress(self.coinbase) + stateDb := self.ethereum.ChainManager().State().Copy() + block := self.ethereum.ChainManager().NewBlock(cb) + coinbase := stateDb.GetStateObject(cb) + coinbase.SetGasPool(big.NewInt(1000000)) + txs := self.ethereum.TxPool().GetTransactions() + + for i := 0; i < len(txs); i++ { + for _, tx := range txs { + if tx.Nonce() == uint64(i) { + _, gas, err := core.ApplyMessage(core.NewEnv(stateDb, self.ethereum.ChainManager(), tx, block), tx, coinbase) + //self.ethereum.TxPool().RemoveSet([]*types.Transaction{tx}) + self.t.Logf("ApplyMessage: gas %v err %v", gas, err) + } + } + } + + self.ethereum.TxPool().RemoveSet(txs) + self.xeth = self.xeth.WithState(stateDb) + +} + +func storageAddress(varidx uint32, key []byte) string { + data := make([]byte, 64) + binary.BigEndian.PutUint32(data[60:64], varidx) + copy(data[0:32], key[0:32]) + return "0x" + common.Bytes2Hex(crypto.Sha3(data)) +} + +func TestSwarmContract(t *testing.T) { + + tf := testInit(t) + defer tf.ethereum.Stop() + + tf.insertTx(tf.coinbase, core.ContractAddrSwarm, "signup(uint256)", []string{"1000"}) + tf.applyTxs() + + addr := common.Hex2BytesFixed(tf.coinbase[2:], 32) + key := storageAddress(0, addr) + data := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key) + key = key[:65] + "6" + data2 := tf.xeth.StorageAt("0x"+core.ContractAddrSwarm, key) + + t.Logf("addr = %x key = %v data = %v, %v", addr, key, data, data2) + +} diff --git a/bzz/bzzcontract/swarm.sol b/bzz/bzzcontract/swarm.sol new file mode 100644 index 0000000000..0086abe7b1 --- /dev/null +++ b/bzz/bzzcontract/swarm.sol @@ -0,0 +1,161 @@ +/// @title Swarm Distributed Preimage Archive +/// @author Daniel A. Nagy +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; + } +} diff --git a/bzz/bzzhash/bzzhash.go b/bzz/bzzhash/bzzhash.go new file mode 100644 index 0000000000..861decf192 --- /dev/null +++ b/bzz/bzzhash/bzzhash.go @@ -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 ") + 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) + } +} diff --git a/bzz/bzzup/bzzhandler.html b/bzz/bzzup/bzzhandler.html new file mode 100644 index 0000000000..0d7a8b01a4 --- /dev/null +++ b/bzz/bzzup/bzzhandler.html @@ -0,0 +1,15 @@ + + + + Register Swarm protocol handler + + + +

Register Swarm protocol handler

+

This web page will install a web protocol handler for the bzz: protocol.

+ + diff --git a/bzz/bzzup/bzzup.sh b/bzz/bzzup/bzzup.sh new file mode 100755 index 0000000000..aaf6b15078 --- /dev/null +++ b/bzz/bzzup/bzzup.sh @@ -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 diff --git a/bzz/chunkIO.go b/bzz/chunkIO.go new file mode 100644 index 0000000000..ab0a031a1f --- /dev/null +++ b/bzz/chunkIO.go @@ -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 +} diff --git a/bzz/chunker.go b/bzz/chunker.go new file mode 100644 index 0000000000..db0520536b --- /dev/null +++ b/bzz/chunker.go @@ -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() +} diff --git a/bzz/chunker_test.go b/bzz/chunker_test.go new file mode 100644 index 0000000000..4538c6c5a7 --- /dev/null +++ b/bzz/chunker_test.go @@ -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 diff --git a/bzz/common_test.go b/bzz/common_test.go new file mode 100644 index 0000000000..edc7b3e3e7 --- /dev/null +++ b/bzz/common_test.go @@ -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) + } +} diff --git a/bzz/database.go b/bzz/database.go new file mode 100644 index 0000000000..3d422c9413 --- /dev/null +++ b/bzz/database.go @@ -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) + } +} diff --git a/bzz/dbstore.go b/bzz/dbstore.go new file mode 100644 index 0000000000..36c52bc4d8 --- /dev/null +++ b/bzz/dbstore.go @@ -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() +} diff --git a/bzz/dbstore_test.go b/bzz/dbstore_test.go new file mode 100644 index 0000000000..6e2d5a8cd4 --- /dev/null +++ b/bzz/dbstore_test.go @@ -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) + } +} diff --git a/bzz/dpa.go b/bzz/dpa.go new file mode 100644 index 0000000000..4c4064dd13 --- /dev/null +++ b/bzz/dpa.go @@ -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: + } + } + }() +} diff --git a/bzz/dpa_test.go b/bzz/dpa_test.go new file mode 100644 index 0000000000..27ee86a1cc --- /dev/null +++ b/bzz/dpa_test.go @@ -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.") + } +} diff --git a/bzz/hive.go b/bzz/hive.go new file mode 100644 index 0000000000..dbe47544d0 --- /dev/null +++ b/bzz/hive.go @@ -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) +} diff --git a/bzz/httpaccess.go b/bzz/httpaccess.go new file mode 100644 index 0000000000..b8f49f9865 --- /dev/null +++ b/bzz/httpaccess.go @@ -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 +} diff --git a/bzz/js_api.go b/bzz/js_api.go new file mode 100644 index 0000000000..ae5ca8d2f3 --- /dev/null +++ b/bzz/js_api.go @@ -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"}}) diff --git a/bzz/localstore.go b/bzz/localstore.go new file mode 100644 index 0000000000..7f6a4eabd8 --- /dev/null +++ b/bzz/localstore.go @@ -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 +} diff --git a/bzz/manifest.go b/bzz/manifest.go new file mode 100644 index 0000000000..02c333b207 --- /dev/null +++ b/bzz/manifest.go @@ -0,0 +1,305 @@ +package bzz + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + manifestType = "application/bzz-manifest+json" +) + +type manifestTrie struct { + dpa *DPA + entries [257]*manifestTrieEntry // indexed by first character of path, entries[256] is the empty path entry + hash Key // if hash != nil, it is stored +} + +type manifestJSON struct { + Entries []*manifestTrieEntry `json:"entries"` +} + +type manifestTrieEntry struct { + Path string `json:"path"` + Hash string `json:"hash"` // for manifest content type, empty until subtrie is evaluated + ContentType string `json:"contentType"` + Status int `json:"status"` + subtrie *manifestTrie +} + +func loadManifest(dpa *DPA, hash Key) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand + + dpaLogger.Debugf("Swarm: manifest lookup key: '%064x'.", hash) + // retrieve manifest via DPA + manifestReader := dpa.Retrieve(hash) + return readManifest(manifestReader, hash, dpa) +} + +func readManifest(manifestReader SectionReader, hash Key, dpa *DPA) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand + + // TODO check size for oversized manifests + manifestData := make([]byte, manifestReader.Size()) + var size int + size, err = manifestReader.Read(manifestData) + if int64(size) < manifestReader.Size() { + dpaLogger.Debugf("Swarm: Manifest %064x not found.", hash) + if err == nil { + err = fmt.Errorf("Manifest retrieval cut short: %v < %v", size, manifestReader.Size()) + } + return + } + + dpaLogger.Debugf("Swarm: Manifest %064x retrieved", hash) + man := manifestJSON{} + err = json.Unmarshal(manifestData, &man) + if err != nil { + err = fmt.Errorf("Manifest %064x is malformed: %v", hash, err) + dpaLogger.Debugf("Swarm: %v", err) + return + } + + dpaLogger.Debugf("Swarm: Manifest %064x has %d entries.", hash, len(man.Entries)) + + trie = &manifestTrie{ + dpa: dpa, + } + for _, entry := range man.Entries { + trie.addEntry(entry) + } + return +} + +func (self *manifestTrie) addEntry(entry *manifestTrieEntry) { + self.hash = nil // trie modified, hash needs to be re-calculated on demand + + if len(entry.Path) == 0 { + self.entries[256] = entry + return + } + + b := byte(entry.Path[0]) + if (self.entries[b] == nil) || (self.entries[b].Path == entry.Path) { + self.entries[b] = entry + return + } + + oldentry := self.entries[b] + cpl := 0 + for (len(entry.Path) > cpl) && (len(oldentry.Path) > cpl) && (entry.Path[cpl] == oldentry.Path[cpl]) { + cpl++ + } + + if (oldentry.ContentType == manifestType) && (cpl == len(oldentry.Path)) { + if self.loadSubTrie(oldentry) != nil { + return + } + entry.Path = entry.Path[cpl:] + oldentry.subtrie.addEntry(entry) + oldentry.Hash = "" + return + } + + commonPrefix := entry.Path[:cpl] + + subtrie := &manifestTrie{ + dpa: self.dpa, + } + entry.Path = entry.Path[cpl:] + oldentry.Path = oldentry.Path[cpl:] + subtrie.addEntry(entry) + subtrie.addEntry(oldentry) + + self.entries[b] = &manifestTrieEntry{ + Path: commonPrefix, + Hash: "", + ContentType: manifestType, + subtrie: subtrie, + } +} + +func (self *manifestTrie) getCountLast() (cnt int, entry *manifestTrieEntry) { + for _, e := range self.entries { + if e != nil { + cnt++ + entry = e + } + } + return +} + +func (self *manifestTrie) deleteEntry(path string) { + self.hash = nil // trie modified, hash needs to be re-calculated on demand + + if len(path) == 0 { + self.entries[256] = nil + return + } + + b := byte(path[0]) + entry := self.entries[b] + if (entry != nil) && (entry.Path == path) { + self.entries[b] = nil + return + } + + epl := len(entry.Path) + if (entry.ContentType == manifestType) && (len(path) >= epl) && (path[:epl] == entry.Path) { + if self.loadSubTrie(entry) != nil { + return + } + entry.subtrie.deleteEntry(path[epl:]) + entry.Hash = "" + // remove subtree if it has less than 2 elements + cnt, lastentry := entry.subtrie.getCountLast() + if cnt < 2 { + if lastentry != nil { + lastentry.Path = entry.Path + lastentry.Path + } + self.entries[b] = lastentry + } + } +} + +func (self *manifestTrie) recalcAndStore() error { + if self.hash != nil { + return nil + } + + var buffer bytes.Buffer + buffer.WriteString(`{"entries":[`) + + list := &manifestJSON{} + for _, entry := range self.entries { + if entry != nil { + if entry.Hash == "" { // TODO: paralellize + err := entry.subtrie.recalcAndStore() + if err != nil { + return err + } + entry.Hash = fmt.Sprintf("%064x", entry.subtrie.hash) + } + list.Entries = append(list.Entries, entry) + } + } + + manifest, err := json.Marshal(list) + if err != nil { + return err + } + + sr := io.NewSectionReader(bytes.NewReader(manifest), 0, int64(len(manifest))) + wg := &sync.WaitGroup{} + key, err2 := self.dpa.Store(sr, wg) + wg.Wait() + self.hash = key + return err2 +} + +func (self *manifestTrie) loadSubTrie(entry *manifestTrieEntry) (err error) { + if entry.subtrie == nil { + hash := common.Hex2Bytes(entry.Hash) + entry.subtrie, err = loadManifest(self.dpa, hash) + entry.Hash = "" // might not match, should be recalculated + } + return +} + +func (self *manifestTrie) listWithPrefixInt(prefix, rp string, cb func(entry *manifestTrieEntry, suffix string)) { + plen := len(prefix) + var start, stop int + if plen == 0 { + start = 0 + stop = 256 + } else { + start = int(prefix[0]) + stop = start + } + + for i := start; i <= stop; i++ { + entry := self.entries[i] + if entry != nil { + epl := len(entry.Path) + if entry.ContentType == manifestType { + l := plen + if epl < l { + l = epl + } + if (prefix[:l] == entry.Path[:l]) && (self.loadSubTrie(entry) == nil) { // TODO: handle errors + entry.subtrie.listWithPrefixInt(prefix[l:], rp+entry.Path[l:], cb) + } + } else { + if (epl >= plen) && (prefix == entry.Path[:plen]) { + cb(entry, rp+entry.Path[plen:]) + } + } + } + } +} + +func (self *manifestTrie) listWithPrefix(prefix string, cb func(entry *manifestTrieEntry, suffix string)) { + self.listWithPrefixInt(prefix, "", cb) +} + +func (self *manifestTrie) findPrefixOf(path string) (entry *manifestTrieEntry, pos int) { + + dpaLogger.Debugf("findPrefixOf(%s)", path) + + if len(path) == 0 { + return self.entries[256], 0 + } + + b := byte(path[0]) + entry = self.entries[b] + if entry == nil { + return self.entries[256], 0 + } + epl := len(entry.Path) + dpaLogger.Debugf("path = %v entry.Path = %v epl = %v", path, entry.Path, epl) + if (len(path) >= epl) && (path[:epl] == entry.Path) { + dpaLogger.Debugf("entry.ContentType = %v", entry.ContentType) + if entry.ContentType == manifestType { + if self.loadSubTrie(entry) != nil { + return nil, 0 + } + entry, pos = entry.subtrie.findPrefixOf(path[epl:]) + if entry != nil { + pos += epl + } + } else { + pos = epl + } + } else { + entry = nil + } + return +} + +// file system manifest always contains regularized paths +// no leading or trailing slashes, only single slashes inside +func regularSlashes(path string) (res string) { + for i := 0; i < len(path); i++ { + if (path[i] != '/') || ((i > 0) && (path[i-1] != '/')) { + res = res + path[i:i+1] + } + } + if (len(res) > 0) && (res[len(res)-1] == '/') { + res = res[:len(res)-1] + } + //fmt.Printf("%v -> %v\n", path, res) + return +} + +func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) { + path := regularSlashes(spath) + var pos int + entry, pos = self.findPrefixOf(path) + /*if (pos > 0) && (pos < len(path)) && (path[pos] != '/') { + return nil, "" + }*/ + return entry, path[:pos] +} diff --git a/bzz/manifest_test.go b/bzz/manifest_test.go new file mode 100644 index 0000000000..2bc6422eec --- /dev/null +++ b/bzz/manifest_test.go @@ -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) { + +} diff --git a/bzz/memstore.go b/bzz/memstore.go new file mode 100644 index 0000000000..9cca945e37 --- /dev/null +++ b/bzz/memstore.go @@ -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< 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 + } + } + +} diff --git a/bzz/memstore_test.go b/bzz/memstore_test.go new file mode 100644 index 0000000000..1aa6e51e5e --- /dev/null +++ b/bzz/memstore_test.go @@ -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) + } +} diff --git a/bzz/netstore.go b/bzz/netstore.go new file mode 100644 index 0000000000..751bf42f81 --- /dev/null +++ b/bzz/netstore.go @@ -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 + } +} diff --git a/bzz/protocol.go b/bzz/protocol.go new file mode 100644 index 0000000000..4e201e81cb --- /dev/null +++ b/bzz/protocol.go @@ -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) + } +} diff --git a/bzz/protocol_test.go b/bzz/protocol_test.go new file mode 100644 index 0000000000..d023723d37 --- /dev/null +++ b/bzz/protocol_test.go @@ -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) {} diff --git a/bzz/roundtripper.go b/bzz/roundtripper.go new file mode 100644 index 0000000000..53e49f3691 --- /dev/null +++ b/bzz/roundtripper.go @@ -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) +} diff --git a/bzz/roundtripper_test.go b/bzz/roundtripper_test.go new file mode 100644 index 0000000000..b5bcff4edf --- /dev/null +++ b/bzz/roundtripper_test.go @@ -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)) + } + +} diff --git a/cmd/geth/admin.go b/cmd/geth/admin.go index 13d10de32f..a73dce30d9 100644 --- a/cmd/geth/admin.go +++ b/cmd/geth/admin.go @@ -14,7 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/compiler" "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/types" "github.com/ethereum/go-ethereum/core/vm" @@ -27,10 +27,7 @@ import ( "gopkg.in/fatih/set.v0" ) -/* -node admin bindings -*/ - +// node admin bindings func (js *jsre) adminBindings() { ethO, _ := js.re.Get("eth") eth := ethO.Object() @@ -53,6 +50,9 @@ func (js *jsre) adminBindings() { admin.Set("verbosity", js.verbosity) admin.Set("progress", js.downloadProgress) admin.Set("setSolc", js.setSolc) + admin.Set("setGlobalRegistrar", js.setGlobalRegistrar) + admin.Set("setHashReg", js.setHashReg) + admin.Set("setUrlHint", js.setUrlHint) admin.Set("contractInfo", struct{}{}) t, _ = admin.Get("contractInfo") @@ -60,8 +60,8 @@ func (js *jsre) adminBindings() { // newRegistry officially not documented temporary option cinfo.Set("start", js.startNatSpec) cinfo.Set("stop", js.stopNatSpec) - cinfo.Set("newRegistry", js.newRegistry) cinfo.Set("get", js.getContractInfo) + cinfo.Set("saveInfo", js.saveInfo) cinfo.Set("register", js.register) cinfo.Set("registerUrl", js.registerUrl) // cinfo.Set("verify", js.verify) @@ -98,6 +98,12 @@ func (js *jsre) adminBindings() { debug.Set("insertBlock", js.insertBlockRlp) // undocumented temporary 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 @@ -202,6 +208,112 @@ func (js *jsre) pendingTransactions(call otto.FunctionCall) otto.Value { 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 { if len(call.ArgumentList) == 0 { fmt.Println("first argument must be a transaction") @@ -238,6 +350,56 @@ func (js *jsre) resend(call otto.FunctionCall) otto.Value { 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 { if len(call.ArgumentList) != 2 { 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 { - height = js.xeth.CurrentBlock().Number() - height.Add(height, common.Big1) + height = new(big.Int).Add(js.xeth.CurrentBlock().Number(), 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 - js.wait <- height +func waitForBlocks(wait chan *big.Int, height *big.Int, timer <-chan time.Time) (newHeight *big.Int, err error) { + wait <- height select { case <-timer: // 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: } }() - return otto.UndefinedValue() - case height = <-wait: + return nil, fmt.Errorf("timeout") + case newHeight = <-wait: } - v, _ := call.Otto.ToValue(height.Uint64()) - return v + return } 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 { - if len(call.ArgumentList) != 4 { - fmt.Println("requires 4 arguments: admin.contractInfo.register(fromaddress, contractaddress, contract, filename)") - return otto.UndefinedValue() + if len(call.ArgumentList) != 3 { + fmt.Println("requires 3 arguments: admin.contractInfo.register(fromaddress, contractaddress, contenthash)") + return otto.FalseValue() } sender, err := call.Argument(0).ToString() if err != nil { fmt.Println(err) - return otto.UndefinedValue() + return otto.FalseValue() } address, err := call.Argument(1).ToString() if err != nil { 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 { fmt.Println(err) return otto.UndefinedValue() @@ -765,36 +958,20 @@ func (js *jsre) register(call otto.FunctionCall) otto.Value { fmt.Println(err) return otto.UndefinedValue() } - var contract compiler.Contract - err = json.Unmarshal(jsonraw, &contract) + var contractInfo compiler.ContractInfo + err = json.Unmarshal(jsonraw, &contractInfo) if err != nil { fmt.Println(err) return otto.UndefinedValue() } - filename, err := call.Argument(3).ToString() + filename, err := call.Argument(1).ToString() if err != nil { fmt.Println(err) return otto.UndefinedValue() } - contenthash, err := compiler.ExtractInfo(&contract, 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) + contenthash, err := compiler.SaveInfo(&contractInfo, filename) if err != nil { fmt.Println(err) 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 { 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() } sender, err := call.Argument(0).ToString() @@ -827,9 +1004,8 @@ func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value { return otto.FalseValue() } - registry := resolver.New(js.xeth) - - _, err = registry.RegisterUrl(common.HexToAddress(sender), common.HexToHash(contenthash), url) + registry := registrar.New(js.xeth) + _, err = registry.SetUrlToHash(common.HexToAddress(sender), common.HexToHash(contenthash), url) if err != nil { fmt.Println(err) 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 { 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() } addr, err := call.Argument(0).ToString() @@ -849,12 +1025,12 @@ func (js *jsre) getContractInfo(call otto.FunctionCall) otto.Value { return otto.FalseValue() } - infoDoc, err := natspec.FetchDocsForContract(addr, js.xeth, ds) + infoDoc, err := natspec.FetchDocsForContract(addr, js.xeth, js.ds) if err != nil { fmt.Println(err) return otto.UndefinedValue() } - var info compiler.ContractInfo + var info interface{} err = json.Unmarshal(infoDoc, &info) if err != nil { fmt.Println(err) @@ -874,28 +1050,6 @@ func (js *jsre) stopNatSpec(call otto.FunctionCall) otto.Value { 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` type tx struct { tx *types.Transaction diff --git a/cmd/geth/contracts.go b/cmd/geth/contracts.go deleted file mode 100644 index 1f27838d15..0000000000 --- a/cmd/geth/contracts.go +++ /dev/null @@ -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" -) diff --git a/cmd/geth/js.go b/cmd/geth/js.go index 706bc6554f..4fb384a77e 100644 --- a/cmd/geth/js.go +++ b/cmd/geth/js.go @@ -26,10 +26,13 @@ import ( "path/filepath" "strings" + "github.com/ethereum/go-ethereum/bzz" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/docserver" "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" re "github.com/ethereum/go-ethereum/jsre" "github.com/ethereum/go-ethereum/rpc" @@ -63,6 +66,7 @@ func (r dumbterm) PasswordPrompt(p string) (string, error) { func (r dumbterm) AppendHistory(string) {} type jsre struct { + ds *docserver.DocServer re *re.JSRE ethereum *eth.Ethereum xeth *xeth.XEth @@ -73,19 +77,27 @@ type jsre struct { 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: "> "} // set default cors domain used by startRpc from CLI flag js.corsDomain = corsDomain if f == nil { f = js } + js.ds = docserver.New("/") js.xeth = xeth.New(ethereum, f) js.wait = js.xeth.UpdateState() - // update state in separare forever blocks js.re = re.New(libPath) + // js.apiBindings(js.xeth) js.apiBindings(f) 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 { 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) { xe := xeth.New(js.ethereum, f) + // func (js *jsre) apiBindings(xe *xeth.XEth) { ethApi := rpc.NewEthereumApi(xe) jeth := rpc.NewJeth(ethApi, js.re) @@ -143,14 +156,12 @@ var net = web3.net; 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 { if self.ethereum.NatSpec { - notice := natspec.GetNotice(self.xeth, tx, ds) + notice := natspec.GetNotice(self.xeth, tx, self.ds) fmt.Println(notice) answer, _ := self.Prompt("Confirm Transaction [y/n]") return strings.HasPrefix(strings.Trim(answer, " "), "y") diff --git a/cmd/geth/js_bzz_test.go b/cmd/geth/js_bzz_test.go new file mode 100644 index 0000000000..4472cb6233 --- /dev/null +++ b/cmd/geth/js_bzz_test.go @@ -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 + } +} diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index 3f34840f3b..466a7a07e5 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -3,21 +3,22 @@ package main import ( "fmt" "io/ioutil" + "math/big" "os" "path/filepath" "regexp" "runtime" "strconv" "testing" + "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/compiler" "github.com/ethereum/go-ethereum/common/docserver" "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/state" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" ) @@ -41,7 +42,6 @@ var ( type testjethre struct { *jsre - stateDb *state.StateDB lastConfirm string ds *docserver.DocServer } @@ -62,6 +62,10 @@ func (self *testjethre) ConfirmTransaction(tx string) bool { } 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") if err != nil { t.Fatal(err) @@ -72,14 +76,19 @@ func testJEthRE(t *testing.T) (string, *testjethre, *eth.Ethereum) { ks := crypto.NewKeyStorePlain(filepath.Join(tmp, "keystore")) am := accounts.NewManager(ks) - ethereum, err := eth.New(ð.Config{ + conf := ð.Config{ NodeKey: testNodeKey, DataDir: tmp, AccountManager: am, MaxPeers: 0, Name: "test", SolcPath: testSolcPath, - }) + PowTest: true, + } + if config != nil { + config(conf) + } + ethereum, err := eth.New(conf) if err != nil { 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") - ds, err := docserver.New("/") - if err != nil { - t.Errorf("Error creating DocServer: %v", err) - } - tf := &testjethre{ds: ds, stateDb: ethereum.ChainManager().State().Copy()} - repl := newJSRE(ethereum, assetPath, "", false, tf) + ds := docserver.New("/") + tf := &testjethre{ds: ds} + repl := newJSRE(ethereum, assetPath, "", conf.Bzz, conf.BzzPort, false, tf) tf.jsre = repl 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) { + t.Skip("broken after p2p update") tmp, repl, ethereum := testJEthRE(t) if err := ethereum.Start(); err != nil { t.Fatalf("error starting ethereum: %v", err) @@ -259,12 +259,21 @@ func TestContract(t *testing.T) { defer ethereum.Stop() defer os.RemoveAll(tmp) - var txc uint64 coinbase := common.HexToAddress(testAddress) - resolver.New(repl.xeth).CreateContracts(coinbase) - // time.Sleep(1000 * time.Millisecond) + reg := registrar.New(repl.xeth) + 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` + " /// @notice Will multiply `a` by 7." + `\n` + ` function multiply(uint a) returns(uint d) {\n` + @@ -272,14 +281,20 @@ func TestContract(t *testing.T) { ` }\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") if err != nil { t.Fatalf("%v", err) } - checkEvalJSON(t, repl, `primary = eth.accounts[0]`, `"`+testAddress+`"`) - checkEvalJSON(t, repl, `source = "`+source+`"`, `"`+source+`"`) + if checkEvalJSON(t, repl, `primary = eth.accounts[0]`, `"`+testAddress+`"`) != nil { + return + } + if checkEvalJSON(t, repl, `source = "`+source+`"`, `"`+source+`"`) != nil { + return + } // if solc is found with right version, test it, otherwise read from file sol, err := compiler.New("") @@ -299,71 +314,157 @@ func TestContract(t *testing.T) { t.Errorf("%v", err) } } 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, - `contractaddress = eth.sendTransaction({from: primary, data: contract.code })`, - `"0x5dcaace5982778b409c524873b319667eba5d074"`, - ) + `contractaddress = eth.sendTransaction({from: primary, data: contract.code})`, + `"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"}]'); Multiply7 = eth.contract(abiDef); multiply7 = Multiply7.at(contractaddress); ` - // time.Sleep(1500 * time.Millisecond) _, err = repl.re.Run(callSetup) if err != nil { 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 := "" if repl.lastConfirm != expNotice { 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`) - checkEvalJSON(t, repl, `multiply7.multiply.sendTransaction(6, { from: primary, gas: "1000000", gasPrice: "100000" })`, `undefined`) - expNotice = `About to submit transaction (no NatSpec info found for contract: content hash not found for '0x87e2802265838c7f14bb69eecd2112911af6767907a702eeaa445239fb20711b'): {"params":[{"to":"0x5dcaace5982778b409c524873b319667eba5d074","data": "0xc6888fa10000000000000000000000000000000000000000000000000000000000000006"}]}` + if !processTxs(repl, t, 1) { + return + } + + 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 { - 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"` - if sol != nil { + var contentHash = `"0x86d2b7cf1e72e9a7a3f8d96601f0151742a2f780f1526414304fbe413dc7f9bd"` + if sol != nil && solcVersion != sol.Version() { modContractInfo := versionRE.ReplaceAll(contractInfo, []byte(`"compilerVersion":"`+sol.Version()+`"`)) - _ = modContractInfo - // contenthash = crypto.Sha3(modContractInfo) + fmt.Printf("modified contractinfo:\n%s\n", modContractInfo) + contentHash = `"` + common.ToHex(crypto.Sha3([]byte(modContractInfo))) + `"` } - checkEvalJSON(t, repl, `filename = "/tmp/info.json"`, `"/tmp/info.json"`) - checkEvalJSON(t, repl, `contenthash = admin.contractInfo.register(primary, contractaddress, contract, filename)`, contenthash) - checkEvalJSON(t, repl, `admin.contractInfo.registerUrl(primary, contenthash, "file://"+filename)`, `true`) - if err != nil { - t.Errorf("unexpected error registering, got %v", err) + if checkEvalJSON(t, repl, `filename = "/tmp/info.json"`, `"/tmp/info.json"`) != nil { + return + } + if checkEvalJSON(t, repl, `contentHash = admin.contractInfo.saveInfo(contract.info, filename)`, contentHash) != nil { + 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 - txc, repl.xeth = repl.xeth.ApplyTestTxs(repl.stateDb, coinbase, txc) + if !processTxs(repl, t, 3) { + 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." 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 { val, err := re.re.Run("JSON.stringify(" + expr + ")") if err == nil && val.String() != want { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ab46fdd3e2..a4167aebcb 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -239,6 +239,8 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso utils.RPCListenAddrFlag, utils.RPCPortFlag, utils.WhisperEnabledFlag, + utils.SwarmEnabledFlag, + utils.SwarmProxyPortFlag, utils.VMDebugFlag, utils.ProtocolVersionFlag, utils.NetworkIdFlag, @@ -304,6 +306,8 @@ func console(ctx *cli.Context) { ethereum, ctx.String(utils.JSpathFlag.Name), ctx.GlobalString(utils.RPCCORSDomainFlag.Name), + ctx.GlobalBool(utils.SwarmEnabledFlag.Name), + ctx.GlobalString(utils.SwarmProxyPortFlag.Name), true, nil, ) @@ -325,6 +329,8 @@ func execJSFiles(ctx *cli.Context) { ethereum, ctx.String(utils.JSpathFlag.Name), ctx.GlobalString(utils.RPCCORSDomainFlag.Name), + ctx.GlobalBool(utils.SwarmEnabledFlag.Name), + ctx.GlobalString(utils.SwarmProxyPortFlag.Name), false, nil, ) diff --git a/cmd/utils/customflags.go b/cmd/utils/customflags.go index 78a6b8d220..2488314f79 100644 --- a/cmd/utils/customflags.go +++ b/cmd/utils/customflags.go @@ -23,7 +23,7 @@ func (self *DirectoryString) String() string { } func (self *DirectoryString) Set(value string) error { - self.Value = expandPath(value) + self.Value = ExpandPath(value) return nil } @@ -119,7 +119,7 @@ func (self *DirectoryFlag) Set(value string) { // 2. expands embedded environment variables // 3. cleans the path, e.g. /a/b/../c -> /a/c // 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 user, err := user.Current(); err == nil { if err == nil { diff --git a/cmd/utils/customflags_test.go b/cmd/utils/customflags_test.go index 11deb38ef2..8093db5f92 100644 --- a/cmd/utils/customflags_test.go +++ b/cmd/utils/customflags_test.go @@ -20,7 +20,7 @@ func TestPathExpansion(t *testing.T) { os.Setenv("DDDXXX", "/tmp") for test, expected := range tests { - got := expandPath(test) + got := ExpandPath(test) if got != expected { t.Errorf("test %s, got %s, expected %s\n", test, got, expected) } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 909d7815ea..98c1cc43b9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -243,6 +243,15 @@ var ( Name: "shh", 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 JSpathFlag = cli.StringFlag{ Name: "jspath", @@ -312,6 +321,8 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config { Discovery: !ctx.GlobalBool(NoDiscoverFlag.Name), NodeKey: MakeNodeKey(ctx), Shh: ctx.GlobalBool(WhisperEnabledFlag.Name), + Bzz: ctx.GlobalBool(SwarmEnabledFlag.Name), + BzzPort: ctx.GlobalString(SwarmProxyPortFlag.Name), Dial: true, BootNodes: ctx.GlobalString(BootnodesFlag.Name), GasPrice: common.String2Big(ctx.GlobalString(GasPriceFlag.Name)), diff --git a/common/compiler/solidity.go b/common/compiler/solidity.go index caf86974e5..4d5ffb473e 100644 --- a/common/compiler/solidity.go +++ b/common/compiler/solidity.go @@ -185,12 +185,12 @@ func (sol *Solidity) Compile(source string) (contracts map[string]*Contract, err return } -func ExtractInfo(contract *Contract, filename string) (contenthash common.Hash, err error) { - contractInfo, err := json.Marshal(contract.Info) +func SaveInfo(info *ContractInfo, filename string) (contenthash common.Hash, err error) { + infojson, err := json.Marshal(info) if err != nil { return } - contenthash = common.BytesToHash(crypto.Sha3(contractInfo)) - err = ioutil.WriteFile(filename, contractInfo, 0600) + contenthash = common.BytesToHash(crypto.Sha3(infojson)) + err = ioutil.WriteFile(filename, infojson, 0600) return } diff --git a/common/compiler/solidity_test.go b/common/compiler/solidity_test.go index 46f733e59d..608617bc11 100644 --- a/common/compiler/solidity_test.go +++ b/common/compiler/solidity_test.go @@ -72,19 +72,15 @@ func TestNoCompiler(t *testing.T) { } } -func TestExtractInfo(t *testing.T) { +func TestSaveInfo(t *testing.T) { var cinfo ContractInfo err := json.Unmarshal([]byte(info), &cinfo) if err != nil { t.Errorf("%v", err) } - contract := &Contract{ - Code: "", - Info: cinfo, - } filename := "/tmp/solctest.info.json" os.Remove(filename) - cinfohash, err := ExtractInfo(contract, filename) + cinfohash, err := SaveInfo(&cinfo, filename) if err != nil { t.Errorf("error extracting info: %v", err) } diff --git a/common/docserver/docserver.go b/common/docserver/docserver.go index 5e076aa7ee..e900b45f5a 100644 --- a/common/docserver/docserver.go +++ b/common/docserver/docserver.go @@ -4,34 +4,25 @@ import ( "fmt" "io/ioutil" "net/http" + "path/filepath" "github.com/ethereum/go-ethereum/common" "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 { *http.Transport DocRoot string + schemes []string } -func New(docRoot string) (self *DocServer, err error) { +func New(docRoot string) (self *DocServer) { self = &DocServer{ Transport: &http.Transport{}, DocRoot: docRoot, + schemes: []string{"file"}, } - err = self.RegisterProtocols(schemes) + self.RegisterProtocol("file", http.NewFileTransport(http.Dir(self.DocRoot))) return } @@ -45,25 +36,24 @@ func (self *DocServer) Client() *http.Client { } } -func (self *DocServer) RegisterProtocols(schemes map[string]func(*DocServer) http.RoundTripper) (err error) { - for scheme, rtf := range schemes { - self.RegisterProtocol(scheme, rtf(self)) +func (self *DocServer) RegisterScheme(scheme string, rt http.RoundTripper) { + self.schemes = append(self.schemes, scheme) + self.RegisterProtocol(scheme, rt) +} + +func (self *DocServer) HasScheme(scheme string) bool { + for _, s := range self.schemes { + if s == scheme { + return true + } } - return + return false } func (self *DocServer) GetAuthContent(uri string, hash common.Hash) (content []byte, err error) { // 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) + + content, err = self.Get(uri, "") if err != nil { return } @@ -80,3 +70,31 @@ func (self *DocServer) GetAuthContent(uri string, hash common.Hash) (content []b 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 + +} diff --git a/common/docserver/docserver_test.go b/common/docserver/docserver_test.go index 400d7447ac..09b16864a7 100644 --- a/common/docserver/docserver_test.go +++ b/common/docserver/docserver_test.go @@ -2,6 +2,7 @@ package docserver import ( "io/ioutil" + "net/http" "os" "testing" @@ -15,7 +16,7 @@ func TestGetAuthContent(t *testing.T) { copy(hash[:], crypto.Sha3([]byte(text))) ioutil.WriteFile("/tmp/test.content", []byte(text), os.ModePerm) - ds, err := New("/tmp/") + ds := New("/tmp/") content, err := ds.GetAuthContent("file:///test.content", hash) if err != nil { 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") + } +} diff --git a/common/kademlia/kademlia.go b/common/kademlia/kademlia.go new file mode 100644 index 0000000000..4e679876e0 --- /dev/null +++ b/common/kademlia/kademlia.go @@ -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) +} diff --git a/common/kademlia/kademlia_test.go b/common/kademlia/kademlia_test.go new file mode 100644 index 0000000000..7796d8ca7d --- /dev/null +++ b/common/kademlia/kademlia_test.go @@ -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) +} diff --git a/common/natspec/natspec.go b/common/natspec/natspec.go index 7e5f053c70..9965a2227a 100644 --- a/common/natspec/natspec.go +++ b/common/natspec/natspec.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "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/xeth" ) @@ -88,7 +88,7 @@ func New(xeth *xeth.XEth, jsontx string, http *docserver.DocServer) (self *NatSp } // 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 codehex := xeth.CodeAt(contractAddress) codeb := xeth.CodeAtBytes(contractAddress) @@ -99,20 +99,32 @@ func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, http *docserv } codehash := common.BytesToHash(crypto.Sha3(codeb)) // set up nameresolver with natspecreg + urlhint contract addresses - res := resolver.New(xeth) + reg := registrar.New(xeth) // 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 { return } // 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 { return } - return } diff --git a/common/natspec/natspec_e2e_test.go b/common/natspec/natspec_e2e_test.go index 7e91726496..a941acbba1 100644 --- a/common/natspec/natspec_e2e_test.go +++ b/common/natspec/natspec_e2e_test.go @@ -10,7 +10,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "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/state" "github.com/ethereum/go-ethereum/crypto" @@ -73,8 +73,7 @@ const ( ) type testFrontend struct { - t *testing.T - // resolver *resolver.Resolver + t *testing.T ethereum *eth.Ethereum xeth *xe.XEth coinbase common.Address @@ -91,10 +90,7 @@ func (self *testFrontend) UnlockAccount(acc []byte) bool { func (self *testFrontend) ConfirmTransaction(tx string) bool { if self.wantNatSpec { - ds, err := docserver.New("/tmp/") - if err != nil { - self.t.Errorf("Error creating DocServer: %v", err) - } + ds := docserver.New("/tmp/") self.lastConfirm = GetNotice(self.xeth, tx, ds) } return true @@ -159,11 +155,16 @@ func testInit(t *testing.T) (self *testFrontend) { self.stateDb = self.ethereum.ChainManager().State().Copy() // initialise the registry contracts - // self.resolver.CreateContracts(addr) - resolver.New(self.xeth).CreateContracts(addr) + reg := registrar.New(self.xeth) + 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() - // t.Logf("HashReg contract registered at %v", resolver.HashRegContractAddress) - // t.Logf("URLHint contract registered at %v", resolver.UrlHintContractAddress) return @@ -192,13 +193,20 @@ func TestNatspecE2E(t *testing.T) { dochash := common.BytesToHash(crypto.Sha3([]byte(testContractInfo))) // take the codehash for the contract we wanna test - // codehex := tf.xeth.CodeAt(resolver.HashRegContractAddress) - codeb := tf.xeth.CodeAtBytes(resolver.HashRegContractAddress) + // codehex := tf.xeth.CodeAt(registar.HashRegAddr) + codeb := tf.xeth.CodeAtBytes(registrar.HashRegAddr) codehash := common.BytesToHash(crypto.Sha3(codeb)) // use resolver to register codehash->dochash->url - registry := resolver.New(tf.xeth) - _, err := registry.Register(tf.coinbase, codehash, dochash, "file:///"+testFileName) + // test if globalregistry works + // 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 { t.Errorf("error registering: %v", err) } @@ -209,18 +217,19 @@ func TestNatspecE2E(t *testing.T) { // now using the same transactions to check confirm messages 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 { 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 { t.Errorf("Wrong confirm message. expected '%v', got '%v'", testExpNotice, tf.lastConfirm) } // test unknown method - exp := fmt.Sprintf(testExpNotice2, resolver.HashRegContractAddress) - _, err = registry.SetOwner(tf.coinbase) + exp := fmt.Sprintf(testExpNotice2, registrar.HashRegAddr) + _, err = reg.SetOwner(tf.coinbase) if err != nil { t.Errorf("error setting owner: %v", err) } @@ -230,9 +239,9 @@ func TestNatspecE2E(t *testing.T) { } // 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 { t.Errorf("error registering: %v", err) } diff --git a/common/registrar/contracts.go b/common/registrar/contracts.go new file mode 100644 index 0000000000..6c624030ef --- /dev/null +++ b/common/registrar/contracts.go @@ -0,0 +1,147 @@ +package registrar + +const ( // built-in contracts address source code and evm code + UrlHintCode = "0x60c180600c6000396000f30060003560e060020a90048063300a3bbf14601557005b6024600435602435604435602a565b60006000f35b6000600084815260200190815260200160002054600160a060020a0316600014806078575033600160a060020a03166000600085815260200190815260200160002054600160a060020a0316145b607f5760bc565b336000600085815260200190815260200160002081905550806001600085815260200190815260200160002083610100811060b657005b01819055505b50505056" + + UrlHintSrc = ` +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; + (uint256 => uint256[256]) url; +} + ` + + HashRegCode = "0x609880600c6000396000f30060003560e060020a9004806331e12c2014601f578063d66d6c1014602b57005b6025603d565b60006000f35b6037600435602435605d565b60006000f35b600054600160a060020a0316600014605357605b565b336000819055505b565b600054600160a060020a031633600160a060020a031614607b576094565b8060016000848152602001908152602001600020819055505b505056" + + HashRegSrc = ` +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; +} +` + + GlobalRegistrarSrc = ` +//sol + +import "owned"; + +contract NameRegister { + function addr(bytes32 _name) constant returns (address o_owner) {} + function name(address _owner) constant returns (bytes32 o_name) {} +} + +contract Registrar is NameRegister { + event Changed(bytes32 indexed name); + event PrimaryChanged(bytes32 indexed name, address indexed addr); + + function owner(bytes32 _name) constant returns (address o_owner) {} + function addr(bytes32 _name) constant returns (address o_address) {} + function subRegistrar(bytes32 _name) constant returns (address o_subRegistrar) {} + function content(bytes32 _name) constant returns (bytes32 o_content) {} + + function name(address _owner) constant returns (bytes32 o_name) {} +} + +contract GlobalRegistrar is Registrar { + struct Record { + address owner; + address primary; + address subRegistrar; + bytes32 content; + uint value; + uint renewalDate; + } + + function Registrar() { + // TODO: Populate with hall-of-fame. + } + + function reserve(bytes32 _name) { + // Don't allow the same name to be overwritten. + // TODO: bidding mechanism + if (m_toRecord[_name].owner == 0) { + m_toRecord[_name].owner = msg.sender; + Changed(_name); + } + } + + /* + TODO + > 12 chars: free + <= 12 chars: auction: + 1. new names are auctioned + - 7 day period to collect all bid bytes32es + deposits + - 1 day period to collect all bids to be considered (validity requires associated deposit to be >10% of bid) + - all valid bids are burnt except highest - difference between that and second highest is returned to winner + 2. remember when last auctioned/renewed + 3. anyone can force renewal process: + - 7 day period to collect all bid bytes32es + deposits + - 1 day period to collect all bids & full amounts - bids only uncovered if sufficiently high. + - 1% of winner burnt; original owner paid rest. + */ + + modifier onlyrecordowner(bytes32 _name) { if (m_toRecord[_name].owner == msg.sender) _ } + + function transfer(bytes32 _name, address _newOwner) onlyrecordowner(_name) { + m_toRecord[_name].owner = _newOwner; + Changed(_name); + } + + function disown(bytes32 _name) onlyrecordowner(_name) { + if (m_toName[m_toRecord[_name].primary] == _name) + { + PrimaryChanged(_name, m_toRecord[_name].primary); + m_toName[m_toRecord[_name].primary] = ""; + } + delete m_toRecord[_name]; + Changed(_name); + } + + function setAddress(bytes32 _name, address _a, bool _primary) onlyrecordowner(_name) { + m_toRecord[_name].primary = _a; + if (_primary) + { + PrimaryChanged(_name, _a); + m_toName[_a] = _name; + } + Changed(_name); + } + function setSubRegistrar(bytes32 _name, address _registrar) onlyrecordowner(_name) { + m_toRecord[_name].subRegistrar = _registrar; + Changed(_name); + } + function setContent(bytes32 _name, bytes32 _content) onlyrecordowner(_name) { + m_toRecord[_name].content = _content; + Changed(_name); + } + + function owner(bytes32 _name) constant returns (address) { return m_toRecord[_name].owner; } + function addr(bytes32 _name) constant returns (address) { return m_toRecord[_name].primary; } +// function subRegistrar(bytes32 _name) constant returns (address) { return m_toRecord[_name].subRegistrar; } // TODO: bring in on next iteration. + function register(bytes32 _name) constant returns (address) { return m_toRecord[_name].subRegistrar; } // only possible for now + function content(bytes32 _name) constant returns (bytes32) { return m_toRecord[_name].content; } + function name(address _owner) constant returns (bytes32 o_name) { return m_toName[_owner]; } + + mapping (address => bytes32) m_toName; + mapping (bytes32 => Record) m_toRecord; +} +` + GlobalRegistrarAbi = `[{"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"}]` + + GlobalRegistrarCode = `0x610b408061000e6000396000f3006000357c01000000000000000000000000000000000000000000000000000000009004806301984892146100b357806302571be3146100ce5780632dff6941146100ff5780633b3b57de1461011a578063432ced041461014b5780635a3a05bd1461016257806379ce9fac1461019357806389a69c0e146101b0578063b9f37c86146101cd578063be99a980146101de578063c3d014d614610201578063d93e75731461021e578063e1fa8e841461023557005b6100c4600480359060200150610b02565b8060005260206000f35b6100df6004803590602001506109f3565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b610110600480359060200150610ad4565b8060005260206000f35b61012b600480359060200150610a3e565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b61015c600480359060200150610271565b60006000f35b610173600480359060200150610266565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101aa600480359060200180359060200150610341565b60006000f35b6101c7600480359060200180359060200150610844565b60006000f35b6101d860045061026e565b60006000f35b6101fb6004803590602001803590602001803590602001506106de565b60006000f35b61021860048035906020018035906020015061092c565b60006000f35b61022f600480359060200150610429565b60006000f35b610246600480359060200150610a89565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b60005b919050565b5b565b60006001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141561033d57336001600050600083815260200190815260200160002060005060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550807fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b5b50565b813373ffffffffffffffffffffffffffffffffffffffff166001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141561042357816001600050600085815260200190815260200160002060005060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550827fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b505b5050565b803373ffffffffffffffffffffffffffffffffffffffff166001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614156106d95781600060005060006001600050600086815260200190815260200160002060005060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000505414156105fd576001600050600083815260200190815260200160002060005060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16827ff63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a85456040604090036040a36000600060005060006001600050600086815260200190815260200160002060005060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600050819055505b6001600050600083815260200190815260200160002060006000820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556001820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556002820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556003820160005060009055600482016000506000905560058201600050600090555050817fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b505b50565b823373ffffffffffffffffffffffffffffffffffffffff166001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141561083d57826001600050600086815260200190815260200160002060005060010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055508115610811578273ffffffffffffffffffffffffffffffffffffffff16847ff63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a85456040604090036040a383600060005060008573ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600050819055505b837fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b505b505050565b813373ffffffffffffffffffffffffffffffffffffffff166001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141561092657816001600050600085815260200190815260200160002060005060020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550827fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b505b5050565b813373ffffffffffffffffffffffffffffffffffffffff166001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614156109ed57816001600050600085815260200190815260200160002060005060030160005081905550827fa6697e974e6a320f454390be03f74955e8978f1a6971ea6730542e37b66179bc6040604090036040a25b505b5050565b60006001600050600083815260200190815260200160002060005060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050610a39565b919050565b60006001600050600083815260200190815260200160002060005060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050610a84565b919050565b60006001600050600083815260200190815260200160002060005060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050610acf565b919050565b600060016000506000838152602001908152602001600020600050600301600050549050610afd565b919050565b6000600060005060008373ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600050549050610b3b565b91905056` +) diff --git a/common/registrar/ethreg/ethreg.go b/common/registrar/ethreg/ethreg.go new file mode 100644 index 0000000000..f5ad3345bf --- /dev/null +++ b/common/registrar/ethreg/ethreg.go @@ -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) +} diff --git a/common/registrar/registrar.go b/common/registrar/registrar.go new file mode 100644 index 0000000000..e88cc54e76 --- /dev/null +++ b/common/registrar/registrar.go @@ -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 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):], "" +} diff --git a/common/resolver/resolver_test.go b/common/registrar/registrar_test.go similarity index 61% rename from common/resolver/resolver_test.go rename to common/registrar/registrar_test.go index 02d12592e5..5612e691ca 100644 --- a/common/resolver/resolver_test.go +++ b/common/registrar/registrar_test.go @@ -1,4 +1,4 @@ -package resolver +package registrar import ( "testing" @@ -20,22 +20,22 @@ var ( ) func NewTestBackend() *testBackend { - HashRegContractAddress = common.BigToAddress(common.Big0).Hex()[2:] - UrlHintContractAddress = common.BigToAddress(common.Big1).Hex()[2:] + HashRegAddr = common.BigToAddress(common.Big0).Hex() //[2:] + UrlHintAddr = common.BigToAddress(common.Big1).Hex() //[2:] self := &testBackend{} 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[:])) - 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[:]) 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))) - self.contracts[UrlHintContractAddress][key] = "0x00" + self.contracts[UrlHintAddr[2:]][key] = "0x0" return self } @@ -52,11 +52,25 @@ func (self *testBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, ga 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() 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 { t.Errorf("expected no error, got %v", err) } else { @@ -66,28 +80,17 @@ func TestKeyToContentHash(t *testing.T) { } } -func TestContentHashToUrl(t *testing.T) { +func TestHashToUrl(t *testing.T) { b := NewTestBackend() res := New(b) - got, err := res.ContentHashToUrl(hash) + // res.SetUrlHint() + + got, err := res.HashToUrl(hash) if err != nil { - t.Errorf("expected no error, got %v", err) + t.Errorf("expected error, got %v", err) } else { if got != url { 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) - } - } -} diff --git a/common/resolver/contracts.go b/common/resolver/contracts.go deleted file mode 100644 index 4aad95e436..0000000000 --- a/common/resolver/contracts.go +++ /dev/null @@ -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; - } - */ - -) diff --git a/common/resolver/resolver.go b/common/resolver/resolver.go deleted file mode 100644 index 9016547e10..0000000000 --- a/common/resolver/resolver.go +++ /dev/null @@ -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) -} diff --git a/core/contracts.go b/core/contracts.go new file mode 100644 index 0000000000..027e9d5019 --- /dev/null +++ b/core/contracts.go @@ -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 + `" }, + ` +) diff --git a/core/genesis.go b/core/genesis.go index a9b7339f34..e95532645c 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -57,7 +57,14 @@ func GenesisBlock(db common.Database) *types.Block { return genesis } +const ( + TestAccount = "e273f01c99144c438695e10f24926dc1f9fbf62d" + TestBalance = "1000000000000" +) + var GenesisAccounts = []byte(`{ + "` + TestAccount + `": {"balance": "` + TestBalance + `"}, + ` + BuiltInContracts + ` "0000000000000000000000000000000000000001": {"balance": "1"}, "0000000000000000000000000000000000000002": {"balance": "1"}, "0000000000000000000000000000000000000003": {"balance": "1"}, diff --git a/eth/backend.go b/eth/backend.go index 3956dfcaa9..2d535b3ec6 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/bzz" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/compiler" "github.com/ethereum/go-ethereum/core" @@ -82,9 +83,12 @@ type Config struct { // If nil, an ephemeral key is used. NodeKey *ecdsa.PrivateKey - NAT nat.Interface - Shh bool - Dial bool + NAT nat.Interface + Shh bool + Dial bool + Bzz bool + BzzPort string + PowTest bool Etherbase string GasPrice *big.Int @@ -193,6 +197,7 @@ type Ethereum struct { pow *ethash.Ethash protocolManager *ProtocolManager downloader *downloader.Downloader + Swarm *bzz.Api SolcPath string solc *compiler.Solidity @@ -283,7 +288,15 @@ func New(config *Config) (*Ethereum, error) { AutoDAG: config.AutoDAG, } - eth.pow = ethash.New() + 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.chainManager = core.NewChainManager(blockDb, stateDb, eth.pow, eth.EventMux()) eth.downloader = downloader.New(eth.EventMux(), eth.chainManager.HasBlock, eth.chainManager.GetBlock) 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 { return nil, err } + 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 { protocols = append(protocols, eth.whisper.Protocol()) } @@ -324,6 +355,8 @@ func New(config *Config) (*Ethereum, error) { eth.net.ListenAddr = ":" + config.Port } + eth.net.Protocols = protocols + vm.Debug = config.VmDebug return eth, nil @@ -450,6 +483,7 @@ func (s *Ethereum) Start() error { ClientString: s.net.Name, ProtocolVersion: ProtocolVersion, }) + err := s.net.Start() if err != nil { return err @@ -469,6 +503,10 @@ func (s *Ethereum) Start() error { s.whisper.Start() } + if s.Swarm != nil { + s.Swarm.Start(s.net.Self(), s.AddPeer) + } + glog.V(logger.Info).Infoln("Server started") return nil } @@ -536,6 +574,11 @@ func (s *Ethereum) Stop() { } s.StopAutoDAG() + if s.Swarm != nil { + s.Swarm.Stop() + } + + glog.V(logger.Info).Infoln("Server stopped") close(s.shutdownChan) } diff --git a/eth/protocol.go b/eth/protocol.go index 948051ed1f..ff3c3cbfdc 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -9,7 +9,7 @@ import ( const ( ProtocolVersion = 60 - NetworkId = 0 + NetworkId = 100 ProtocolLength = uint64(8) ProtocolMaxMsgSize = 10 * 1024 * 1024 ) diff --git a/ethdb/database.go b/ethdb/database.go index 019645cedd..f605036c40 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -69,6 +69,10 @@ func (self *LDBDatabase) Flush() error { return nil } +func (self *LDBDatabase) Write(batch *leveldb.Batch) error { + return self.db.Write(batch, nil) +} + func (self *LDBDatabase) Close() { if err := self.Flush(); err != nil { glog.V(logger.Error).Infof("error: flush '%s': %v\n", self.fn, err) diff --git a/p2p/discover/node.go b/p2p/discover/node.go index a365ade159..4ad5158d2a 100644 --- a/p2p/discover/node.go +++ b/p2p/discover/node.go @@ -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 { return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)} } diff --git a/p2p/nat/natupnp.go b/p2p/nat/natupnp.go index ef7765e8de..21a9cf8b13 100644 --- a/p2p/nat/natupnp.go +++ b/p2p/nat/natupnp.go @@ -12,6 +12,8 @@ import ( "github.com/huin/goupnp/dcps/internetgateway2" ) +const soapRequestTimeout = 3 * time.Second + type upnp struct { dev *goupnp.RootDevice service string @@ -131,6 +133,7 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, } // check for a matching IGD service sc := goupnp.ServiceClient{service.NewSOAPClient(), devs[i].Root, service} + sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout upnp := matcher(devs[i].Root, sc) if upnp == nil { return diff --git a/rlp/encode_test.go b/rlp/encode_test.go index 6eb930d6c0..37b1079a33 100644 --- a/rlp/encode_test.go +++ b/rlp/encode_test.go @@ -40,8 +40,6 @@ func (e *encodableReader) Read(b []byte) (int, error) { panic("called") } -type namedByteType byte - var ( _ = Encoder(&testEncoder{}) _ = Encoder(byteEncoder(0))