diff --git a/accounts/account_manager.go b/accounts/account_manager.go index 74006395cd..2ada1a9804 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -91,6 +91,16 @@ func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) return signature, err } +func (am *Manager) GetUnlocked(addr common.Address) (prvkey *ecdsa.PrivateKey, err error) { + am.mutex.RLock() + defer am.mutex.RUnlock() + unlockedKey, found := am.unlocked[addr] + if !found { + return nil, ErrLocked + } + return unlockedKey.PrivateKey, nil +} + // Unlock unlocks the given account indefinitely. func (am *Manager) Unlock(addr common.Address, keyAuth string) error { return am.TimedUnlock(addr, keyAuth, 0) diff --git a/cmd/geth/js.go b/cmd/geth/js.go index 56b7a8b001..008b581024 100644 --- a/cmd/geth/js.go +++ b/cmd/geth/js.go @@ -24,9 +24,8 @@ import ( "os/signal" "path/filepath" "regexp" - "strings" - "sort" + "strings" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" @@ -190,6 +189,8 @@ func newJSRE(stack *node.Node, docRoot, corsDomain string, client comms.Ethereum if clt, ok := js.client.(*comms.InProcClient); ok { if offeredApis, err := api.ParseApiString(shared.AllApis, codec.JSON, js.xeth, stack); err == nil { clt.Initialize(api.Merge(offeredApis...)) + } else { + utils.Fatalf("Unable to offer apis: %v", err) } } diff --git a/cmd/geth/js_bzz_test.go b/cmd/geth/js_bzz_test.go new file mode 100644 index 0000000000..2d8b70efb9 --- /dev/null +++ b/cmd/geth/js_bzz_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/swarm" + "github.com/ethereum/go-ethereum/swarm/api" +) + +var port = 8500 + +func bzzREPL(t *testing.T, configf func(*api.Config)) (string, string, *testjethre, *node.Node) { + prvKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal("unable to generate key") + } + bzztmp, err := ioutil.TempDir("", "bzz-js-test") + config, err := api.NewConfig(bzztmp, common.Address{}, prvKey) + if err != nil { + t.Fatal("unable to configure swarm") + } + if configf != nil { + configf(config) + } + tmp, repl, stack := testREPL(t, func(n *node.Node) { + if err := n.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return swarm.NewSwarm(ctx, config, false) + }); err != nil { + t.Fatalf("Failed to register the Swarm service: %v", err) + } + }) + return bzztmp, tmp, repl, stack +} + +func withREPL(t *testing.T, cf func(*api.Config), f func(repl *testjethre)) { + bzztmp, tmp, repl, stack := bzzREPL(t, cf) + defer stack.Stop() + defer os.RemoveAll(tmp) + defer os.RemoveAll(bzztmp) + f(repl) +} + +func TestBzzPutGet(t *testing.T) { + withREPL(t, + func(c *api.Config) { + c.Port = "" + }, func(repl *testjethre) { + 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) { + withREPL(t, nil, func(repl *testjethre) { + if checkEvalJSON(t, repl, `hash = bzz.put("f42 = function() { return 42 }", "application/javascript")`, `"e6847876f00102441f850b2d438a06d10e3bf24e6a0a76d47b073a86c3c2f9ac"`) != nil { + return + } + if checkEvalJSON(t, repl, `admin.httpGet("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 ca636188f4..975f8dde5e 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -91,7 +91,7 @@ func testJEthRE(t *testing.T) (string, *testjethre, *node.Node) { return testREPL(t, nil) } -func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *node.Node) { +func testREPL(t *testing.T, config func(*node.Node)) (string, *testjethre, *node.Node) { tmp, err := ioutil.TempDir("", "geth-test") if err != nil { t.Fatal(err) @@ -107,17 +107,16 @@ func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *nod db, _ := ethdb.NewMemDatabase() core.WriteGenesisBlockForTesting(db, core.GenesisAccount{common.HexToAddress(testAddress), common.String2Big(testBalance)}) + coinbase := common.HexToAddress(testAddress) ethConf := ð.Config{ TestGenesisState: db, AccountManager: accman, DocRoot: "/", SolcPath: testSolcPath, + Etherbase: coinbase, PowTest: true, } - if config != nil { - config(ethConf) - } if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return eth.New(ctx, ethConf) }); err != nil { t.Fatalf("failed to register ethereum protocol: %v", err) } @@ -133,6 +132,12 @@ func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *nod if err := accman.Unlock(key.Address, ""); err != nil { t.Fatal(err) } + + // tests can register services here + if config != nil { + config(stack) + } + // Start the node and assemble the REPL tester if err := stack.Start(); err != nil { t.Fatalf("failed to start test stack: %v", err) @@ -267,10 +272,7 @@ func TestSignature(t *testing.T) { func TestContract(t *testing.T) { t.Skip("contract testing is implemented with mining in ethash test mode. This takes about 7seconds to run. Unskip and run on demand") coinbase := common.HexToAddress(testAddress) - tmp, repl, ethereum := testREPL(t, func(conf *eth.Config) { - conf.Etherbase = coinbase - conf.PowTest = true - }) + tmp, repl, ethereum := testREPL(t, nil) if err := ethereum.Start(); err != nil { t.Errorf("error starting ethereum: %v", err) return diff --git a/cmd/geth/main.go b/cmd/geth/main.go index bb291ccde2..6557de2df8 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -4,7 +4,7 @@ // go-ethereum is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. +// (at your option) any later version. // // go-ethereum is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -30,9 +30,7 @@ import ( "github.com/codegangsta/cli" "github.com/ethereum/ethash" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" @@ -316,6 +314,9 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso utils.IPCExperimental, utils.ExecFlag, utils.WhisperEnabledFlag, + utils.SwarmConfigPathFlag, + utils.SwarmAccountAddrFlag, + utils.ChequebookAddrFlag, utils.DevModeFlag, utils.TestNetFlag, utils.VMDebugFlag, @@ -427,6 +428,7 @@ func attach(ctx *cli.Context) { func console(ctx *cli.Context) { // Create and start the node based on the CLI flags node := utils.MakeSystemNode(ClientIdentifier, nodeNameVersion, makeDefaultExtra(), ctx) + startNode(ctx, node) // Attach to the newly started node, and either execute script or become interactive @@ -465,46 +467,14 @@ func execScripts(ctx *cli.Context) { node.Stop() } -// tries unlocking the specified account a few times. -func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (common.Address, string) { - account, err := utils.MakeAddress(accman, address) - if err != nil { - utils.Fatalf("Unlock error: %v", err) - } - - for trials := 0; trials < 3; trials++ { - prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) - password := getPassPhrase(prompt, false, i, passwords) - if err := accman.Unlock(account, password); err == nil { - return account, password - } - } - // All trials expended to unlock account, bail out - utils.Fatalf("Failed to unlock account: %s", address) - return common.Address{}, "" -} - -// startNode boots up the system node and all registered protocols, after which -// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the +// startNode unlocks any requested accounts the boots up the system node +// starts all registered protocols and +// starts the RPC/IPC interfaces and the // miner. func startNode(ctx *cli.Context, stack *node.Node) { // Start up the node itself utils.StartNode(stack) - // Unlock any account specifically requested - var ethereum *eth.Ethereum - if err := stack.Service(ðereum); err != nil { - utils.Fatalf("ethereum service not running: %v", err) - } - accman := ethereum.AccountManager() - passwords := utils.MakePasswordList(ctx) - - accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") - for i, account := range accounts { - if trimmed := strings.TrimSpace(account); trimmed != "" { - unlockAccount(ctx, accman, trimmed, i, passwords) - } - } // Start auxiliary services if enabled. if !ctx.GlobalBool(utils.IPCDisabledFlag.Name) { if err := utils.StartIPC(stack, ctx); err != nil { @@ -516,6 +486,10 @@ func startNode(ctx *cli.Context, stack *node.Node) { utils.Fatalf("Failed to start RPC: %v", err) } } + var ethereum *eth.Ethereum + if err := stack.Service(ðereum); err != nil { + utils.Fatalf("ethereum service not running: %v", err) + } if ctx.GlobalBool(utils.MiningEnabledFlag.Name) { if err := ethereum.StartMining(ctx.GlobalInt(utils.MinerThreadsFlag.Name), ctx.GlobalString(utils.MiningGPUFlag.Name)); err != nil { utils.Fatalf("Failed to start mining: %v", err) @@ -534,38 +508,10 @@ func accountList(ctx *cli.Context) { } } -// getPassPhrase retrieves the passwor associated with an account, either fetched -// from a list of preloaded passphrases, or requested interactively from the user. -func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { - // If a list of passwords was supplied, retrieve from them - if len(passwords) > 0 { - if i < len(passwords) { - return passwords[i] - } - return passwords[len(passwords)-1] - } - // Otherwise prompt the user for the password - fmt.Println(prompt) - password, err := utils.PromptPassword("Passphrase: ", true) - if err != nil { - utils.Fatalf("Failed to read passphrase: %v", err) - } - if confirmation { - confirm, err := utils.PromptPassword("Repeat passphrase: ", false) - if err != nil { - utils.Fatalf("Failed to read passphrase confirmation: %v", err) - } - if password != confirm { - utils.Fatalf("Passphrases do not match") - } - } - return password -} - // accountCreate creates a new account into the keystore defined by the CLI flags. func accountCreate(ctx *cli.Context) { accman := utils.MakeAccountManager(ctx) - password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) + password := utils.GetPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) account, err := accman.NewAccount(password) if err != nil { @@ -582,8 +528,8 @@ func accountUpdate(ctx *cli.Context) { } accman := utils.MakeAccountManager(ctx) - account, oldPassword := unlockAccount(ctx, accman, ctx.Args().First(), 0, nil) - newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) + account, oldPassword := utils.UnlockAccount(ctx, accman, ctx.Args().First(), 0, nil) + newPassword := utils.GetPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) if err := accman.Update(account, oldPassword, newPassword); err != nil { utils.Fatalf("Could not update the account: %v", err) } @@ -600,7 +546,7 @@ func importWallet(ctx *cli.Context) { } accman := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) + passphrase := utils.GetPassPhrase("", false, 0, utils.MakePasswordList(ctx)) acct, err := accman.ImportPreSaleKey(keyJson, passphrase) if err != nil { @@ -615,7 +561,7 @@ func accountImport(ctx *cli.Context) { utils.Fatalf("keyfile must be given as argument") } accman := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) + passphrase := utils.GetPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) acct, err := accman.Import(keyfile, passphrase) if err != nil { utils.Fatalf("Could not create the account: %v", err) diff --git a/cmd/utils/customflags.go b/cmd/utils/customflags.go index 4450065c14..faf531121d 100644 --- a/cmd/utils/customflags.go +++ b/cmd/utils/customflags.go @@ -39,7 +39,7 @@ func (self *DirectoryString) String() string { } func (self *DirectoryString) Set(value string) error { - self.Value = expandPath(value) + self.Value = ExpandPath(value) return nil } @@ -135,7 +135,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 { p = user.HomeDir + p[1:] diff --git a/cmd/utils/customflags_test.go b/cmd/utils/customflags_test.go index de39ca36a1..05e47abe4d 100644 --- a/cmd/utils/customflags_test.go +++ b/cmd/utils/customflags_test.go @@ -33,7 +33,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 abee6210f3..0e65daa7cb 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -55,6 +55,8 @@ import ( "github.com/ethereum/go-ethereum/rpc/shared" "github.com/ethereum/go-ethereum/rpc/useragent" rpc "github.com/ethereum/go-ethereum/rpc/v2" + "github.com/ethereum/go-ethereum/swarm" + bzzapi "github.com/ethereum/go-ethereum/swarm/api" "github.com/ethereum/go-ethereum/whisper" "github.com/ethereum/go-ethereum/xeth" ) @@ -351,6 +353,22 @@ var ( Name: "shh", Usage: "Enable Whisper", } + ChequebookAddrFlag = cli.StringFlag{ + Name: "chequebook", + Usage: "chequebook contract address", + } + SwarmAccountAddrFlag = cli.StringFlag{ + Name: "bzzaccount", + Usage: "Swarm account address (swarm disabled if empty)", + } + SwarmConfigPathFlag = cli.StringFlag{ + Name: "bzzconfig", + Usage: "Swarm config file path (datadir/bzz)", + } + SwarmSwapDisabled = cli.BoolFlag{ + Name: "bzznoswap", + Usage: "Swarm SWAP disabled (false)", + } // ATM the url is left to the user and deployment to JSpathFlag = cli.StringFlag{ Name: "jspath", @@ -582,6 +600,53 @@ func MakePasswordList(ctx *cli.Context) []string { return nil } +func UnlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (common.Address, string) { + // Try to unlock the specified account a few times + account, err := MakeAddress(accman, address) + if err != nil { + Fatalf("unable to unlock account %v: %v", address, err) + } + + for trials := 0; trials < 3; trials++ { + prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) + password := GetPassPhrase(prompt, false, i, passwords) + if err := accman.Unlock(account, password); err == nil { + return account, password + } + } + // All trials expended to unlock account, bail out + Fatalf("Failed to unlock account: %s", address) + return common.Address{}, "" +} + +// getPassPhrase retrieves the passwor associated with an account, either fetched +// from a list of preloaded passphrases, or requested interactively from the user. +func GetPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { + // If a list of passwords was supplied, retrieve from them + if len(passwords) > 0 { + if i < len(passwords) { + return passwords[i] + } + return passwords[len(passwords)-1] + } + // Otherwise prompt the user for the password + fmt.Println(prompt) + password, err := PromptPassword("Passphrase: ", true) + if err != nil { + Fatalf("Failed to read passphrase: %v", err) + } + if confirmation { + confirm, err := PromptPassword("Repeat passphrase: ", false) + if err != nil { + Fatalf("Failed to read passphrase confirmation: %v", err) + } + if password != confirm { + Fatalf("Passphrases do not match") + } + } + return password +} + // MakeSystemNode sets up a local node, configures the services to launch and // assembles the P2P protocol stack. func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node.Node { @@ -595,10 +660,12 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node. if networks > 1 { Fatalf("The %v flags are mutually exclusive", netFlags) } + datadir := MustMakeDataDir(ctx) + netprv := MakeNodeKey(ctx) // Configure the node's service container stackConf := &node.Config{ - DataDir: MustMakeDataDir(ctx), - PrivateKey: MakeNodeKey(ctx), + DataDir: datadir, + PrivateKey: netprv, Name: MakeNodeName(name, version, ctx), NoDiscovery: ctx.GlobalBool(NoDiscoverFlag.Name), BootstrapNodes: MakeBootstrapNodes(ctx), @@ -609,6 +676,14 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node. } // Configure the Ethereum service accman := MakeAccountManager(ctx) + passwords := MakePasswordList(ctx) + + accounts := strings.Split(ctx.GlobalString(UnlockedAccountFlag.Name), ",") + for i, account := range accounts { + if trimmed := strings.TrimSpace(account); trimmed != "" { + UnlockAccount(ctx, accman, trimmed, i, passwords) + } + } ethConf := ð.Config{ Genesis: MakeGenesisBlock(ctx), @@ -685,17 +760,49 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node. if err != nil { Fatalf("Failed to create the protocol stack: %v", err) } + + // eth.Ethereum: ethereum if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return eth.New(ctx, ethConf) }); err != nil { Fatalf("Failed to register the Ethereum service: %v", err) } + + // Whisper if shhEnable { if err := stack.Register(func(*node.ServiceContext) (node.Service, error) { return whisper.New(), nil }); err != nil { Fatalf("Failed to register the Whisper service: %v", err) } } + // bzz. Swarm + var bzzconfig *bzzapi.Config + hexaddr := ctx.GlobalString(SwarmAccountAddrFlag.Name) + if hexaddr != "" { + swarmaccount := common.HexToAddress(hexaddr) + if !accman.HasAccount(swarmaccount) { + Fatalf("swarm account '%v' does not exist: %v", hexaddr, err) + } + prvkey, err := accman.GetUnlocked(swarmaccount) + if err != nil { + Fatalf("unable to unlock swarm account: %v", err) + } + chbookaddr := common.HexToAddress(ctx.GlobalString(ChequebookAddrFlag.Name)) + bzzdir := ctx.GlobalString(SwarmConfigPathFlag.Name) + if bzzdir == "" { + bzzdir = filepath.Join(datadir, "bzz") + } + bzzconfig, err = bzzapi.NewConfig(bzzdir, chbookaddr, prvkey) + if err != nil { + Fatalf("unable to configure swarm: %v", err) + } + swap := ctx.GlobalBool(SwarmSwapDisabled.Name) + if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return swarm.NewSwarm(ctx, bzzconfig, swap) + }); err != nil { + Fatalf("Failed to register the Swarm service: %v", err) + } + } return stack } diff --git a/common/bytes.go b/common/bytes.go index c6e2cd3127..ba6987a9ec 100644 --- a/common/bytes.go +++ b/common/bytes.go @@ -23,14 +23,9 @@ import ( "encoding/hex" "fmt" "math/big" - "regexp" "strings" ) -var ( - hexRe = regexp.MustCompile("^(0x)?([a-fA-f0-9]{2})+$") -) - func ToHex(b []byte) string { hex := Bytes2Hex(b) // Prefer output of "0x0" instead of "0x" @@ -144,14 +139,8 @@ func HasHexPrefix(str string) bool { } func IsHex(str string) bool { - return hexRe.MatchString(str) -} - -func NormaliseHex(str string) (string, bool) { - if HasHexPrefix(str) { - str = str[2:] - } - return str, IsHex(str) + l := len(str) + return l >= 4 && l%2 == 0 && str[0:2] == "0x" } func Bytes2Hex(d []byte) string { @@ -213,8 +202,8 @@ func ParseData(data ...interface{}) (ret []byte) { switch t := item.(type) { case string: var str []byte - if n, ok := NormaliseHex(t); ok { - str = Hex2Bytes(n) + if IsHex(t) { + str = Hex2Bytes(t[2:]) } else { str = []byte(t) } diff --git a/common/chequebook/cheque.go b/common/chequebook/cheque.go new file mode 100644 index 0000000000..41c89418b0 --- /dev/null +++ b/common/chequebook/cheque.go @@ -0,0 +1,660 @@ +package chequebook + +import ( + "bytes" + "crypto/ecdsa" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "os" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/swap" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +Chequebook package is a go API to the 'chequebook' ethereum smart contract +With convenience methods that allow using chequebook for +* issuing, receiving, verifying cheques in ether +* (auto)cashing cheques in ether +* (auto)depositing ether to the chequebook contract +TODO: +* watch peer solvency and notify of bouncing cheques +* enable paying with cheque by signing off + +Some functionality require interacting with the blockchain: +* setting current balance on peer's chequebook +* sending the transaction to cash the cheque +* depositing ether to the chequebook +* watching incoming ether + +Backend is the interface for that +*/ + +const ( + gasToCash = "2000000" // gas cost of a cash transaction using chequebook + getSentAbiPre = "d75d691d" // sent amount accessor in the chequebook contract + cashAbiPre = "fbf788d6" // abi preamble signature for cash method of the chequebook + queryInterval = 15000000000 // 15 seconds + deployGas = "3000000" + // confirmationInterval = 3 * 10 * *11 // 5 minutes +) + +// Backend is the interface to interact with the Ethereum blockchain +// implemented by xeth.XEth +type Backend interface { + Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) + Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, string, error) + GetTxReceipt(txhash common.Hash) *types.Receipt + CodeAt(address string) string +} + +// rlp serialised cheque model for use with the chequebook +type Cheque struct { + // the address of the contract itself needed to avoid cross-contract submission + Contract common.Address // contract address + Beneficiary common.Address // beneficiary + Amount *big.Int // cumulative amount of all funds sent + Sig []byte // signature Sign(Sha3(contract, beneficiary, amount), prvKey) +} + +type Params struct { + ContractCode, ContractAbi, ContractSource string +} + +var ContractParams = &Params{ContractCode, ContractAbi, ContractSource} + +func (self *Cheque) String() string { + return fmt.Sprintf("contract: %s, beneficiary: %s, amount: %v, signature: %x", self.Contract.Hex(), self.Beneficiary.Hex(), self.Amount, self.Sig) +} + +// chequebook to create, sign cheques from single contract to multiple beneficiarys +// outgoing payment handler for peer to peer micropayments +type Chequebook struct { + path string // path to chequebook file + prvKey *ecdsa.PrivateKey // private key to sign cheque with + lock sync.Mutex // + backend Backend // blockchain API + quit chan bool // when closed causes autodeposit to stop + owner common.Address // owner address (derived from pubkey) + + // persisted fields + balance *big.Int // not synced with blockchain + contract common.Address // contract address + sent map[common.Address]*big.Int //tallies for beneficiarys + + txhash string // tx hash of last deposit tx + threshold *big.Int // threshold that triggers autodeposit if not nil + buffer *big.Int // buffer to keep on top of balance for fork protection +} + +func Deploy(owner common.Address, backend Backend, amount *big.Int, confirmationInterval, timeout time.Duration) (contract common.Address, err error) { + if (owner == common.Address{}) { + return contract, fmt.Errorf("invalid owner %v. Owner needed to deploy chequebook contract: %v", owner.Hex(), err) + } + + txhash, err := backend.Transact(owner.Hex(), "", "", amount.String(), deployGas, "", ContractCode) + if err != nil { + return contract, fmt.Errorf("unable to send chequebook creation transaction: %v", err) + } + + timer := time.NewTimer(timeout).C + ticker := time.NewTicker(queryInterval).C +OUT: + for { + select { + + case <-timer: + if ticker == nil { + // ticker is nil, receipt was found and confirmation interval passed + err = Validate(contract, backend) + if err != nil { + return contract, fmt.Errorf("invalid contract at %v after %v: %v", contract.Hex(), confirmationInterval, err) + } + break OUT + } + // ticker is non-nil meaning receipt not found yet + return contract, fmt.Errorf("chequebook deployment timed out in %v", timeout) + + case <-ticker: + receipt := backend.GetTxReceipt(common.HexToHash(txhash)) + if receipt != nil { + contract = receipt.ContractAddress + glog.V(logger.Detail).Infof("[CHEQUEBOOK] chequebook deployed at %v (owner: %v)", contract.Hex(), owner.Hex()) + timer = time.NewTimer(confirmationInterval).C + ticker = nil + } else { + glog.V(logger.Detail).Infof("[CHEQUEBOOK] check if chequebook deployed (txhash: %v)", txhash) + } + + } + } + return contract, nil +} + +func Validate(contract common.Address, backend Backend) (err error) { + if (contract == common.Address{}) { + return fmt.Errorf("zero address") + } + code := backend.CodeAt(contract.Hex()) + if code != ContractDeployedCode { + return fmt.Errorf("incorrect code %v:\n%v\n%v", contract.Hex(), code, ContractDeployedCode) + } + return nil +} + +// NewChequebook(path, contract, balance, prvKey) creates a new Chequebook +func NewChequebook(path string, contract common.Address, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) { + balance := new(big.Int) + sent := make(map[common.Address]*big.Int) + owner := crypto.PubkeyToAddress(prvKey.PublicKey) + self = &Chequebook{ + balance: balance, + contract: contract, + sent: sent, + path: path, + prvKey: prvKey, + backend: backend, + owner: owner, + } + if (contract != common.Address{}) { + glog.V(logger.Detail).Infof("[CHEQUEBOOK] new chequebook initialised from %v (owner: %v)", contract.Hex(), owner.Hex()) + } + return +} + +// LoadChequebook(path, prvKey, backend) loads a chequebook from disk (file path) +func LoadChequebook(path string, prvKey *ecdsa.PrivateKey, backend Backend) (self *Chequebook, err error) { + var data []byte + data, err = ioutil.ReadFile(path) + if err != nil { + return + } + + self, _ = NewChequebook(path, common.Address{}, prvKey, backend) + + err = json.Unmarshal(data, self) + if err != nil { + return nil, err + } + glog.V(logger.Detail).Infof("[CHEQUEBOOK] loaded chequebook (%s, owner: %v) initialised from %v", self.contract.Hex(), self.owner.Hex(), path) + + return +} + +// chequebook serialisation +type chequebookFile struct { + Balance string + Contract string + Owner string + Sent map[string]string +} + +func (self *Chequebook) UnmarshalJSON(data []byte) error { + var file chequebookFile + err := json.Unmarshal(data, &file) + if err != nil { + return err + } + _, ok := self.balance.SetString(file.Balance, 10) + if !ok { + return fmt.Errorf("cumulative amount sent: unable to convert string to big integer: %v", file.Balance) + } + self.contract = common.HexToAddress(file.Contract) + for addr, sent := range file.Sent { + self.sent[common.HexToAddress(addr)], ok = new(big.Int).SetString(sent, 10) + if !ok { + return fmt.Errorf("beneficiary %v cumulative amount sent: unable to convert string to big integer: %v", addr, sent) + } + } + return nil +} + +func (self *Chequebook) MarshalJSON() ([]byte, error) { + var file = &chequebookFile{ + Balance: self.balance.String(), + Contract: self.contract.Hex(), + Owner: self.owner.Hex(), + Sent: make(map[string]string), + } + for addr, sent := range self.sent { + file.Sent[addr.Hex()] = sent.String() + } + return json.Marshal(file) +} + +// Save() persists the chequebook on disk +// remembers balance, contract address and +// cumulative amount of funds sent for each beneficiary +func (self *Chequebook) Save() (err error) { + data, err := json.MarshalIndent(self, "", " ") + if err != nil { + return err + } + glog.V(logger.Detail).Infof("[CHEQUEBOOK] saving chequebook (%s) to %v", self.contract.Hex(), self.path) + + return ioutil.WriteFile(self.path, data, os.ModePerm) +} + +// Stop() quits the autodeposit go routine to terminate +func (self *Chequebook) Stop() { + defer self.lock.Unlock() + self.lock.Lock() + if self.quit != nil { + close(self.quit) + self.quit = nil + } +} + +// Issue(beneficiary, amount) will create a Cheque +// the cheque is signed by the chequebook owner's private key +// the signer commits to a contract (one that they own), a beneficiary and amount +func (self *Chequebook) Issue(beneficiary common.Address, amount *big.Int) (ch *Cheque, err error) { + defer self.lock.Unlock() + self.lock.Lock() + if amount.Cmp(common.Big0) <= 0 { + return nil, fmt.Errorf("amount must be greater than zero (%v)", amount) + } + if self.balance.Cmp(amount) < 0 { + err = fmt.Errorf("insufficent funds to issue cheque for amount: %v. balance: %v", amount, self.balance) + } else { + var sig []byte + sent, found := self.sent[beneficiary] + if !found { + sent = new(big.Int) + self.sent[beneficiary] = sent + } + sum := new(big.Int).Set(sent) + sum.Add(sum, amount) + sig, err = crypto.Sign(sigHash(self.contract, beneficiary, sum), self.prvKey) + if err == nil { + ch = &Cheque{ + Contract: self.contract, + Beneficiary: beneficiary, + Amount: sum, + Sig: sig, + } + sent.Set(sum) + self.balance.Sub(self.balance, amount) // subtract amount from balance + } + } + + // auto deposit if threshold is set and balance is less then threshold + // note this is called even if issueing cheque fails + // so we reattempt depositing + if self.threshold != nil { + if self.balance.Cmp(self.threshold) < 0 { + send := new(big.Int).Sub(self.buffer, self.balance) + self.deposit(send) + } + } + + return +} + +// convenience method to cash any cheque +func (self *Chequebook) Cash(ch *Cheque) (txhash string, err error) { + return ch.Cash(self.owner, self.backend) +} + +// data to sign: contract address, beneficiary, cumulative amount of funds ever sent +func sigHash(contract, beneficiary common.Address, sum *big.Int) []byte { + bigamount := sum.Bytes() + if len(bigamount) > 32 { + return nil + } + var amount32 [32]byte + copy(amount32[32-len(bigamount):32], bigamount) + input := append(contract.Bytes(), beneficiary.Bytes()...) + input = append(input, amount32[:]...) + return crypto.Sha3(input) +} + +// Balance() public accessor for balance +func (self *Chequebook) Balance() *big.Int { + defer self.lock.Unlock() + self.lock.Lock() + return new(big.Int).Set(self.balance) +} + +// Balance() public accessor for balance +func (self *Chequebook) Owner() common.Address { + return self.owner +} + +// Backend() public accessor for backend +func (self *Chequebook) Backend() Backend { + return self.backend +} + +// Address() public accessor for contract +func (self *Chequebook) Address() common.Address { + return self.contract +} + +// Deposit(amount) deposits amount to the chequebook account +func (self *Chequebook) Deposit(amount *big.Int) (string, error) { + defer self.lock.Unlock() + self.lock.Lock() + return self.deposit(amount) +} + +// deposit(amount) deposits amount to the chequebook account +// caller holds the lock +func (self *Chequebook) deposit(amount *big.Int) (string, error) { + txhash, err := self.backend.Transact(self.owner.Hex(), self.contract.Hex(), "", amount.String(), "", "", "") + // assume that transaction is actually successful, we add the amount to balance right away + if err != nil { + glog.V(logger.Warn).Infof("[CHEQUEBOOK] error depositing %d wei to chequebook (%s, balance: %v, target: %v): %v", amount, self.contract.Hex(), self.balance, self.buffer, err) + } else { + self.balance.Add(self.balance, amount) + glog.V(logger.Detail).Infof("[CHEQUEBOOK] deposited %d wei to chequebook (%s, balance: %v, target: %v)", amount, self.contract.Hex(), self.balance, self.buffer) + } + return txhash, err +} + +// AutoDeposit(interval, threshold, buffer) (re)sets interval time and amount +// which triggers sending funds to the chequebook contract +// backend needs to be set +// if threshold is not less than buffer, then deposit will be triggered on +// every new cheque issued +func (self *Chequebook) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { + defer self.lock.Unlock() + self.lock.Lock() + self.threshold = threshold + self.buffer = buffer + self.autoDeposit(interval) +} + +// autoDeposit(interval) starts a go routine that periodically sends funds to the +// chequebook contract +// caller holds the lock +// the go routine terminates if Chequebook.quit us closed +func (self *Chequebook) autoDeposit(interval time.Duration) { + if self.quit != nil { + close(self.quit) + self.quit = nil + } + // if threshold >= balance autodeposit after every cheque issued + if interval == time.Duration(0) || self.threshold != nil && self.buffer != nil && self.threshold.Cmp(self.buffer) >= 0 { + return + } + + ticker := time.NewTicker(interval) + self.quit = make(chan bool) + quit := self.quit + go func() { + FOR: + for { + select { + case <-quit: + break FOR + case <-ticker.C: + self.lock.Lock() + if self.balance.Cmp(self.buffer) < 0 { + amount := new(big.Int).Sub(self.buffer, self.balance) + txhash, err := self.deposit(amount) + if err == nil { + self.txhash = txhash + } + } + self.lock.Unlock() + } + } + }() + return +} + +type Outbox struct { + chequeBook *Chequebook + beneficiary common.Address +} + +func NewOutbox(chbook *Chequebook, beneficiary common.Address) *Outbox { + return &Outbox{chbook, beneficiary} +} + +func (self *Outbox) Issue(amount *big.Int) (swap.Promise, error) { + return self.chequeBook.Issue(self.beneficiary, amount) +} + +func (self *Outbox) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { + self.chequeBook.AutoDeposit(interval, threshold, buffer) +} + +func (self *Outbox) Stop() {} + +func (self *Outbox) String() string { + return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.chequeBook.Address().Hex(), self.beneficiary.Hex(), self.chequeBook.Balance()) +} + +// type ChequeQueue struct { +// beneficiary common.Address +// last map[string]*Inbox +// } + +// inbox to deposit, verify and cash cheques +// from a single contract to single beneficiary +// incoming payment handler for peer to peer micropayments +type Inbox struct { + lock sync.Mutex + contract common.Address // peer's chequebook contract + beneficiary common.Address // local peer's receiving address + sender common.Address // local peer's address to send cashing tx from + signer *ecdsa.PublicKey // peer's public key + txhash string // tx hash of last cashing tx + backend Backend // blockchain API + quit chan bool // when closed causes autocash to stop + maxUncashed *big.Int // threshold that triggers autocashing + cashed *big.Int // cumulative amount cashed + cheque *Cheque // last cheque, nil if none yet received +} + +// NewInbox(contract, beneficiary, signer, backend) constructor for Inbox +// not persisted, cumulative sum updated from blockchain when first cheque received +// backend used to sync amount (Call) as well as cash the cheques (Transact) +func NewInbox(contract, sender, beneficiary common.Address, signer *ecdsa.PublicKey, backend Backend) (self *Inbox, err error) { + + if signer == nil { + return nil, fmt.Errorf("signer is null") + } + + self = &Inbox{ + contract: contract, + beneficiary: beneficiary, + sender: sender, + signer: signer, + backend: backend, + cashed: new(big.Int).Set(common.Big0), + } + glog.V(logger.Detail).Infof("[CHEQUEBOOK] initialised inbox (%s -> %s) expected signer: %x", self.contract.Hex(), self.beneficiary.Hex(), crypto.FromECDSAPub(signer)) + return +} + +func (self *Inbox) String() string { + return fmt.Sprintf("chequebook: %v, beneficiary: %s, balance: %v", self.contract.Hex(), self.beneficiary.Hex(), self.cheque.Amount) +} + +// Stop() quits the autocash go routine to terminate +func (self *Inbox) Stop() { + defer self.lock.Unlock() + self.lock.Lock() + if self.quit != nil { + close(self.quit) + self.quit = nil + } +} + +func (self *Inbox) Cash() (txhash string, err error) { + if self.cheque != nil { + txhash, err = self.cheque.Cash(self.sender, self.backend) + glog.V(logger.Detail).Infof("[CHEQUEBOOK] cashing cheque (total: %v) on chequebook (%s) sending to %v", self.cheque.Amount, self.contract.Hex(), self.beneficiary.Hex()) + self.cashed = self.cheque.Amount + } + return +} + +// AutoCash(cashInterval, maxUncashed) (re)sets maximum time and amount which +// triggers cashing of the last uncashed cheque +// if maxUncashed is set to 0, then autocash on receipt +func (self *Inbox) AutoCash(cashInterval time.Duration, maxUncashed *big.Int) { + defer self.lock.Unlock() + self.lock.Lock() + self.maxUncashed = maxUncashed + self.autoCash(cashInterval) +} + +// autoCash(d) starts a loop that periodically clears the last check +// if the peer is trusted, clearing period could be 24h, or a week +// caller holds the lock +func (self *Inbox) autoCash(cashInterval time.Duration) { + if self.quit != nil { + close(self.quit) + self.quit = nil + } + // if maxUncashed is set to 0, then autocash on receipt + if cashInterval == time.Duration(0) || self.maxUncashed != nil && self.maxUncashed.Cmp(common.Big0) == 0 { + return + } + + ticker := time.NewTicker(cashInterval) + self.quit = make(chan bool) + quit := self.quit + go func() { + FOR: + for { + select { + case <-quit: + break FOR + case <-ticker.C: + self.lock.Lock() + if self.cheque != nil && self.cheque.Amount.Cmp(self.cashed) != 0 { + txhash, err := self.Cash() + if err == nil { + self.txhash = txhash + } + } + self.lock.Unlock() + } + } + }() + return +} + +// Reveive(cheque) called to deposit latest cheque to incoming Inbox +func (self *Inbox) Receive(promise swap.Promise) (*big.Int, error) { + ch := promise.(*Cheque) + + defer self.lock.Unlock() + self.lock.Lock() + var sum *big.Int + if self.cheque == nil { + // the sum is checked against the blockchain once a check is received + tallyhex, _, err := self.backend.Call(self.beneficiary.Hex(), self.contract.Hex(), "", "", "", getSentAbiEncode(ch.Contract)) + if err != nil { + return nil, fmt.Errorf("inbox: error calling backend to set amount: %v", err) + } + + tally := common.FromHex(tallyhex) + // var ok bool + // sum, ok = new(big.Int).SetString(tally, 10) + // if !ok { + // return nil, fmt.Errorf("inbox: cannot convert amount '%s' (%v) to integer", tallyhex, tally) + // } + sum = new(big.Int).SetBytes(tally) + } else { + sum = self.cheque.Amount + } + + amount, err := ch.Verify(self.signer, self.contract, self.beneficiary, sum) + var uncashed *big.Int + if err == nil { + self.cheque = ch + + if self.maxUncashed != nil { + uncashed = new(big.Int).Sub(ch.Amount, self.cashed) + if self.maxUncashed.Cmp(uncashed) < 0 { + self.Cash() + } + } + glog.V(logger.Detail).Infof("[CHEQUEBOOK] received cheque of %v wei in inbox (%s, uncashed: %v)", amount, self.contract.Hex(), uncashed) + } + + return amount, err +} + +// RSV representation of signature +func sig2rsv(sig []byte) (v byte, r, s []byte) { + v = sig[64] + 27 + r = sig[:32] + s = sig[32:64] + return +} + +func getSentAbiEncode(beneficiary common.Address) string { + var beneficiary32 [32]byte + copy(beneficiary32[12:], beneficiary.Bytes()) + return getSentAbiPre + common.Bytes2Hex(beneficiary32[:]) +} + +// abi encoding of a cheque to send as eth tx data +func (self *Cheque) cashAbiEncode() string { + v, r, s := sig2rsv(self.Sig) + // cashAbiPre, beneficiary, amount, v, r, s + bigamount := self.Amount.Bytes() + if len(bigamount) > 32 { + glog.V(logger.Detail).Infof("[CHEQUEBOOK] number too big: %v (>32 bytes)", self.Amount) + return "" + } + var beneficiary32, amount32, vabi [32]byte + copy(beneficiary32[12:], self.Beneficiary.Bytes()) + copy(amount32[32-len(bigamount):32], bigamount) + vabi[31] = v + return cashAbiPre + common.Bytes2Hex(beneficiary32[:]) + common.Bytes2Hex(amount32[:]) + + common.Bytes2Hex(vabi[:]) + common.Bytes2Hex(r) + common.Bytes2Hex(s) +} + +// Verify(cheque) verifies cheque for signer, contract, beneficiary, amount, valid signature +func (self *Cheque) Verify(signerKey *ecdsa.PublicKey, contract, beneficiary common.Address, sum *big.Int) (*big.Int, error) { + glog.V(logger.Detail).Infof("[CHEQUEBOOK] verify cheque: %v - sum: %v", self, sum) + if sum == nil { + return nil, fmt.Errorf("invalid amount") + } + + if self.Beneficiary != beneficiary { + return nil, fmt.Errorf("beneficiary mismatch: %v != %v", self.Beneficiary.Hex(), beneficiary.Hex()) + } + if self.Contract != contract { + return nil, fmt.Errorf("contract mismatch: %v != %v", self.Contract.Hex(), contract.Hex()) + } + + amount := new(big.Int).Set(self.Amount) + if sum != nil { + amount.Sub(amount, sum) + if amount.Cmp(common.Big0) <= 0 { + return nil, fmt.Errorf("incorrect amount: %v <= 0", amount) + } + } + + pubKey, err := crypto.SigToPub(sigHash(self.Contract, beneficiary, self.Amount), self.Sig) + if err != nil { + return nil, fmt.Errorf("invalid signature: %v", err) + } + if !bytes.Equal(crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) { + return nil, fmt.Errorf("signer mismatch: %x != %x", crypto.FromECDSAPub(pubKey), crypto.FromECDSAPub(signerKey)) + } + return amount, nil +} + +// Cash(backend) will cash the check using xeth backend to send a transaction +// Beneficiary address should be unlocked +func (self *Cheque) Cash(sender common.Address, backend Backend) (string, error) { + return backend.Transact(sender.Hex(), self.Contract.Hex(), "", "", "", gasToCash, self.cashAbiEncode()) +} diff --git a/common/chequebook/cheque_test.go b/common/chequebook/cheque_test.go new file mode 100644 index 0000000000..06d95ff380 --- /dev/null +++ b/common/chequebook/cheque_test.go @@ -0,0 +1,498 @@ +package chequebook + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +type testBackend struct { + calls []string + errs []error + txs []string +} + +func newTestBackend() *testBackend { + return &testBackend{} +} + +func (b *testBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) { + txhash := string(crypto.Sha3([]byte(codeStr))) + b.txs = append(b.txs, txhash) + return txhash, nil +} + +func (b *testBackend) Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, string, error) { + if len(b.calls) == 0 { + panic("test backend called too many times") + } + res := b.calls[0] + err := b.errs[0] + b.calls = b.calls[1:] + b.errs = b.errs[1:] + return res, "", err +} + +func (b *testBackend) GetTxReceipt(txhash common.Hash) *types.Receipt { + return nil +} + +func (b *testBackend) CodeAt(address string) string { + return "" +} + +func genAddr() common.Address { + prvKey, _ := crypto.GenerateKey() + return crypto.PubkeyToAddress(prvKey.PublicKey) +} + +func TestIssueAndReceive(t *testing.T) { + prvKey, _ := crypto.GenerateKey() + sender := genAddr() + path := "/tmp/checkbook.json" + chbook, err := NewChequebook(path, sender, prvKey, nil) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + recipient := genAddr() + chbook.sent[recipient] = new(big.Int).SetUint64(42) + amount := common.Big1 + ch, err := chbook.Issue(recipient, amount) + if err == nil { + t.Errorf("expected insufficient funds error, got none") + } + + chbook.balance = new(big.Int).Set(common.Big1) + if chbook.Balance().Cmp(common.Big1) != 0 { + t.Errorf("expected: %v, got %v", "0", chbook.Balance()) + } + + ch, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if chbook.Balance().Cmp(common.Big0) != 0 { + t.Errorf("expected: %v, got %v", "0", chbook.Balance()) + } + + backend := newTestBackend() + backend.calls = []string{common.ToHex(big.NewInt(42).Bytes())} + backend.errs = []error{nil} + chbox, err := NewInbox(sender, recipient, recipient, &prvKey.PublicKey, backend) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + received, err := chbox.Receive(ch) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if received.Cmp(common.Big1) != 0 { + t.Errorf("expected: %v, got %v", "1", received) + } + +} + +func TestCheckbookFile(t *testing.T) { + prvKey, _ := crypto.GenerateKey() + sender := genAddr() + path := "/tmp/checkbook.json" + chbook, err := NewChequebook(path, sender, prvKey, nil) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + recipient := genAddr() + chbook.sent[recipient] = new(big.Int).SetUint64(42) + chbook.balance = new(big.Int).Set(common.Big1) + + chbook.Save() + + chbook, err = LoadChequebook(path, prvKey, nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if chbook.Balance().Cmp(common.Big1) != 0 { + t.Errorf("expected: %v, got %v", "0", chbook.Balance()) + } + + ch, err := chbook.Issue(recipient, common.Big1) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if ch.Amount.Cmp(new(big.Int).SetUint64(43)) != 0 { + t.Errorf("expected: %v, got %v", "0", ch.Amount) + } + + err = chbook.Save() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestVerifyErrors(t *testing.T) { + prvKey, _ := crypto.GenerateKey() + sender0 := genAddr() + sender1 := genAddr() + path0 := "/tmp/checkbook0.json" + chbook0, err := NewChequebook(path0, sender0, prvKey, nil) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + path1 := "/tmp/checkbook1.json" + chbook1, err := NewChequebook(path1, sender1, prvKey, nil) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + recipient0 := genAddr() + recipient1 := genAddr() + chbook0.balance = new(big.Int).Set(common.Big2) + chbook1.balance = new(big.Int).Set(common.Big1) + chbook0.sent[recipient0] = new(big.Int).SetUint64(42) + amount := common.Big1 + ch0, err := chbook0.Issue(recipient0, amount) + + backend := newTestBackend() + backend.calls = []string{common.ToHex(big.NewInt(42).Bytes())} + backend.errs = []error{nil} + chbox, err := NewInbox(sender0, recipient0, recipient0, &prvKey.PublicKey, backend) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + received, err := chbox.Receive(ch0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if received.Cmp(common.Big1) != 0 { + t.Errorf("expected: %v, got %v", "1", received) + } + + ch1, err := chbook0.Issue(recipient1, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + received, err = chbox.Receive(ch1) + t.Log(err) + if err == nil { + t.Fatalf("expected receiver error, got none") + } + + ch2, err := chbook1.Issue(recipient0, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + received, err = chbox.Receive(ch2) + t.Log(err) + if err == nil { + t.Fatalf("expected sender error, got none") + } + + _, err = chbook1.Issue(recipient0, new(big.Int).SetInt64(-1)) + t.Log(err) + if err == nil { + t.Fatalf("expected incorrect amount error, got none") + } + + received, err = chbox.Receive(ch0) + t.Log(err) + if err == nil { + t.Fatalf("expected incorrect amount error, got none") + } + +} + +func TestDeposit(t *testing.T) { + prvKey, _ := crypto.GenerateKey() + sender := genAddr() + path := "/tmp/checkbook.json" + + backend := newTestBackend() + chbook, err := NewChequebook(path, sender, prvKey, backend) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + balance := new(big.Int).SetUint64(42) + chbook.Deposit(balance) + if len(backend.txs) != 1 { + t.Fatalf("expected 1 txs to send, got %v", len(backend.txs)) + } + if chbook.balance.Cmp(balance) != 0 { + t.Fatalf("expected balance %v, got %v", balance, chbook.balance) + } + + recipient := genAddr() + amount := common.Big1 + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + exp := new(big.Int).SetUint64(41) + if chbook.balance.Cmp(exp) != 0 { + t.Fatalf("expected balance %v, got %v", exp, chbook.balance) + } + + // autodeposit on each issue + chbook.AutoDeposit(0, balance, balance) + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(backend.txs) != 3 { + t.Fatalf("expected 3 txs to send, got %v", len(backend.txs)) + } + if chbook.balance.Cmp(balance) != 0 { + t.Fatalf("expected balance %v, got %v", balance, chbook.balance) + } + + // autodeposit off + chbook.AutoDeposit(0, common.Big0, balance) + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(backend.txs) != 3 { + t.Fatalf("expected 3 txs to send, got %v", len(backend.txs)) + } + exp = new(big.Int).SetUint64(40) + if chbook.balance.Cmp(exp) != 0 { + t.Fatalf("expected balance %v, got %v", exp, chbook.balance) + } + + // autodeposit every 10ms if new cheque issued + interval := 30 * time.Millisecond + chbook.AutoDeposit(interval, common.Big1, balance) + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(backend.txs) != 3 { + t.Fatalf("expected 3 txs to send, got %v", len(backend.txs)) + } + exp = new(big.Int).SetUint64(38) + if chbook.balance.Cmp(exp) != 0 { + t.Fatalf("expected balance %v, got %v", exp, chbook.balance) + } + + time.Sleep(3 * interval) + if len(backend.txs) != 4 { + t.Fatalf("expected 4 txs to send, got %v", len(backend.txs)) + } + if chbook.balance.Cmp(balance) != 0 { + t.Fatalf("expected balance %v, got %v", balance, chbook.balance) + } + + exp = new(big.Int).SetUint64(40) + chbook.AutoDeposit(4*interval, exp, balance) + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + time.Sleep(3 * interval) + if len(backend.txs) != 4 { + t.Fatalf("expected 4 txs to send, got %v", len(backend.txs)) + } + if chbook.balance.Cmp(exp) != 0 { + t.Fatalf("expected balance %v, got %v", exp, chbook.balance) + } + + _, err = chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + time.Sleep(1 * interval) + + if len(backend.txs) != 5 { + t.Fatalf("expected 5 txs to send, got %v", len(backend.txs)) + } + if chbook.balance.Cmp(balance) != 0 { + t.Fatalf("expected balance %v, got %v", balance, chbook.balance) + } + + chbook.AutoDeposit(1*interval, common.Big0, balance) + chbook.Stop() + + _, err = chbook.Issue(recipient, common.Big1) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + _, err = chbook.Issue(recipient, common.Big2) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + time.Sleep(1 * interval) + + if len(backend.txs) != 5 { + t.Fatalf("expected 5 txs to send, got %v", len(backend.txs)) + } + exp = new(big.Int).SetUint64(39) + if chbook.balance.Cmp(exp) != 0 { + t.Fatalf("expected balance %v, got %v", exp, chbook.balance) + } + +} + +func TestCash(t *testing.T) { + prvKey, _ := crypto.GenerateKey() + sender := genAddr() + path := "/tmp/checkbook.json" + chbook, err := NewChequebook(path, sender, prvKey, nil) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + recipient := genAddr() + chbook.sent[recipient] = new(big.Int).SetUint64(42) + amount := common.Big1 + chbook.balance = new(big.Int).Set(common.Big1) + ch, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + backend := newTestBackend() + backend.calls = []string{common.ToHex(big.NewInt(42).Bytes())} + backend.errs = []error{nil} + chbox, err := NewInbox(sender, recipient, recipient, &prvKey.PublicKey, backend) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // cashing latest cheque + _, err = chbox.Receive(ch) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = ch.Cash(recipient, backend) + if len(backend.txs) != 1 { + t.Fatalf("expected 1 txs to send, got %v", len(backend.txs)) + } + + chbook.balance = new(big.Int).Set(common.Big3) + ch0, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + ch1, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + interval := 10 * time.Millisecond + // setting autocash with interval of 10ms + chbox.AutoCash(interval, nil) + _, err = chbox.Receive(ch0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbox.Receive(ch1) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + // after < interval time and 2 cheques received, no new cashing tx is sent + if len(backend.txs) != 1 { + t.Fatalf("expected 1 txs to send, got %v", len(backend.txs)) + } + // after 3x interval time and 2 cheques received, exactly one cashing tx is sent + time.Sleep(4 * interval) + if len(backend.txs) != 2 { + t.Fatalf("expected 2 txs to send, got %v", len(backend.txs)) + } + + // after stopping autocash no more tx are sent + ch2, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + chbox.Stop() + time.Sleep(interval) // make sure loop stops + _, err = chbox.Receive(ch2) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + time.Sleep(2 * interval) + if len(backend.txs) != 2 { + t.Fatalf("expected 2 txs to send, got %v", len(backend.txs)) + } + + // autocash below 1 + chbook.balance = new(big.Int).Set(common.Big2) + chbox.AutoCash(0, common.Big1) + + ch3, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + ch4, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + _, err = chbox.Receive(ch3) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbox.Receive(ch4) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // 2 checks of amount 1 received, exactly 1 tx is sent + if len(backend.txs) != 3 { + t.Fatalf("expected 3 txs to send, got %v", len(backend.txs)) + } + + // autochash on receipt when maxUncashed is 0 + chbook.balance = new(big.Int).Set(common.Big2) + chbox.AutoCash(0, common.Big0) + + ch5, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + ch6, err := chbook.Issue(recipient, amount) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbox.Receive(ch5) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + _, err = chbox.Receive(ch6) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(backend.txs) != 5 { + t.Fatalf("expected 5 txs to send, got %v", len(backend.txs)) + } + +} diff --git a/common/chequebook/chequebook.sol b/common/chequebook/chequebook.sol new file mode 100644 index 0000000000..836d570472 --- /dev/null +++ b/common/chequebook/chequebook.sol @@ -0,0 +1,49 @@ +import "mortal"; + +/// @title Chequebook for Ethereum micropayments +/// @author Daniel A. Nagy +contract chequebook is mortal { + // Cumulative paid amount in wei to each beneficiary + mapping (address => uint256) sent; + + /// @notice Overdraft event + event Overdraft(address deadbeat); + + /// @notice Accessor for sent map + /// + /// @param beneficiary beneficiary address + /// @return cumulative amount in wei sent to beneficiary + function getSent(address beneficiary) constant returns (uint256) { + return sent[beneficiary]; + } + + /// @notice Cash cheque + /// + /// @param beneficiary beneficiary address + /// @param amount cumulative amount in wei + /// @param sig_v signature parameter v + /// @param sig_r signature parameter r + /// @param sig_s signature parameter s + /// The digital signature is calculated on the concatenated triplet of contract address, beneficiary address and cumulative amount + function cash(address beneficiary, uint256 amount, + uint8 sig_v, bytes32 sig_r, bytes32 sig_s) { + // Check if the cheque is old. + // Only cheques that are more recent than the last cashed one are considered. + if(amount <= sent[beneficiary]) return; + // Check the digital signature of the cheque. + bytes32 hash = sha3(address(this), beneficiary, amount); + if(owner != ecrecover(hash, sig_v, sig_r, sig_s)) return; + // Attempt sending the difference between the cumulative amount on the cheque + // and the cumulative amount on the last cashed cheque to beneficiary. + if (beneficiary.send(amount - sent[beneficiary])) { + // Upon success, update the cumulative amount. + sent[beneficiary] = amount; + } else { + // Upon failure, punish owner for writing a bounced cheque. + // owner.sendToDebtorsPrison(); + Overdraft(owner); + // Compensate beneficiary. + suicide(beneficiary); + } + } +} diff --git a/common/chequebook/contract.go b/common/chequebook/contract.go new file mode 100644 index 0000000000..866d7c3358 --- /dev/null +++ b/common/chequebook/contract.go @@ -0,0 +1,63 @@ +package chequebook + +import () + +const ( + ContractCode = `606060405260008054600160a060020a03191633179055610201806100246000396000f3606060405260e060020a600035046341c0e1b58114610031578063d75d691d14610059578063fbf788d61461007e575b005b61002f60005433600160a060020a03908116911614156101ff57600054600160a060020a0316ff5b600160a060020a03600435166000908152600160205260409020546060908152602090f35b61002f600435602435604435606435608435600160a060020a03851660009081526001602052604081205485116100b8575b505050505050565b30600160a060020a039081166c0100000000000000000000000090810260609081529188160260745260888690526048812080825260ff8616608090815260a086905260c0859052909260019260e0926020928290866161da5a03f11561000257505060405151600054600160a060020a03908116911614610139576100b0565b85600160a060020a031660006001600050600089600160a060020a03168152602001908152602001600020600050548703604051809050600060405180830381858888f19350505050156101b357846001600050600088600160a060020a03168152602001908152602001600020600050819055506100b0565b60005460408051600160a060020a03929092168252517f2250e2993c15843b32621c89447cc589ee7a9f049c026986e545d3c2c0c6f9789181900360200190a185600160a060020a0316ff5b56` + + ContractDeployedCode = `0x606060405260e060020a600035046341c0e1b58114610031578063d75d691d14610059578063fbf788d61461007e575b005b61002f60005433600160a060020a03908116911614156101ff57600054600160a060020a0316ff5b600160a060020a03600435166000908152600160205260409020546060908152602090f35b61002f600435602435604435606435608435600160a060020a03851660009081526001602052604081205485116100b8575b505050505050565b30600160a060020a039081166c0100000000000000000000000090810260609081529188160260745260888690526048812080825260ff8616608090815260a086905260c0859052909260019260e0926020928290866161da5a03f11561000257505060405151600054600160a060020a03908116911614610139576100b0565b85600160a060020a031660006001600050600089600160a060020a03168152602001908152602001600020600050548703604051809050600060405180830381858888f19350505050156101b357846001600050600088600160a060020a03168152602001908152602001600020600050819055506100b0565b60005460408051600160a060020a03929092168252517f2250e2993c15843b32621c89447cc589ee7a9f049c026986e545d3c2c0c6f9789181900360200190a185600160a060020a0316ff5b56` + + ContractAbi = `[{"constant":false,"inputs":[],"name":"kill","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"beneficiary","type":"address"}],"name":"getSent","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"beneficiary","type":"address"},{"name":"amount","type":"uint256"},{"name":"sig_v","type":"uint8"},{"name":"sig_r","type":"bytes32"},{"name":"sig_s","type":"bytes32"}],"name":"cash","outputs":[],"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"deadbeat","type":"address"}],"name":"Overdraft","type":"event"}]` + + ContractSource = ` +import "mortal"; + +/// @title Chequebook for Ethereum micropayments +/// @author Daniel A. Nagy +contract chequebook is mortal { + // Cumulative paid amount in wei to each beneficiary + mapping (address => uint256) sent; + + /// @notice Overdraft event + event Overdraft(address deadbeat); + + /// @notice Accessor for sent map + /// + /// @param beneficiary beneficiary address + /// @return cumulative amount in wei sent to beneficiary + function getSent(address beneficiary) returns (uint256) { + return sent[beneficiary]; + } + + /// @notice Cash cheque + /// + /// @param beneficiary beneficiary address + /// @param amount cumulative amount in wei + /// @param sig_v signature parameter v + /// @param sig_r signature parameter r + /// @param sig_s signature parameter s + /// The digital signature is calculated on the concatenated triplet of contract address, beneficiary address and cumulative amount + function cash(address beneficiary, uint256 amount, + uint8 sig_v, bytes32 sig_r, bytes32 sig_s) { + // Check if the cheque is old. + // Only cheques that are more recent than the last cashed one are considered. + if(amount <= sent[beneficiary]) return; + // Check the digital signature of the cheque. + bytes32 hash = sha3(address(this), beneficiary, amount); + if(owner != ecrecover(hash, sig_v, sig_r, sig_s)) return; + // Attempt sending the difference between the cumulative amount on the cheque + // and the cumulative amount on the last cashed cheque to beneficiary. + if (beneficiary.send(amount - sent[beneficiary])) { + // Upon success, update the cumulative amount. + sent[beneficiary] = amount; + } else { + // Upon failure, punish owner for writing a bounced cheque. + // owner.sendToDebtorsPrison(); + Overdraft(owner); + // Compensate beneficiary. + suicide(beneficiary); + } + } +} +` +) diff --git a/common/kademlia/address.go b/common/kademlia/address.go new file mode 100644 index 0000000000..a6ce4e4770 --- /dev/null +++ b/common/kademlia/address.go @@ -0,0 +1,157 @@ +package kademlia + +import ( + "fmt" + "math/rand" + "strings" + + "github.com/ethereum/go-ethereum/common" +) + +type Address common.Hash + +func (a Address) String() string { + return fmt.Sprintf("%x", a[:]) +} + +func (a *Address) MarshalJSON() (out []byte, err error) { + return []byte(`"` + a.String() + `"`), nil +} + +func (a *Address) UnmarshalJSON(value []byte) error { + *a = Address(common.HexToHash(string(value[1 : len(value)-1]))) + return nil +} + +// the string form of the binary representation of an address (only first 8 bits) +func (a Address) Bin() string { + var bs []string + for _, b := range a[:] { + bs = append(bs, fmt.Sprintf("%08b", b)) + } + return strings.Join(bs, "") +} + +/* +Proximity(x, y) returns the proximity order of the MSB distance between x and y + +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 x^y, ie., x and y bitwise xor-ed. +the binary cast is big endian: most significant bit first (=MSB). + +Proximity(x, y) is a discrete logarithmic scaling of the MSB distance. +It is defined as the reverse rank of the integer part of the base 2 +logarithm of the distance. +It is calculated by counting the number of common leading zeros in the (MSB) +binary representation of the x^y. + +(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))&0x01 != 0 { + return i*8 + j + } + } + } + return len(one) * 8 +} + +// Address.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 (target Address) ProxCmp(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 +} + +// 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 +} + +// KeyRange(a0, a1, proxLimit) returns the address inclusive address +// range that contain addresses closer to one than other +func KeyRange(one, other Address, proxLimit int) (start, stop Address) { + prox := proximity(one, other) + if prox >= proxLimit { + prox = proxLimit + } + start = CommonBitsAddrByte(one, other, byte(0x00), prox) + stop = CommonBitsAddrByte(one, other, byte(0xff), prox) + return +} + +func CommonBitsAddrF(self, other Address, f func() byte, p int) (addr Address) { + prox := proximity(self, other) + var pos int + if p <= prox { + prox = p + } + pos = prox / 8 + addr = self + trans := byte(prox % 8) + var transbytea byte + if p > prox { + transbytea = byte(0x7f) + } else { + transbytea = byte(0xff) + } + transbytea >>= trans + transbyteb := transbytea ^ byte(0xff) + addrpos := addr[pos] + addrpos &= transbyteb + if p > prox { + addrpos ^= byte(0x80 >> trans) + } + addrpos |= transbytea & f() + addr[pos] = addrpos + for i := pos + 1; i < len(addr); i++ { + addr[i] = f() + } + + return +} + +func CommonBitsAddr(self, other Address, prox int) (addr Address) { + return CommonBitsAddrF(self, other, func() byte { return byte(rand.Intn(255)) }, prox) +} + +func CommonBitsAddrByte(self, other Address, b byte, prox int) (addr Address) { + return CommonBitsAddrF(self, other, func() byte { return b }, prox) +} + +// randomAddressAt() generates a random address +func RandomAddress() Address { + return RandomAddressAt(Address{}, -1) +} diff --git a/common/kademlia/address_test.go b/common/kademlia/address_test.go new file mode 100644 index 0000000000..b0577dc243 --- /dev/null +++ b/common/kademlia/address_test.go @@ -0,0 +1,80 @@ +package kademlia + +import ( + "math/rand" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func (Address) Generate(rand *rand.Rand, size int) reflect.Value { + var id Address + for i := 0; i < len(id); i++ { + id[i] = byte(uint8(rand.Intn(255))) + } + return reflect.ValueOf(id) +} + +func TestCommonBitsAddrF(t *testing.T) { + a := Address(common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + b := Address(common.HexToHash("0x8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + c := Address(common.HexToHash("0x4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + d := Address(common.HexToHash("0x0023456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + e := Address(common.HexToHash("0x01A3456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + ab := CommonBitsAddrF(a, b, func() byte { return byte(0x00) }, 10) + expab := Address(common.HexToHash("0x8000000000000000000000000000000000000000000000000000000000000000")) + + if ab != expab { + t.Fatalf("%v != %v", ab, expab) + } + ac := CommonBitsAddrF(a, c, func() byte { return byte(0x00) }, 10) + expac := Address(common.HexToHash("0x4000000000000000000000000000000000000000000000000000000000000000")) + + if ac != expac { + t.Fatalf("%v != %v", ac, expac) + } + ad := CommonBitsAddrF(a, d, func() byte { return byte(0x00) }, 10) + expad := Address(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")) + + if ad != expad { + t.Fatalf("%v != %v", ad, expad) + } + ae := CommonBitsAddrF(a, e, func() byte { return byte(0x00) }, 10) + expae := Address(common.HexToHash("0x0180000000000000000000000000000000000000000000000000000000000000")) + + if ae != expae { + t.Fatalf("%v != %v", ae, expae) + } + acf := CommonBitsAddrF(a, c, func() byte { return byte(0xff) }, 10) + expacf := Address(common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")) + + if acf != expacf { + t.Fatalf("%v != %v", acf, expacf) + } + aeo := CommonBitsAddrF(a, e, func() byte { return byte(0x00) }, 2) + expaeo := Address(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")) + + if aeo != expaeo { + t.Fatalf("%v != %v", aeo, expaeo) + } + aep := CommonBitsAddrF(a, e, func() byte { return byte(0xff) }, 2) + expaep := Address(common.HexToHash("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")) + + if aep != expaep { + t.Fatalf("%v != %v", aep, expaep) + } + +} + +func TestRandomAddressAt(t *testing.T) { + var a Address + for i := 0; i < 100; i++ { + a = RandomAddress() + prox := rand.Intn(255) + b := RandomAddressAt(a, prox) + if proximity(a, b) != prox { + t.Fatalf("incorrect address prox(%v, %v) == %v (expected %v)", a, b, proximity(a, b), prox) + } + } +} diff --git a/common/kademlia/kaddb.go b/common/kademlia/kaddb.go new file mode 100644 index 0000000000..e45a9dc48e --- /dev/null +++ b/common/kademlia/kaddb.go @@ -0,0 +1,360 @@ +package kademlia + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +type Time time.Time + +func (t *Time) MarshalJSON() (out []byte, err error) { + return []byte(fmt.Sprintf("%d", t.Unix())), nil +} + +func (t *Time) UnmarshalJSON(value []byte) error { + var i int64 + _, err := fmt.Sscanf(string(value), "%d", &i) + if err != nil { + return err + } + *t = Time(time.Unix(i, 0)) + return nil +} + +func (t Time) Unix() int64 { + return time.Time(t).Unix() +} + +type NodeData interface { + json.Marshaler + json.Unmarshaler +} + +// allow inactive peers under +type NodeRecord struct { + Addr Address // address of node + Url string // Url, used to connect to node + After Time // next call after time + Seen Time // last connected at time + Meta *json.RawMessage // arbitrary metadata saved for a peer + + node Node + connected bool +} + +// set checked to current time, +func (self *NodeRecord) setSeen() { + self.Seen = Time(time.Now()) +} + +func (self *NodeRecord) String() string { + return fmt.Sprintf("<%v>", self.Addr) +} + +// persisted node record database () +type KadDb struct { + Address Address + Nodes [][]*NodeRecord + index map[Address]*NodeRecord + cursors []int + lock sync.Mutex + purgeInterval time.Duration + initialRetryInterval time.Duration + connRetryExp int +} + +func newKadDb(addr Address, params *KadParams) *KadDb { + return &KadDb{ + Address: addr, + Nodes: make([][]*NodeRecord, params.MaxProx+1), // overwritten by load + cursors: make([]int, params.MaxProx+1), + index: make(map[Address]*NodeRecord), + purgeInterval: params.PurgeInterval, + initialRetryInterval: params.InitialRetryInterval, + connRetryExp: params.ConnRetryExp, + } +} + +func (self *KadDb) findOrCreate(index int, a Address, url string) *NodeRecord { + defer self.lock.Unlock() + self.lock.Lock() + + record, found := self.index[a] + if !found { + record = &NodeRecord{ + Addr: a, + Url: url, + } + glog.V(logger.Info).Infof("[KΛÐ]: add new record %v to kaddb", record) + // insert in kaddb + self.index[a] = record + self.Nodes[index] = append(self.Nodes[index], record) + } else { + glog.V(logger.Info).Infof("[KΛÐ]: found record %v in kaddb", record) + } + // update last seen time + record.setSeen() + // update with url in case IP/port changes + record.Url = url + return record +} + +// add adds node records to kaddb (persisted node record db) +func (self *KadDb) add(nrs []*NodeRecord, proximityBin func(Address) int) { + defer self.lock.Unlock() + self.lock.Lock() + var n int + var nodes []*NodeRecord + for _, node := range nrs { + _, found := self.index[node.Addr] + if !found && node.Addr != self.Address { + node.setSeen() + self.index[node.Addr] = node + index := proximityBin(node.Addr) + dbcursor := self.cursors[index] + nodes = self.Nodes[index] + // this is inefficient for allocation, need to just append then shift + newnodes := make([]*NodeRecord, len(nodes)+1) + copy(newnodes[:], nodes[:dbcursor]) + newnodes[dbcursor] = node + copy(newnodes[dbcursor+1:], nodes[dbcursor:]) + glog.V(logger.Detail).Infof("[KΛÐ]: new nodes: %v (keys: %v)\nnodes: %v", newnodes, nodes) + self.Nodes[index] = newnodes + n++ + } + } + glog.V(logger.Detail).Infof("[KΛÐ]: received %d node records, added %d new", len(nrs), n) +} + +/* +next return one node record with the highest priority for desired +connection. +This is used to pick candidates for live nodes that are most wanted for +a higly connected low centrality network structure for Swarm which best suits +for a Kademlia-style routing. + +The candidate is chosen using the following strategy. +We check for missing online nodes in the buckets for 1 upto Max BucketSize rounds. +On each round we proceed from the low to high proximity order buckets. +If the number of active nodes (=connected peers) is < rounds, then start looking +for a known candidate. To determine if there is a candidate to recommend the +node record database row corresponding to the bucket is checked.a +If the row cursor is on position i, the ith element in the row is chosen. +If the record is scheduled not to be retried before NOW, the next element is taken. +If the record is scheduled can be retried, it is set as checked, scheduled for +checking and is returned. The time of the next check is in X (duration) such that +X = ConnRetryExp * delta where delta is the time past since the last check and +ConnRetryExp is constant obsoletion factor. (Note that when node records are added +from peer messages, they are marked as checked and placed at the cursor, ie. +given priority over older entries). Entries which were checked more than +purgeInterval ago are deleted from the kaddb row. If no candidate is found after +a full round of checking the next bucket up is considered. If no candidate is +found when we reach the maximum-proximity bucket, the next round starts. + +node record a is more favoured to b a > b iff a is a passive node (record of +offline past peer) +|proxBin(a)| < |proxBin(b)| +|| (proxBin(a) < proxBin(b) && |proxBin(a)| == |proxBin(b)|) +|| (proxBin(a) == proxBin(b) && lastChecked(a) < lastChecked(b)) + +This has double role. Starting as naive node with empty db, this implements +Kademlia bootstrapping +As a mature node, it fills short lines. All on demand. + +The second argument returned names the first missing slot found +*/ +func (self *KadDb) findBest(bucketSize int, binsize func(int) int) (node *NodeRecord, proxLimit int) { + // return value -1 indicates that buckets are filled in all + proxLimit = -1 + defer self.lock.Unlock() + self.lock.Lock() + + var interval int64 + var found bool + for rounds := 1; rounds <= bucketSize; rounds++ { + ROUND: + for po, dbrow := range self.Nodes { + if po > len(self.Nodes) { + break ROUND + } + size := binsize(po) + if size < rounds { + if proxLimit < 0 { + // set the first missing slot found + proxLimit = po + } + var count int + var purge []int + n := self.cursors[po] + + // try node records in the relavant kaddb row (of identical prox order) + // if they are ripe for checking + ROW: + for count < len(dbrow) { + node = dbrow[n] + + // skip already connected nodes + if !node.connected { + + glog.V(logger.Detail).Infof("[KΛÐ]: kaddb record %v (PO%03d:%d) not to be retried before %v", node.Addr, po, n, node.After) + + // time since last known connection attempt + delta := node.After.Unix() - node.Seen.Unix() + // if delta < 4 { + // node.After = Time(time.Time{}) + // } + + // if node is scheduled to connect + if time.Time(node.After).Before(time.Now()) { + + // if checked longer than purge interval + if time.Time(node.Seen).Add(self.purgeInterval).Before(time.Now()) { + // delete node + purge = append(purge, n) + glog.V(logger.Detail).Infof("[KΛÐ]: inactive node record %v (PO%03d:%d) last check: %v, next check: %v", node.Addr, po, n, node.Seen, node.After) + } else { + // scheduling next check + if (node.After == Time(time.Time{})) { + node.After = Time(time.Now().Add(self.initialRetryInterval)) + } else { + interval = delta * int64(self.connRetryExp) + node.After = Time(time.Unix(time.Now().Unix()+interval, 0)) + } + + glog.V(logger.Detail).Infof("[KΛÐ]: serve node record %v (PO%03d:%d), last check: %v, next check: %v", node.Addr, po, n, node.Seen, node.After) + } + found = true + break ROW + } + glog.V(logger.Detail).Infof("[KΛÐ]: kaddb record %v (PO%03d:%d) not ready. skipped. not to be retried before: %v", node.Addr, po, n, node.After) + } // if node.node == nil + n++ + count++ + // cycle: n = n % len(dbrow) + if n >= len(dbrow) { + n = 0 + } + } + self.cursors[po] = n + self.delete(po, purge...) + if found { + glog.V(logger.Detail).Infof("[KΛÐ]: rounds %d: prox limit: PO%03d\n%v", rounds, proxLimit, node) + node.setSeen() + return + } + } // if len < rounds + } // for po-s + glog.V(logger.Detail).Infof("[KΛÐ]: rounds %d: proxlimit: PO%03d", rounds, proxLimit) + if proxLimit == 0 || proxLimit < 0 && bucketSize == rounds { + return + } + } // for round + + return +} + +// deletes the noderecords of a kaddb row corresponding to the indexes +// caller must hold the dblock +// the call is unsafe, no index checks +func (self *KadDb) delete(row int, indexes ...int) { + var prev int + var nodes []*NodeRecord + dbrow := self.Nodes[row] + for _, next := range indexes { + // need to adjust dbcursor + if next > 0 { + if next <= self.cursors[row] { + self.cursors[row]-- + } + nodes = append(nodes, dbrow[prev:next]...) + } + prev = next + 1 + delete(self.index, dbrow[next].Addr) + } + self.Nodes[row] = append(nodes, dbrow[prev:]...) +} + +// save persists kaddb on disk (written to file on path in json format. +func (self *KadDb) save(path string, cb func(*NodeRecord, Node)) error { + defer self.lock.Unlock() + self.lock.Lock() + + var n int + + for _, b := range self.Nodes { + for _, node := range b { + n++ + node.After = Time(time.Now()) + node.Seen = Time(time.Now()) + if cb != nil { + cb(node, node.node) + } + } + } + + data, err := json.MarshalIndent(self, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(path, data, os.ModePerm) + if err != nil { + glog.V(logger.Warn).Infof("[KΛÐ]: unable to save kaddb with %v nodes to %v: err", n, path, err) + } else { + glog.V(logger.Info).Infof("[KΛÐ] saved kaddb with %v nodes to %v", n, path) + } + return err +} + +// Load(path) loads the node record database (kaddb) from file on path. +func (self *KadDb) load(path string, cb func(*NodeRecord, Node) error) (err error) { + defer self.lock.Unlock() + self.lock.Lock() + + var data []byte + data, err = ioutil.ReadFile(path) + if err != nil { + return + } + + err = json.Unmarshal(data, self) + if err != nil { + return + } + var n int + var purge []int + for po, b := range self.Nodes { + ROW: + for i, node := range b { + if cb != nil { + err = cb(node, node.node) + if err != nil { + purge = append(purge, i) + continue ROW + } + } + n++ + if (node.After == Time(time.Time{})) { + node.After = Time(time.Now()) + } + self.index[node.Addr] = node + } + self.delete(po, purge...) + } + glog.V(logger.Info).Infof("[KΛÐ] loaded kaddb with %v nodes from %v", n, path) + + return +} + +// accessor for KAD offline db count +func (self *KadDb) count() int { + defer self.lock.Unlock() + self.lock.Lock() + return len(self.index) +} diff --git a/common/kademlia/kademlia.go b/common/kademlia/kademlia.go new file mode 100644 index 0000000000..ac80e8b406 --- /dev/null +++ b/common/kademlia/kademlia.go @@ -0,0 +1,457 @@ +package kademlia + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +const ( + bucketSize = 20 + maxProx = 255 + connRetryExp = 2 +) + +var ( + purgeInterval = 42 * time.Hour + initialRetryInterval = 42 * 100 * time.Millisecond +) + +type KadParams struct { + // adjustable parameters + MaxProx int + ProxBinSize int + BucketSize int + PurgeInterval time.Duration + InitialRetryInterval time.Duration + ConnRetryExp int +} + +func NewKadParams() *KadParams { + return &KadParams{ + MaxProx: maxProx, + ProxBinSize: bucketSize, + BucketSize: bucketSize, + PurgeInterval: purgeInterval, + InitialRetryInterval: initialRetryInterval, + ConnRetryExp: connRetryExp, + } +} + +// Kademlia is a table of active nodes +type Kademlia struct { + addr Address // immutable baseaddress of the table + *KadParams // Kademlia configuration parameters + proxLimit int // state, the PO of the first row of the most proximate bin + proxSize int // state, the number of peers in the most proximate bin + count int // number of active peers (w live connection) + buckets []*bucket // the actual bins + db *KadDb // kaddb, node record database + lock sync.RWMutex // mutex to access buckets +} + +type Node interface { + Addr() Address + Url() string + LastActive() time.Time + Drop() +} + +// public constructor +// add is the base address of the table +// params is KadParams configuration +func New(addr Address, params *KadParams) *Kademlia { + buckets := make([]*bucket, params.MaxProx+1) + for i, _ := range buckets { + buckets[i] = &bucket{size: params.BucketSize} // will initialise bucket{int(0),[]Node(nil),sync.Mutex} + } + glog.V(logger.Info).Infof("[KΛÐ] base address %v", addr) + + return &Kademlia{ + addr: addr, + KadParams: params, + buckets: buckets, + db: newKadDb(addr, params), + } +} + +// accessor for KAD base address +func (self *Kademlia) Addr() Address { + return self.addr +} + +// accessor for KAD active node count +func (self *Kademlia) Count() int { + defer self.lock.Unlock() + self.lock.Lock() + return self.count +} + +// accessor for KAD active node count +func (self *Kademlia) DBCount() int { + return self.db.count() +} + +// On is the entry point called when a new nodes is added +// unsafe in that node is not checked to be already active node (to be called once) +func (self *Kademlia) On(node Node, cb func(*NodeRecord, Node) error) (err error) { + index := self.proximityBin(node.Addr()) + record := self.db.findOrCreate(index, node.Addr(), node.Url()) + // callback on add node + // setting the node on the record, set it checked (for connectivity) + record.node = node + + glog.V(logger.Info).Infof("[KΛÐ]: add node record %v with node %v", record, node) + if cb != nil { + err = cb(record, node) + glog.V(logger.Info).Infof("[KΛÐ]: cb(%v, %v) ->%v", record, node, err) + if err != nil { + return fmt.Errorf("node %v not added: %v", node.Addr(), err) + } + } + record.connected = true + + defer self.lock.Unlock() + self.lock.Lock() + + // insert in kademlia table of active nodes + bucket := self.buckets[index] + // if bucket is full insertion replaces the worst node + // TODO: give priority to peers with active traffic + if worst, pos := bucket.insert(node); worst != nil { + glog.V(logger.Info).Infof("[KΛÐ]: replace node %v (%d) with node %v", worst, pos, node) + // no prox adjustment needed + // do not change count + } else { + glog.V(logger.Info).Infof("[KΛÐ]: add node %v to table", node) + self.count++ + self.adjustProxMore(index) + } + + return + +} + +// is the entrypoint called when a node is taken offline +func (self *Kademlia) Off(node Node, cb func(*NodeRecord, Node)) (err error) { + self.lock.Lock() + defer self.lock.Unlock() + + var found bool + index := self.proximityBin(node.Addr()) + bucket := self.buckets[index] + for i := 0; i < len(bucket.nodes); i++ { + if node.Addr() == bucket.nodes[i].Addr() { + found = true + bucket.nodes = append(bucket.nodes[:i], bucket.nodes[(i+1):]...) + } + } + + if !found { + return + } + glog.V(logger.Info).Infof("[KΛÐ]: remove node %v from table", node) + + self.count-- + if len(bucket.nodes) < bucket.size { + err = fmt.Errorf("insufficient nodes (%v) in bucket %v", len(bucket.nodes), index) + } + self.adjustProxLess(index) + + r := self.db.index[node.Addr()] + // callback on remove + if cb != nil { + cb(r, r.node) + } + r.node = nil + r.connected = false + + return +} + +// proxLimit is dynamically adjusted so that 1) there is no +// empty buckets in bin < proxLimit and 2) the sum of all items sare the maximum +// possible but lower than ProxBinSize +// adjust Prox (proxLimit and proxSize after an insertion of add nodes into bucket r) +func (self *Kademlia) adjustProxMore(r int) { + if r >= self.proxLimit { + exLimit := self.proxLimit + exSize := self.proxSize + self.proxSize++ + + var i int + for i = self.proxLimit; i < self.MaxProx && len(self.buckets[i].nodes) > 0 && self.proxSize-len(self.buckets[i].nodes) > self.ProxBinSize; i++ { + self.proxSize -= len(self.buckets[i].nodes) + } + self.proxLimit = i + + glog.V(logger.Detail).Infof("[KΛÐ]: Max Prox Bin: Lower Limit: %v (was %v): Bin Size: %v (was %v)", self.proxLimit, exLimit, self.proxSize, exSize) + } +} + +func (self *Kademlia) adjustProxLess(r int) { + exLimit := self.proxLimit + exSize := self.proxSize + if r >= self.proxLimit { + self.proxSize-- + } + + if 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 + } else if self.proxLimit > 0 && r >= self.proxLimit-1 { + var i int + for i = self.proxLimit - 1; i > 0 && len(self.buckets[i].nodes)+self.proxSize <= self.ProxBinSize; i-- { + self.proxSize += len(self.buckets[i].nodes) + } + self.proxLimit = i + } + + if exLimit != self.proxLimit || exSize != self.proxSize { + glog.V(logger.Detail).Infof("[KΛÐ]: Max Prox Bin: Lower Limit: %v (was %v): Bin Size: %v (was %v)", self.proxLimit, exLimit, self.proxSize, exSize) + } +} + +/* +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. +*/ +func (self *Kademlia) FindClosest(target Address, max int) []Node { + defer self.lock.RUnlock() + self.lock.RLock() + r := nodesByDistance{ + 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++ + } + } + } + glog.V(logger.Detail).Infof("[KΛÐ]: serve %d (=<%d) nodes for target lookup %v (PO%d)", n, self.MaxProx, target, index) + return r.nodes +} + +func (self *Kademlia) binsize(p int) int { + b := self.buckets[p] + defer b.lock.RUnlock() + b.lock.RLock() + return len(b.nodes) +} + +func (self *Kademlia) FindBest() (node *NodeRecord, proxLimit int) { + return self.db.findBest(self.BucketSize, self.binsize) +} + +// adds node records to kaddb (persisted node record db) +func (self *Kademlia) Add(nrs []*NodeRecord) { + self.db.add(nrs, self.proximityBin) +} + +// in situ mutable bucket +type bucket struct { + size int + nodes []Node + lock sync.RWMutex +} + +// 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 target.ProxCmp(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 h.target.ProxCmp(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 bucketSize, or by replacing the worst +// Node in the bucket +func (self *bucket) insert(node Node) (dropped Node, pos int) { + self.lock.Lock() + defer self.lock.Unlock() + if len(self.nodes) >= self.size { // >= allows us to add peers beyond the bucketsize limitation + dropped, pos = self.worstNode() + if dropped != nil { + self.nodes[pos] = node + glog.V(logger.Info).Infof("[KΛÐ] dropping node %v (%d)", dropped, pos) + dropped.Drop() + return + } + } + self.nodes = append(self.nodes, node) + return +} + +func (self *bucket) length(node Node) int { + self.lock.Lock() + defer self.lock.Unlock() + return len(self.nodes) +} + +// worst expunges the single worst node in a row, where worst entry is the node +// that has been inactive for the longests time +func (self *bucket) worstNode() (node Node, pos int) { + var oldest time.Time + for p, n := range self.nodes { + if (oldest == time.Time{}) || !oldest.Before(n.LastActive()) { + oldest = n.LastActive() + node = n + pos = p + } + } + return +} + +/* +Taking the proximity order relative to a fix point x classifies the points in +the space (n byte long byte sequences) into bins. Items in each are at +most half as distant from x as items in the previous bin. Given a sample of +uniformly distributed 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 hop, 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 +} + +// provides keyrange for chunk db iteration +func (self *Kademlia) KeyRange(other Address) (start, stop Address) { + defer self.lock.RUnlock() + self.lock.RLock() + return KeyRange(self.addr, other, self.proxLimit) +} + +// save persists kaddb on disk (written to file on path in json format. +func (self *Kademlia) Save(path string, cb func(*NodeRecord, Node)) error { + return self.db.save(path, cb) +} + +// Load(path) loads the node record database (kaddb) from file on path. +func (self *Kademlia) Load(path string, cb func(*NodeRecord, Node) error) (err error) { + return self.db.load(path, cb) +} + +// kademlia table + kaddb table displayed with ascii +func (self *Kademlia) String() string { + + var rows []string + rows = append(rows, "=========================================================================") + rows = append(rows, fmt.Sprintf("%v : MaxProx: %d, ProxBinSize: %d, BucketSize: %d, proxLimit: %d, proxSize: %d", time.Now(), self.MaxProx, self.ProxBinSize, self.BucketSize, self.proxLimit, self.proxSize)) + + for i, b := range self.buckets { + + if i == self.proxLimit { + rows = append(rows, fmt.Sprintf("===================== PROX LIMIT: %d =================================", i)) + } + row := []string{fmt.Sprintf("%03d", i), fmt.Sprintf("%2d", len(b.nodes))} + var k int + c := self.db.cursors[i] + for ; k < len(b.nodes); k++ { + p := b.nodes[(c+k)%len(b.nodes)] + row = append(row, fmt.Sprintf("%s", p.Addr().String()[:8])) + if k == 3 { + break + } + } + for ; k < 3; k++ { + row = append(row, " ") + } + row = append(row, fmt.Sprintf("| %2d %2d", len(self.db.Nodes[i]), self.db.cursors[i])) + + for j, p := range self.db.Nodes[i] { + row = append(row, fmt.Sprintf("%08x", p.Addr[:4])) + if j == 2 { + break + } + } + rows = append(rows, strings.Join(row, " ")) + if i == self.MaxProx { + break + } + } + rows = append(rows, "=========================================================================") + return strings.Join(rows, "\n") +} diff --git a/common/kademlia/kademlia_test.go b/common/kademlia/kademlia_test.go new file mode 100644 index 0000000000..f36158c378 --- /dev/null +++ b/common/kademlia/kademlia_test.go @@ -0,0 +1,389 @@ +package kademlia + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + "testing/quick" + "time" +) + +var ( + quickrand = rand.New(rand.NewSource(time.Now().Unix())) + quickcfgFindClosest = &quick.Config{MaxCount: 5000, Rand: quickrand} + quickcfgBootStrap = &quick.Config{MaxCount: 1000, Rand: quickrand} +) + +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 TestOn(t *testing.T) { + addr, ok := gen(Address{}, quickrand).(Address) + other, ok := gen(Address{}, quickrand).(Address) + if !ok { + t.Errorf("oops") + } + kad := New(addr, NewKadParams()) + err := kad.On(&testNode{addr: other}, nil) + _ = err +} + +func TestBootstrap(t *testing.T) { + + test := func(test *bootstrapTest) bool { + // for any node kad.le, Target and N + params := NewKadParams() + params.MaxProx = test.MaxProx + params.BucketSize = test.BucketSize + params.ProxBinSize = test.BucketSize + kad := New(test.Self, params) + var err error + + addr := RandomAddress() + prox := proximity(addr, test.Self) + + for p := 0; p <= prox; p++ { + var nrs []*NodeRecord + for i := 0; i < 3; i++ { + nrs = append(nrs, &NodeRecord{ + Addr: RandomAddressAt(test.Self, p), + }) + } + kad.Add(nrs) + } + + node := &testNode{addr} + + n := 0 + for n < 100 { + err = kad.On(node, nil) + if err != nil { + t.Errorf("backend not accepting node") + return false + } + var nrs []*NodeRecord + prox := proximity(test.Self, node.addr) + for i := 0; i < 13; i++ { + nrs = append(nrs, &NodeRecord{ + Addr: RandomAddressAt(test.Self, prox+1), + }) + } + kad.Add(nrs) + + record, _ := kad.FindBest() + if record == nil { + break + } + node = &testNode{record.Addr} + n++ + } + exp := test.BucketSize * (test.MaxProx + 1) + if kad.Count() != exp { + t.Errorf("incorrect number of peers, expected %d, got %d\n%v", exp, kad.Count(), kad) + return false + } + return true + } + if err := quick.Check(test, quickcfgBootStrap); err != nil { + t.Error(err) + } + +} + +func TestFindClosest(t *testing.T) { + + test := func(test *FindClosestTest) bool { + // for any node kad.le, Target and N + params := NewKadParams() + params.MaxProx = 10 + kad := New(test.Self, params) + var err error + // t.Logf("FindClosestTest %v: %v\n", len(test.All), test) + for _, node := range test.All { + err = kad.On(node, nil) + if err != nil { + t.Errorf("backend not accepting node") + return false + } + } + + if len(test.All) == 0 || test.N == 0 { + return true + } + nodes := kad.FindClosest(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 test.Target.ProxCmp(n.Addr(), farthestResult) < 0 { + _ = i * j + 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, quickcfgFindClosest); err != nil { + t.Error(err) + } +} + +type proxTest struct { + add bool + index int + addr Address +} + +var ( + addresses []Address +) + +func TestProxAdjust(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + self := gen(Address{}, r).(Address) + params := NewKadParams() + params.MaxProx = 10 + kad := New(self, params) + + var err error + for i := 0; i < 100; i++ { + a := gen(Address{}, r).(Address) + addresses = append(addresses, a) + err = kad.On(&testNode{addr: a}, nil) + if err != nil { + t.Errorf("backend not accepting node") + return + } + if !kad.proxCheck(t) { + return + } + } + + test := func(test *proxTest) bool { + node := &testNode{test.addr} + if test.add { + kad.On(node, nil) + } else { + kad.Off(node, nil) + } + return kad.proxCheck(t) + } + if err := quick.Check(test, quickcfgFindClosest); err != nil { + t.Error(err) + } +} + +func TestSaveLoad(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + addresses := gen([]Address{}, r).([]Address) + self := RandomAddress() + params := NewKadParams() + params.MaxProx = 10 + kad := New(self, params) + + var err error + + for _, a := range addresses { + err = kad.On(&testNode{addr: a}, nil) + if err != nil { + t.Errorf("backend not accepting node") + return + } + } + nodes := kad.FindClosest(self, 100) + path := "/tmp/bzz.peers" + err = kad.Save(path, nil) + if err != nil { + t.Fatalf("unepected error saving kaddb: %v", err) + } + kad = New(self, params) + err = kad.Load(path, nil) + if err != nil { + t.Fatalf("unepected error loading kaddb: %v", err) + } + for _, b := range kad.db.Nodes { + for _, node := range b { + err = kad.On(&testNode{node.Addr}, nil) + if err != nil { + t.Errorf("backend not accepting node") + return + } + } + } + loadednodes := kad.FindClosest(self, 100) + for i, node := range loadednodes { + if nodes[i].Addr() != node.Addr() { + t.Errorf("node mismatch at %d/%d: %v != %v", i, len(nodes), nodes[i].Addr(), node.Addr()) + } + } +} + +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 { + sum += l + } else if l == 0 { + t.Errorf("bucket %d empty, yet proxLimit is %d\n%v", len(b.nodes), self.proxLimit, self) + 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\n%v", i, self.proxSize, self) + // 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 + BucketSize int + Self Address +} + +func (*bootstrapTest) Generate(rand *rand.Rand, size int) reflect.Value { + t := &bootstrapTest{ + Self: gen(Address{}, rand).(Address), + MaxProx: 10 + rand.Intn(3), + BucketSize: rand.Intn(3) + 1, + } + return reflect.ValueOf(t) +} + +type FindClosestTest struct { + Self Address + Target Address + All []Node + N int +} + +func (c FindClosestTest) String() string { + return fmt.Sprintf("A: %064x\nT: %064x\n(%d)\n", c.Self[:], c.Target[:], c.N) +} + +func (*FindClosestTest) Generate(rand *rand.Rand, size int) reflect.Value { + t := &FindClosestTest{ + 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{ + addr: 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() +} diff --git a/common/registrar/ethreg/ethreg.go b/common/registrar/ethreg/ethreg.go index 626ebe1d74..7b3b41769c 100644 --- a/common/registrar/ethreg/ethreg.go +++ b/common/registrar/ethreg/ethreg.go @@ -20,18 +20,22 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common/registrar" - "github.com/ethereum/go-ethereum/xeth" ) +type Backend interface { + registrar.Backend + AtStateNum(int64) registrar.Backend +} + // implements a versioned Registrar on an archiving full node type EthReg struct { - backend *xeth.XEth + backend Backend registry *registrar.Registrar } -func New(xe *xeth.XEth) (self *EthReg) { - self = &EthReg{backend: xe} - self.registry = registrar.New(xe) +func New(backend Backend) (self *EthReg) { + self = &EthReg{backend: backend} + self.registry = registrar.New(backend) return } @@ -40,9 +44,11 @@ func (self *EthReg) Registry() *registrar.Registrar { } func (self *EthReg) Resolver(n *big.Int) *registrar.Registrar { - xe := self.backend + var s registrar.Backend if n != nil { - xe = self.backend.AtStateNum(n.Int64()) + s = self.backend.AtStateNum(n.Int64()) + } else { + s = registrar.Backend(self.backend) } - return registrar.New(xe) + return registrar.New(s) } diff --git a/common/swap/swap.go b/common/swap/swap.go new file mode 100644 index 0000000000..c8cf7a7770 --- /dev/null +++ b/common/swap/swap.go @@ -0,0 +1,238 @@ +package swap + +import ( + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +// SwAP Swarm Accounting Protocol with +// Swift Automatic Payments +// a peer to peer micropayment system + +// public swap profile +// public parameters for SWAP, serializable config struct passed in handshake +type Profile struct { + BuyAt *big.Int // accepted max price for chunk + SellAt *big.Int // offered sale price for chunk + PayAt uint // threshold that triggers payment request + DropAt uint // threshold that triggers disconnect +} + +// Strategy encapsulates parameters relating to +// automatic deposit and automatic cashing +type Strategy struct { + AutoCashInterval time.Duration // default interval for autocash + AutoCashThreshold *big.Int // threshold that triggers autocash (wei) + AutoDepositInterval time.Duration // default interval for autocash + AutoDepositThreshold *big.Int // threshold that triggers autodeposit (wei) + AutoDepositBuffer *big.Int // buffer that is surplus for fork protection etc (wei) +} + +// Params extends the public profile with private parameters relating to +// automatic deposit and automatic cashing +type Params struct { + *Profile + *Strategy +} + +// Promise +// 3rd party Provable Promise of Payment +// issued by outPayment +// serialisable to send with Protocol +type Promise interface{} + +// interface for the peer protocol for testing or external alternative payment +type Protocol interface { + Pay(int, Promise) // units, payment proof + Drop() + String() string +} + +// interface for the (delayed) ougoing payment system with autodeposit +type OutPayment interface { + Issue(amount *big.Int) (promise Promise, err error) + AutoDeposit(interval time.Duration, threshold, buffer *big.Int) + Stop() +} + +// interface for the (delayed) incoming payment system with autocash +type InPayment interface { + Receive(promise Promise) (*big.Int, error) + AutoCash(cashInterval time.Duration, maxUncashed *big.Int) + Stop() +} + +// swap is the swarm accounting protocol instance +// * pairwise accounting and payments +type Swap struct { + lock sync.Mutex // mutex for balance access + balance int // units of chunk/retrieval request + local *Params // local peer's swap parameters + remote *Profile // remote peer's swap profile + proto Protocol // peer communication protocol + Payment +} + +type Payment struct { + Out OutPayment // outgoing payment handler + In InPayment // incoming payment handler + Buys, Sells bool +} + +// swap constructor +func New(local *Params, pm Payment, proto Protocol) (self *Swap, err error) { + + self = &Swap{ + local: local, + Payment: pm, + proto: proto, + } + + self.SetParams(local) + + return +} + +// entry point for setting remote swap profile (e.g from handshake or other message) +func (self *Swap) SetRemote(remote *Profile) { + defer self.lock.Unlock() + self.lock.Lock() + + self.remote = remote + if self.Sells && (remote.BuyAt.Cmp(common.Big0) <= 0 || self.local.SellAt.Cmp(common.Big0) <= 0 || remote.BuyAt.Cmp(self.local.SellAt) < 0) { + self.Out.Stop() + self.Sells = false + } + if self.Buys && (remote.SellAt.Cmp(common.Big0) <= 0 || self.local.BuyAt.Cmp(common.Big0) <= 0 || self.local.BuyAt.Cmp(self.remote.SellAt) < 0) { + self.In.Stop() + self.Buys = false + } + + glog.V(logger.Debug).Infof("[SWAP] <%v> remote profile set: pay at: %v, drop at: %v, buy at: %v, sell at: %v", self.proto, remote.PayAt, remote.DropAt, remote.BuyAt, remote.SellAt) + +} + +// to set strategy dynamically +func (self *Swap) SetParams(local *Params) { + defer self.lock.Unlock() + self.lock.Lock() + self.local = local + self.setParams(local) +} + +// caller holds the lock + +func (self *Swap) setParams(local *Params) { + + if self.Sells { + self.In.AutoCash(local.AutoCashInterval, local.AutoCashThreshold) + glog.V(logger.Info).Infof("[SWAP] <%v> set autocash to every %v, max uncashed limit: %v", self.proto, local.AutoCashInterval, local.AutoCashThreshold) + } else { + glog.V(logger.Info).Infof("[SWAP] <%v> autocash off (not selling)", self.proto) + } + if self.Buys { + self.Out.AutoDeposit(local.AutoDepositInterval, local.AutoDepositThreshold, local.AutoDepositBuffer) + glog.V(logger.Info).Infof("[SWAP] <%v> set autodeposit to every %v, pay at: %v, buffer: %v", self.proto, local.AutoDepositInterval, local.AutoDepositThreshold, local.AutoDepositBuffer) + } else { + glog.V(logger.Info).Infof("[SWAP] <%v> autodeposit off (not buying)", self.proto) + } +} + +// Add(n) +// n > 0 called when promised/provided n units of service +// n < 0 called when used/requested n units of service +func (self *Swap) Add(n int) error { + defer self.lock.Unlock() + self.lock.Lock() + self.balance += n + if !self.Sells && self.balance > 0 { + glog.V(logger.Detail).Infof("[SWAP] <%v> remote peer cannot have debt (unable to buy)", self.proto, self.balance) + self.proto.Drop() + return fmt.Errorf("[SWAP] <%v> remote peer cannot have debt (unable to buy)", self.proto, self.balance) + } + if !self.Buys && self.balance < 0 { + glog.V(logger.Detail).Infof("[SWAP] <%v> we cannot have debt (unable to buy)", self.proto, self.balance) + return fmt.Errorf("[SWAP] <%v> we cannot have debt (unable to buy)", self.proto, self.balance) + } + if self.balance >= int(self.local.DropAt) { + glog.V(logger.Detail).Infof("[SWAP] <%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", self.proto, self.balance, self.local.DropAt) + self.proto.Drop() + return fmt.Errorf("[SWAP] <%v> remote peer has too much debt (balance: %v, disconnect threshold: %v)", self.proto, self.balance, self.local.DropAt) + } else if self.balance <= -int(self.remote.PayAt) { + self.send() + } + return nil +} + +func (self *Swap) Balance() int { + defer self.lock.Unlock() + self.lock.Lock() + return self.balance +} + +// send(units) is called when payment is due +// In case of insolvency no promise is issued and sent, safe against fraud +// No return value: no error = payment is opportunistic = hang in till dropped +func (self *Swap) send() { + if self.local.BuyAt != nil && self.balance < 0 { + amount := big.NewInt(int64(-self.balance)) + amount.Mul(amount, self.remote.SellAt) + promise, err := self.Out.Issue(amount) + if err != nil { + glog.V(logger.Warn).Infof("[SWAP] <%v> cannot issue cheque (amount: %v, channel: %v): %v", self.proto, amount, self.Out, err) + } else { + glog.V(logger.Warn).Infof("[SWAP] <%v> cheque issued (amount: %v, channel: %v)", self.proto, amount, self.Out) + self.proto.Pay(-self.balance, promise) + self.balance = 0 + } + } +} + +// receive(units, promise) is called by the protocol when a payment msg is received +// returns error if promise is invalid. +func (self *Swap) Receive(units int, promise Promise) error { + if units <= 0 { + return fmt.Errorf("invalid units: %v <= 0", units) + } + + price := new(big.Int).SetInt64(int64(units)) + price.Mul(price, self.local.SellAt) + + amount, err := self.In.Receive(promise) + + if err != nil { + err = fmt.Errorf("invalid promise: %v", err) + } else if price.Cmp(amount) != 0 { + // verify amount = units * unit sale price + return fmt.Errorf("invalid amount: %v = %v * %v (units sent in msg * agreed sale unit price) != %v (signed in cheque)", price, units, self.local.SellAt, amount) + } + if err != nil { + glog.V(logger.Detail).Infof("[SWAP] <%v> invalid promise (amount: %v, channel: %v): %v", self.proto, amount, self.In, err) + return err + } + + // credit remote peer with units + self.Add(-units) + glog.V(logger.Detail).Infof("[SWAP] <%v> received promise (amount: %v, channel: %v): %v", self.proto, amount, self.In, promise) + + return nil +} + +// stop() causes autocash loop to terminate. +// Called after protocol handle loop terminates. +func (self *Swap) Stop() { + defer self.lock.Unlock() + self.lock.Lock() + if self.Buys { + self.Out.Stop() + } + if self.Sells { + self.In.Stop() + } +} diff --git a/common/swap/swap_test.go b/common/swap/swap_test.go new file mode 100644 index 0000000000..664cd81d20 --- /dev/null +++ b/common/swap/swap_test.go @@ -0,0 +1,178 @@ +package swap + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type testInPayment struct { + received []*testPromise + autocashInterval time.Duration + autocashLimit *big.Int +} + +type testPromise struct { + amount *big.Int +} + +func (self *testInPayment) Receive(promise Promise) (*big.Int, error) { + p := promise.(*testPromise) + self.received = append(self.received, p) + return p.amount, nil +} + +func (self *testInPayment) AutoCash(interval time.Duration, limit *big.Int) { + self.autocashInterval = interval + self.autocashLimit = limit +} + +func (self *testInPayment) Cash() (string, error) { return "", nil } + +func (self *testInPayment) Stop() {} + +type testOutPayment struct { + deposits []*big.Int + autodepositInterval time.Duration + autodepositThreshold *big.Int + autodepositBuffer *big.Int +} + +func (self *testOutPayment) Issue(amount *big.Int) (promise Promise, err error) { + return &testPromise{amount}, nil +} + +func (self *testOutPayment) Deposit(amount *big.Int) (string, error) { + self.deposits = append(self.deposits, amount) + return "", nil +} + +func (self *testOutPayment) AutoDeposit(interval time.Duration, threshold, buffer *big.Int) { + self.autodepositInterval = interval + self.autodepositThreshold = threshold + self.autodepositBuffer = buffer +} + +func (self *testOutPayment) Stop() {} + +type testProtocol struct { + drop bool + amounts []int + promises []*testPromise +} + +func (self *testProtocol) Drop() { + self.drop = true +} + +func (self *testProtocol) String() string { + return "" +} + +func (self *testProtocol) Pay(amount int, promise Promise) { + p := promise.(*testPromise) + self.promises = append(self.promises, p) + self.amounts = append(self.amounts, amount) +} + +func TestSwap(t *testing.T) { + + strategy := &Strategy{ + AutoCashInterval: 1 * time.Second, + AutoCashThreshold: big.NewInt(20), + AutoDepositInterval: 1 * time.Second, + AutoDepositThreshold: big.NewInt(20), + AutoDepositBuffer: big.NewInt(40), + } + + local := &Params{ + Profile: &Profile{ + PayAt: 5, + DropAt: 10, + BuyAt: common.Big3, + SellAt: common.Big2, + }, + Strategy: strategy, + } + + in := &testInPayment{} + out := &testOutPayment{} + proto := &testProtocol{} + + swap, _ := New(local, Payment{In: in, Out: out, Buys: true, Sells: true}, proto) + + if in.autocashInterval != strategy.AutoCashInterval { + t.Fatalf("autocash interval not properly set, expect %v, got ", strategy.AutoCashInterval, in.autocashInterval) + } + if out.autodepositInterval != strategy.AutoDepositInterval { + t.Fatalf("autodeposit interval not properly set, expect %v, got ", strategy.AutoDepositInterval, out.autodepositInterval) + } + + remote := &Profile{ + PayAt: 3, + DropAt: 10, + BuyAt: common.Big2, + SellAt: common.Big3, + } + swap.SetRemote(remote) + + swap.Add(9) + if proto.drop { + t.Fatalf("not expected peer to be dropped") + } + swap.Add(1) + if !proto.drop { + t.Fatalf("expected peer to be dropped") + } + if !proto.drop { + t.Fatalf("expected peer to be dropped") + } + proto.drop = false + + swap.Receive(10, &testPromise{big.NewInt(20)}) + if swap.balance != 0 { + t.Fatalf("expected zero balance, got %v", swap.balance) + } + + if len(proto.amounts) != 0 { + t.Fatalf("expected zero balance, got %v", swap.balance) + } + + swap.Add(-2) + if len(proto.amounts) > 0 { + t.Fatalf("expected no payments yet, got %v", proto.amounts) + } + + swap.Add(-1) + if len(proto.amounts) != 1 { + t.Fatalf("expected one payment, got %v", len(proto.amounts)) + } + + if proto.amounts[0] != 3 { + t.Fatalf("expected payment for %v units, got %v", proto.amounts[0]) + } + + exp := new(big.Int).Mul(big.NewInt(int64(proto.amounts[0])), remote.SellAt) + if proto.promises[0].amount.Cmp(exp) != 0 { + t.Fatalf("expected payment amount %v, got %v", exp, proto.promises[0].amount) + } + + swap.SetParams(&Params{ + Profile: &Profile{ + PayAt: 5, + DropAt: 10, + BuyAt: common.Big3, + SellAt: common.Big2, + }, + Strategy: &Strategy{ + AutoCashInterval: 2 * time.Second, + AutoCashThreshold: big.NewInt(40), + AutoDepositInterval: 2 * time.Second, + AutoDepositThreshold: big.NewInt(40), + AutoDepositBuffer: big.NewInt(60), + }, + }) + +} diff --git a/common/types.go b/common/types.go index b1666d7338..4e5500c06c 100644 --- a/common/types.go +++ b/common/types.go @@ -187,3 +187,16 @@ func PP(value []byte) string { return fmt.Sprintf("%x...%x", value[:4], value[len(value)-4]) } + +func (a *Address) MarshalJSON() (out []byte, err error) { + return []byte(quote(a.Hex())), nil +} + +func (m *Address) UnmarshalJSON(value []byte) error { + m.Set(HexToAddress(string(value[1 : len(value)-1]))) + return nil +} + +func quote(s string) string { + return `"` + s + `"` +} diff --git a/core/genesis.go b/core/genesis.go index d8c6e9cea3..6e54f50495 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -35,6 +35,11 @@ import ( "github.com/ethereum/go-ethereum/params" ) +const ( + TestAccount = "e273f01c99144c438695e10f24926dc1f9fbf62d" + TestBalance = "1000000000000" +) + // WriteGenesisBlock writes the genesis block to the database as block number 0 func WriteGenesisBlock(chainDb ethdb.Database, reader io.Reader) (*types.Block, error) { contents, err := ioutil.ReadAll(reader) diff --git a/rpc/api/bzz.go b/rpc/api/bzz.go new file mode 100644 index 0000000000..c258879857 --- /dev/null +++ b/rpc/api/bzz.go @@ -0,0 +1,278 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with go-ethereum. If not, see . + +package api + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc/codec" + "github.com/ethereum/go-ethereum/rpc/shared" + "github.com/ethereum/go-ethereum/swarm" +) + +const ( + BzzApiVersion = "1.0" +) + +// eth api provider +// See https://github.com/ethereum/wiki/wiki/JSON-RPC +type bzzApi struct { + swarm *swarm.Swarm + methods map[string]bzzhandler + codec codec.ApiCoder +} + +// eth callback handler +type bzzhandler func(*bzzApi, *shared.Request) (interface{}, error) + +var ( + bzzMapping = map[string]bzzhandler{ + "bzz_info": (*bzzApi).Info, + "bzz_issue": (*bzzApi).Issue, + "bzz_cash": (*bzzApi).Cash, + "bzz_deposit": (*bzzApi).Deposit, + "bzz_register": (*bzzApi).Register, + "bzz_resolve": (*bzzApi).Resolve, + "bzz_download": (*bzzApi).Download, + "bzz_upload": (*bzzApi).Upload, + "bzz_get": (*bzzApi).Get, + "bzz_put": (*bzzApi).Put, + "bzz_modify": (*bzzApi).Modify, + } +) + +func newSwarmOfflineError(method string) error { + return shared.NewNotAvailableError(method, "swarm offline") +} + +// create new bzzApi instance +func NewBzzApi(stack *node.Node, codec codec.Codec) *bzzApi { + var swarm *swarm.Swarm + stack.Service(&swarm) + return &bzzApi{swarm, bzzMapping, codec.New(nil)} +} + +// collection with supported methods +func (self *bzzApi) Methods() []string { + methods := make([]string, len(self.methods)) + i := 0 + for k := range self.methods { + methods[i] = k + i++ + } + return methods +} + +// Execute given request +func (self *bzzApi) Execute(req *shared.Request) (interface{}, error) { + if callback, ok := self.methods[req.Method]; ok { + return callback(self, req) + } + + return nil, shared.NewNotImplementedError(req.Method) +} + +func (self *bzzApi) Name() string { + return shared.BzzApiName +} + +func (self *bzzApi) ApiVersion() string { + return BzzApiVersion +} + +func (self *bzzApi) Info(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + return s.Api().Info(), nil +} + +func (self *bzzApi) Issue(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzIssueArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + cheque, err := s.Api().Issue(common.HexToAddress(args.Beneficiary), args.Amount) + if err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + out, err := json.MarshalIndent(cheque, " ", "") + if err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return string(out), nil +} + +func (self *bzzApi) Cash(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzCashArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return s.Api().Cash(args.Cheque) + +} + +func (self *bzzApi) Deposit(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzDepositArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return s.Api().Deposit(args.Amount) +} + +func (self *bzzApi) Register(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzRegisterArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + err := s.Api().Register(common.HexToAddress(args.Address), args.Domain, common.HexToHash(args.ContentHash)) + return err == nil, err +} + +func (self *bzzApi) Resolve(req *shared.Request) (interface{}, error) { + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzResolveArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + key, err := s.Api().Resolve(args.Domain) + return key.Hex(), err +} + +func (self *bzzApi) Download(req *shared.Request) (interface{}, error) { + + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzDownloadArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + err := s.Api().Download(args.BzzPath, args.LocalPath) + return err == nil, err +} + +func (self *bzzApi) Upload(req *shared.Request) (interface{}, error) { + + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzUploadArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return s.Api().Upload(args.LocalPath, args.Index) +} + +func (self *bzzApi) Get(req *shared.Request) (interface{}, error) { + + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzGetArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + var content []byte + var mimeType string + var status, size int + var err error + content, mimeType, status, size, err = s.Api().Get(args.Path) + + obj := map[string]string{ + "content": string(content), + "contentType": mimeType, + "status": fmt.Sprintf("%v", status), + "size": fmt.Sprintf("%v", size), + } + + return obj, err +} + +func (self *bzzApi) Put(req *shared.Request) (interface{}, error) { + + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzPutArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return s.Api().Put(args.Content, args.ContenType) +} + +func (self *bzzApi) Modify(req *shared.Request) (interface{}, error) { + + s := self.swarm + if s == nil { + return nil, newSwarmOfflineError(req.Method) + } + + args := new(BzzModifyArgs) + if err := self.codec.Decode(req.Params, &args); err != nil { + return nil, shared.NewDecodeParamError(err.Error()) + } + + return s.Api().Modify(args.RootHash, args.Path, args.ContentHash, args.ContentType) +} diff --git a/rpc/api/bzz_args.go b/rpc/api/bzz_args.go new file mode 100644 index 0000000000..2aee255e15 --- /dev/null +++ b/rpc/api/bzz_args.go @@ -0,0 +1,322 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with go-ethereum. If not, see . + +package api + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/rpc/shared" +) + +type BzzDepositArgs struct { + Amount *big.Int +} + +func (args *BzzDepositArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + amount, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Amount", "not a string") + } + args.Amount, ok = new(big.Int).SetString(amount, 10) + if !ok { + return shared.NewInvalidTypeError("Amount", "not a number") + } + + return nil +} + +type BzzCashArgs struct { + Cheque *chequebook.Cheque +} + +func (args *BzzCashArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + chequestr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Cheque", "not a string") + } + var cheque chequebook.Cheque + err = json.Unmarshal([]byte(chequestr), &cheque) + if err != nil { + return shared.NewDecodeParamError(err.Error()) + } + args.Cheque = &cheque + + return nil +} + +type BzzIssueArgs struct { + Beneficiary string + Amount *big.Int +} + +func (args *BzzIssueArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 2 { + return shared.NewInsufficientParamsError(len(obj), 2) + } + + beneficiary, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Amount", "not a string") + } + args.Beneficiary = beneficiary + + amount, ok := obj[1].(string) + if !ok { + return shared.NewInvalidTypeError("Amount", "not a string") + } + args.Amount, ok = new(big.Int).SetString(amount, 10) + if !ok { + return shared.NewInvalidTypeError("Amount", "not a number") + } + + return nil +} + +type BzzRegisterArgs struct { + Address, ContentHash, Domain string +} + +func (args *BzzRegisterArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 3 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Address", "not a string") + } + args.Address = addstr + + addstr, ok = obj[1].(string) + if !ok { + return shared.NewInvalidTypeError("Domain", "not a string") + } + args.Domain = addstr + + addstr, ok = obj[2].(string) + if !ok { + return shared.NewInvalidTypeError("ContentHash", "not a string") + } + args.ContentHash = addstr + + return nil +} + +type BzzResolveArgs struct { + Domain string +} + +func (args *BzzResolveArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Domain", "not a string") + } + args.Domain = addstr + + return nil +} + +type BzzDownloadArgs struct { + BzzPath, LocalPath string +} + +func (args *BzzDownloadArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 2 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("BzzPath", "not a string") + } + args.BzzPath = addstr + + addstr, ok = obj[1].(string) + if !ok { + return shared.NewInvalidTypeError("LocalPath", "not a string") + } + args.LocalPath = addstr + + return nil +} + +type BzzUploadArgs struct { + LocalPath, Index string +} + +func (args *BzzUploadArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("LocalPath", "not a string") + } + args.LocalPath = addstr + + if len(obj) > 1 { + addstr, ok := obj[1].(string) + if ok { + args.Index = addstr + } + } + + return nil +} + +type BzzGetArgs struct { + Path string +} + +func (args *BzzGetArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Path", "not a string") + } + args.Path = addstr + + return nil +} + +type BzzPutArgs struct { + Content, ContenType string +} + +func (args *BzzPutArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 1 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("Content", "not a string") + } + args.Content = addstr + + addstr, ok = obj[1].(string) + if !ok { + return shared.NewInvalidTypeError("ContenType", "not a string") + } + args.ContenType = addstr + + return nil +} + +type BzzModifyArgs struct { + RootHash, Path, ContentHash, ContentType string +} + +func (args *BzzModifyArgs) UnmarshalJSON(b []byte) (err error) { + var obj []interface{} + if err := json.Unmarshal(b, &obj); err != nil { + return shared.NewDecodeParamError(err.Error()) + } + + if len(obj) < 2 { + return shared.NewInsufficientParamsError(len(obj), 1) + } + + addstr, ok := obj[0].(string) + if !ok { + return shared.NewInvalidTypeError("RootHash", "not a string") + } + args.RootHash = addstr + + addstr, ok = obj[1].(string) + if !ok { + return shared.NewInvalidTypeError("Path", "not a string") + } + args.Path = addstr + + if len(obj) >= 4 { + addstr, ok = obj[2].(string) + if ok { + args.ContentHash = addstr + } + + addstr, ok = obj[3].(string) + if ok { + args.ContentType = addstr + } + } + + return nil +} diff --git a/rpc/api/bzz_js.go b/rpc/api/bzz_js.go new file mode 100644 index 0000000000..50bbd43d07 --- /dev/null +++ b/rpc/api/bzz_js.go @@ -0,0 +1,95 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with go-ethereum. If not, see . + +package api + +const Bzz_JS = ` +web3._extend({ + property: 'bzz', + methods: + [ + new web3._extend.Method({ + name: 'deposit', + call: 'bzz_deposit', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'info', + call: 'bzz_info', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'cash', + call: 'bzz_cash', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'issue', + call: 'bzz_issue', + params: 2, + inputFormatter: [null, null] + }), + new web3._extend.Method({ + name: 'register', + call: 'bzz_register', + params: 3, + inputFormatter: [null, null, null] + }), + new web3._extend.Method({ + name: 'resolve', + call: 'bzz_resolve', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'download', + call: 'bzz_download', + params: 2, + inputFormatter: [null, null] + }), + new web3._extend.Method({ + name: 'upload', + call: 'bzz_upload', + params: 2, + inputFormatter: [null, null] + }), + new web3._extend.Method({ + name: 'get', + call: 'bzz_get', + params: 1, + inputFormatter: [null] + }), + new web3._extend.Method({ + name: 'put', + call: 'bzz_put', + params: 2, + inputFormatter: [null, null] + }), + new web3._extend.Method({ + name: 'modify', + call: 'bzz_modify', + params: 4, + inputFormatter: [null, null, null, null] + }) + ], + properties: + [ + ] +}); +` diff --git a/rpc/api/utils.go b/rpc/api/utils.go index 794b6abeea..d52f6b3dc3 100644 --- a/rpc/api/utils.go +++ b/rpc/api/utils.go @@ -54,8 +54,28 @@ var ( "startRPC", "stopNatSpec", "stopRPC", + "setGlobalRegistrar", + "setHashReg", + "setUrlHint", + "saveInfo", + "getContractInfo", + "sleep", + "httpGet", "verbosity", }, + "bzz": []string{ + "info", + "issue", + "cash", + "deposit", + "register", + "resolve", + "download", + "upload", + "get", + "put", + "modify", + }, "db": []string{ "getString", "putString", @@ -102,7 +122,7 @@ var ( "hashrate", "mining", "namereg", - "pendingTransactions", + "getPendingTransactions", "resend", "sendRawTransaction", "sendTransaction", @@ -116,6 +136,7 @@ var ( "setExtra", "setGasPrice", "startAutoDAG", + "setEtherbase", "start", "stopAutoDAG", "stop", @@ -173,6 +194,8 @@ func ParseApiString(apistr string, codec codec.Codec, xeth *xeth.XEth, stack *no switch strings.ToLower(strings.TrimSpace(name)) { case shared.AdminApiName: apis[i] = NewAdminApi(xeth, stack, codec) + case shared.BzzApiName: + apis[i] = NewBzzApi(stack, codec) case shared.DebugApiName: apis[i] = NewDebugApi(xeth, eth, codec) case shared.DbApiName: @@ -204,6 +227,8 @@ func Javascript(name string) string { switch strings.ToLower(strings.TrimSpace(name)) { case shared.AdminApiName: return Admin_JS + case shared.BzzApiName: + return Bzz_JS case shared.DebugApiName: return Debug_JS case shared.DbApiName: diff --git a/rpc/shared/utils.go b/rpc/shared/utils.go index b13e9eb1b4..1590265a01 100644 --- a/rpc/shared/utils.go +++ b/rpc/shared/utils.go @@ -20,6 +20,7 @@ import "strings" const ( AdminApiName = "admin" + BzzApiName = "bzz" EthApiName = "eth" DbApiName = "db" DebugApiName = "debug" @@ -37,7 +38,7 @@ const ( var ( // All API's AllApis = strings.Join([]string{ - AdminApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, + AdminApiName, BzzApiName, DbApiName, EthApiName, DebugApiName, MinerApiName, NetApiName, ShhApiName, TxPoolApiName, PersonalApiName, Web3ApiName, }, ",") ) diff --git a/swarm/api/api.go b/swarm/api/api.go new file mode 100644 index 0000000000..11bf4d81ec --- /dev/null +++ b/swarm/api/api.go @@ -0,0 +1,456 @@ +package api + +import ( + "bufio" + "fmt" + "io" + "math/big" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/common/registrar" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +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 { + dpa *storage.DPA + registrar registrar.VersionedRegistrar + conf *Config +} + +//the api constructor initialises +func NewApi(dpa *storage.DPA, registrar registrar.VersionedRegistrar, conf *Config) (self *Api) { + return &Api{dpa, registrar, conf} +} + +// this should move over to chequebook ipc api +func (self *Api) Issue(beneficiary common.Address, amount *big.Int) (cheque *chequebook.Cheque, err error) { + return self.conf.Swap.Chequebook().Issue(beneficiary, amount) +} + +func (self *Api) Cash(cheque *chequebook.Cheque) (txhash string, err error) { + return self.conf.Swap.Chequebook().Cash(cheque) +} + +func (self *Api) Deposit(amount *big.Int) (txhash string, err error) { + return self.conf.Swap.Chequebook().Deposit(amount) +} + +// serialisable info about swarm +type Info struct { + *Config + *chequebook.Params +} + +func (self *Api) Info() *Info { + + return &Info{ + Config: self.conf, + Params: chequebook.ContractParams, + } + +} + +// 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 storage.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":"%v","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 key.String(), 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 trie.hash.String(), nil +} + +const maxParallelFiles = 5 + +// 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]) + "/" + } + glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path) + + //resolving host and port + var key storage.Key + key, err = self.Resolve(hostPort) + if err != nil { + err = errResolve(err) + glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err) + return + } + + trie, err := loadManifest(self.dpa, key) + if err != nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err) + return + } + + type downloadListEntry struct { + key storage.Key + path string + } + + var list []*downloadListEntry + var mde, mderr error + + prevPath := lpath + err = trie.listWithPrefix(path, func(entry *manifestTrieEntry, suffix string) { // TODO: paralellize + key := common.Hex2Bytes(entry.Hash) + path := lpath + "/" + suffix + dir := filepath.Dir(path) + if dir != prevPath { + mde = os.MkdirAll(dir, os.ModePerm) + if mde != nil { + mderr = mde + } + prevPath = dir + } + if (mde == nil) && (path != dir+"/") { + list = append(list, &downloadListEntry{key: key, path: path}) + } + }) + if err == nil { + err = mderr + } + + 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 *downloadListEntry, done chan bool) { + f, err := os.Create(entry.path) // TODO: path separators + if err == nil { + reader := self.dpa.Retrieve(entry.key) + writer := bufio.NewWriter(f) + _, err = io.CopyN(writer, reader, reader.Size()) // TODO: handle errors + err2 := writer.Flush() + if err == nil { + err = err2 + } + err2 = f.Close() + if err == nil { + err = err2 + } + } + + errors[i] = err + done <- true + }(i, entry, done) + } + for dcnt < cnt { + <-done + dcnt++ + } + + if err != nil { + return + } + for i, _ := range list { + if errors[i] != nil { + return errors[i] + } + } + return +} + +// 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) + glog.V(logger.Debug).Infof("[BZZ] 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 storage.Key + hash, err = self.dpa.Store(sr, wg) + if hash != nil { + list[i].Hash = hash.String() + } + 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 = trie.hash.String() + } + 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 { + glog.V(logger.Debug).Infof("[BZZ] 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 storage.Key, err error) { + host := hostPort + if hashMatcher.MatchString(host) { + contentHash = storage.Key(common.Hex2Bytes(host)) + glog.V(logger.Debug).Infof("[BZZ] Swarm: host is a contentHash: '%v'", 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 := crypto.Sha3Hash([]byte(host)) + hash, err = self.registrar.Resolver(version).HashToHash(hostHash) + if err != nil { + err = fmt.Errorf("unable to resolve '%s': %v", hostPort, err) + } + contentHash = storage.Key(hash.Bytes()) + glog.V(logger.Debug).Infof("[BZZ] Swarm: resolve host '%s' to contentHash: '%v'", hostPort, contentHash) + } else { + err = fmt.Errorf("no resolver '%s': %v", hostPort, err) + } + } + return +} + +func (self *Api) getPath(uri string) (reader storage.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] + } + glog.V(logger.Debug).Infof("[BZZ] Swarm: host: '%s', path '%s' requested.", hostPort, path) + + //resolving host and port + var key storage.Key + key, err = self.Resolve(hostPort) + if err != nil { + err = errResolve(err) + glog.V(logger.Debug).Infof("[BZZ] Swarm: error : %v", err) + return + } + + trie, err := loadManifest(self.dpa, key) + if err != nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm: loadManifestTrie error: %v", err) + return + } + + glog.V(logger.Debug).Infof("[BZZ] Swarm: getEntry(%s)", path) + entry, _ := trie.getEntry(path) + if entry != nil { + key = common.Hex2Bytes(entry.Hash) + status = entry.Status + mimeType = entry.ContentType + glog.V(logger.Debug).Infof("[BZZ] Swarm: content lookup key: '%v' (%v)", key, mimeType) + reader = self.dpa.Retrieve(key) + } else { + err = fmt.Errorf("manifest entry for '%s' not found", path) + glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err) + } + return +} diff --git a/swarm/api/api_test.go b/swarm/api/api_test.go new file mode 100644 index 0000000000..81156afcd8 --- /dev/null +++ b/swarm/api/api_test.go @@ -0,0 +1,205 @@ +package api + +import ( + "bytes" + "io/ioutil" + "os" + "path" + "runtime" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +//TODO: add tests for resolver/registrar +// will most likely be its own package under service? + +var ( + testDir string +) + +func init() { + _, filename, _, _ := runtime.Caller(1) + testDir = path.Join(path.Dir(filename), "../test") +} + +func testApi() (api *Api, err error) { + datadir, err := ioutil.TempDir("", "bzz-test") + if err != nil { + return nil, err + } + os.RemoveAll(datadir) + dpa, err := storage.NewLocalDPA(datadir) + if err != nil { + return + } + prvkey, _ := crypto.GenerateKey() + + config, err := NewConfig(datadir, common.Address{}, prvkey) + if err != nil { + return + } + api = NewApi(dpa, nil, config) + api.dpa.Start() + + return +} + +func TestApiPut(t *testing.T) { + api, err := testApi() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + defer api.dpa.Stop() + 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) { + t.Skip("FIXME") + 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; charset=utf-8", 0, 202) + + content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css")) + testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 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 TestApiDirUploadModify(t *testing.T) { + t.Skip("FIXME") + 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 + } + + bzzhash, err = api.Modify(bzzhash, "index.html", "", "") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash, "index2.html", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + bzzhash, err = api.Modify(bzzhash, "img/logo.png", "9ea1f60ebd80786d6005f6b256376bdb494a82496cd86fe8c307cdfb23c99e71", "text/html; charset=utf-8") + 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, "index2.html"), content, "text/html; charset=utf-8", 0, 202) + testGet(t, api, path.Join(bzzhash, "img", "logo.png"), content, "text/html; charset=utf-8", 0, 202) + + content, err = ioutil.ReadFile(path.Join(testDir, "test0", "index.css")) + testGet(t, api, path.Join(bzzhash, "index.css"), content, "text/css", 0, 132) + + _, _, _, _, 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; charset=utf-8", 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; charset=utf-8", 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; charset=utf-8", 0, 202) +} diff --git a/swarm/api/config.go b/swarm/api/config.go new file mode 100644 index 0000000000..6657167984 --- /dev/null +++ b/swarm/api/config.go @@ -0,0 +1,105 @@ +package api + +import ( + "crypto/ecdsa" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/swarm/network" + "github.com/ethereum/go-ethereum/swarm/services/swap" + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + port = "8500" +) + +// separate bzz directories +// allow several bzz nodes running in parallel +type Config struct { + // serialised/persisted fields + *storage.StoreParams + *storage.ChunkerParams + *network.HiveParams + Swap *swap.SwapParams + *network.SyncParams + Path string + Port string + PublicKey string + BzzKey string +} + +// config is agnostic to where private key is coming from +// so managing accounts is outside swarm and left to wrappers +func NewConfig(path string, contract common.Address, prvKey *ecdsa.PrivateKey) (self *Config, err error) { + + address := crypto.PubkeyToAddress(prvKey.PublicKey) // default beneficiary address + dirpath := filepath.Join(path, common.Bytes2Hex(address.Bytes())) + err = os.MkdirAll(dirpath, os.ModePerm) + if err != nil { + return + } + confpath := filepath.Join(dirpath, "config.json") + var data []byte + pubkey := crypto.FromECDSAPub(&prvKey.PublicKey) + pubkeyhex := common.ToHex(pubkey) + keyhex := crypto.Sha3Hash(pubkey).Hex() + + self = &Config{ + SyncParams: network.NewSyncParams(dirpath), + HiveParams: network.NewHiveParams(dirpath), + ChunkerParams: storage.NewChunkerParams(), + StoreParams: storage.NewStoreParams(dirpath), + Port: port, + Path: dirpath, + Swap: swap.DefaultSwapParams(contract, prvKey), + PublicKey: pubkeyhex, + BzzKey: keyhex, + } + data, err = ioutil.ReadFile(confpath) + if err != nil { + if !os.IsNotExist(err) { + return + } + // file does not exist + // write out config file + err = self.Save() + if err != nil { + err = fmt.Errorf("error writing config: %v", err) + } + return + } + // file exists, deserialise + err = json.Unmarshal(data, self) + if err != nil { + return nil, fmt.Errorf("unable to parse config: %v", err) + } + // check public key + if pubkeyhex != self.PublicKey { + return nil, fmt.Errorf("public key does not match the one in the config file %v != %v", pubkeyhex, self.PublicKey) + } + if keyhex != self.BzzKey { + return nil, fmt.Errorf("bzz key does not match the one in the config file %v != %v", keyhex, self.BzzKey) + } + self.Swap.SetKey(prvKey) + + return +} + +func (self *Config) Save() error { + data, err := json.MarshalIndent(self, "", " ") + if err != nil { + return err + } + err = os.MkdirAll(self.Path, os.ModePerm) + if err != nil { + return err + } + confpath := filepath.Join(self.Path, "config.json") + return ioutil.WriteFile(confpath, data, os.ModePerm) +} diff --git a/swarm/api/config_test.go b/swarm/api/config_test.go new file mode 100644 index 0000000000..44bb484667 --- /dev/null +++ b/swarm/api/config_test.go @@ -0,0 +1,107 @@ +package api + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + hexprvkey = "65138b2aa745041b372153550584587da326ab440576b2a1191dd95cee30039c" + defaultConfig = `{ + "ChunkDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "chunks") + `", + "DbCapacity": 5000000, + "CacheCapacity": 5000, + "Radius": 0, + "Branches": 128, + "Hash": "SHA256", + "JoinTimeout": 120, + "SplitTimeout": 120, + "CallInterval": 10000000000, + "KadDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "bzz-peers.json") + `", + "MaxProx": 10, + "ProxBinSize": 8, + "BucketSize": 3, + "PurgeInterval": 151200000000000, + "InitialRetryInterval": 4200000000, + "ConnRetryExp": 2, + "Swap": { + "BuyAt": 20000000000, + "SellAt": 20000000000, + "PayAt": 100, + "DropAt": 10000, + "AutoCashInterval": 300000000000, + "AutoCashThreshold": 50000000000000, + "AutoDepositInterval": 300000000000, + "AutoDepositThreshold": 50000000000000, + "AutoDepositBuffer": 100000000000000, + "PublicKey": "0x045f5cfd26692e48d0017d380349bcf50982488bc11b5145f3ddf88b24924299048450542d43527fbe29a5cb32f38d62755393ac002e6bfdd71b8d7ba725ecd7a3", + "Contract": "0x0000000000000000000000000000000000000000", + "Beneficiary": "0x0d2f62485607cf38d9d795d93682a517661e513e" + }, + "RequestDbPath": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e", "requests") + `", + "RequestDbBatchSize": 512, + "KeyBufferSize": 1024, + "SyncBatchSize": 128, + "SyncBufferSize": 128, + "SyncCacheSize": 1024, + "SyncPriorities": [ + 2, + 1, + 1, + 0, + 0 + ], + "SyncModes": [ + true, + true, + true, + true, + false + ], + "Path": "` + filepath.Join("TMPDIR", "0d2f62485607cf38d9d795d93682a517661e513e") + `", + "Port": "8500", + "PublicKey": "0x045f5cfd26692e48d0017d380349bcf50982488bc11b5145f3ddf88b24924299048450542d43527fbe29a5cb32f38d62755393ac002e6bfdd71b8d7ba725ecd7a3", + "BzzKey": "0xe861964402c0b78e2d44098329b8545726f215afa737d803714a4338552fcb81" +}` +) + +func TestConfigWriteRead(t *testing.T) { + tmp, err := ioutil.TempDir(os.TempDir(), "bzz-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + prvkey := crypto.ToECDSA(common.Hex2Bytes(hexprvkey)) + orig, err := NewConfig(tmp, common.Address{}, prvkey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + account := crypto.PubkeyToAddress(prvkey.PublicKey) + dirpath := filepath.Join(tmp, common.Bytes2Hex(account.Bytes())) + confpath := filepath.Join(dirpath, "config.json") + data, err := ioutil.ReadFile(confpath) + if err != nil { + t.Fatalf("default config file cannot be read: %v", err) + } + exp := strings.Replace(defaultConfig, "TMPDIR", tmp, -1) + + if string(data) != exp { + t.Fatalf("default config mismatch:\nexpected:\n'%v'\ngot:\n'%v'", exp, string(data)) + } + + conf, err := NewConfig(tmp, common.Address{}, prvkey) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if conf.Swap.Beneficiary.Hex() != orig.Swap.Beneficiary.Hex() { + t.Fatalf("expected beneficiary from loaded config %v to match original %v", conf.Swap.Beneficiary.Hex(), orig.Swap.Beneficiary.Hex()) + } + +} diff --git a/swarm/api/ethereum.go b/swarm/api/ethereum.go new file mode 100644 index 0000000000..1027d1e768 --- /dev/null +++ b/swarm/api/ethereum.go @@ -0,0 +1,320 @@ +package api + +import ( + "errors" + "fmt" + "math/big" + "regexp" + "sync" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "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/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! THIS IS CURRENTLY A PLACEHOLEDER (HACK) UNTIL PRC v2 +!! https://github.com/ethereum/go-ethereum/pull/1912 is merged +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +*/ + +var ( + defaultGasPrice = big.NewInt(10000000000000) //150000000000 + defaultGas = big.NewInt(90000) //500000 + addrReg = regexp.MustCompile(`^(0x)?[a-fA-F0-9]{40}$`) +) + +type ethApi struct { + eth *eth.Ethereum + gpo *eth.GasPriceOracle + transactionMu sync.RWMutex + transactMu sync.RWMutex + state *state.StateDB +} + +func NewEthApi(ethereum *eth.Ethereum) *ethApi { + return ðApi{ + eth: ethereum, + gpo: eth.NewGasPriceOracle(ethereum), + } +} + +// subscribes to new head block events and +// waits until blockchain height is greater n at any time +// given the current head, waits for the next chain event +// sets the state to the current head +// loop is async and quit by closing the channel +// used in tests and JS console debug module to control advancing private chain manually +// Note: this is not threadsafe, only called in JS single process and tests +func (self *ethApi) UpdateState() (wait chan *big.Int) { + wait = make(chan *big.Int) + self.state, _ = state.New(self.eth.BlockChain().GetBlockByNumber(0).Root(), self.eth.ChainDb()) + + go func() { + eventSub := self.eth.EventMux().Subscribe(core.ChainHeadEvent{}) + defer eventSub.Unsubscribe() + + var m, n *big.Int + var ok bool + + eventCh := eventSub.Chan() + for { + select { + case event, ok := <-eventCh: + if !ok { + // Event subscription closed, set the channel to nil to stop spinning + eventCh = nil + continue + } + // A real event arrived, process if new head block assignment + if event, ok := event.Data.(core.ChainHeadEvent); ok { + m = event.Block.Number() + if n != nil && n.Cmp(m) < 0 { + wait <- n + n = nil + } + statedb, err := state.New(event.Block.Root(), self.eth.ChainDb()) + if err != nil { + glog.V(logger.Error).Infoln("Could not create new state: %v", err) + return + } + self.state = statedb + } + case n, ok = <-wait: + if !ok { + return + } + } + } + }() + return +} + +func (self *ethApi) AtStateNum(num int64) registrar.Backend { + var st *state.StateDB + var err error + switch num { + case -2: + st = self.eth.Miner().PendingState().Copy() + default: + if block := self.getBlockByHeight(num); block != nil { + st, err = state.New(block.Root(), self.eth.ChainDb()) + if err != nil { + return nil + } + } else { + st, err = state.New(self.eth.BlockChain().GetBlockByNumber(0).Root(), self.eth.ChainDb()) + if err != nil { + return nil + } + } + } + return registrar.Backend(ðApi{ + eth: self.eth, + state: st, + }) +} +func (self *ethApi) GetTxReceipt(txhash common.Hash) *types.Receipt { + return core.GetReceipt(self.eth.ChainDb(), txhash) +} + +func (self *ethApi) StorageAt(addr, storageAddr string) string { + return self.state.GetState(common.HexToAddress(addr), common.HexToHash(storageAddr)).Hex() +} + +func (self *ethApi) CodeAt(address string) string { + return common.ToHex(self.state.GetCode(common.HexToAddress(address))) +} + +func (self *ethApi) Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, dataStr string) (string, string, error) { + statedb := self.state.Copy() + var from *state.StateObject + if len(fromStr) == 0 { + accounts, err := self.eth.AccountManager().Accounts() + if err != nil || len(accounts) == 0 { + from = statedb.GetOrNewStateObject(common.Address{}) + } else { + from = statedb.GetOrNewStateObject(accounts[0].Address) + } + } else { + from = statedb.GetOrNewStateObject(common.HexToAddress(fromStr)) + } + + from.SetBalance(common.MaxBig) + + msg := callmsg{ + from: from, + gas: common.Big(gasStr), + gasPrice: common.Big(gasPriceStr), + value: common.Big(valueStr), + data: common.FromHex(dataStr), + } + if len(toStr) > 0 { + addr := common.HexToAddress(toStr) + msg.to = &addr + } + + if msg.gas.Cmp(big.NewInt(0)) == 0 { + msg.gas = big.NewInt(50000000) + } + + if msg.gasPrice.Cmp(big.NewInt(0)) == 0 { + msg.gasPrice = self.DefaultGasPrice() + } + + header := self.CurrentBlock().Header() + vmenv := core.NewEnv(statedb, self.eth.BlockChain(), msg, header) + gp := new(core.GasPool).AddGas(common.MaxBig) + res, gas, err := core.ApplyMessage(vmenv, msg, gp) + return common.ToHex(res), gas.String(), err +} + +func (self *ethApi) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) { + + if len(toStr) > 0 && toStr != "0x" && !isAddress(toStr) { + return "", errors.New("Invalid address") + } + + var ( + from = common.HexToAddress(fromStr) + to = common.HexToAddress(toStr) + value = common.Big(valueStr) + gas *big.Int + price *big.Int + data []byte + contractCreation bool + ) + + if len(gasStr) == 0 { + gas = DefaultGas() + } else { + gas = common.Big(gasStr) + } + + if len(gasPriceStr) == 0 { + price = self.DefaultGasPrice() + } else { + price = common.Big(gasPriceStr) + } + + data = common.FromHex(codeStr) + if len(toStr) == 0 { + contractCreation = true + } + + self.transactMu.Lock() + defer self.transactMu.Unlock() + + var nonce uint64 + if len(nonceStr) != 0 { + nonce = common.Big(nonceStr).Uint64() + } else { + state := self.eth.TxPool().State() + nonce = state.GetNonce(from) + } + var tx *types.Transaction + if contractCreation { + tx = types.NewContractCreation(nonce, value, gas, price, data) + } else { + tx = types.NewTransaction(nonce, to, value, gas, price, data) + } + + signed, err := self.sign(tx, from, false) + if err != nil { + return "", err + } + if err = self.eth.TxPool().Add(signed); err != nil { + return "", err + } + + if contractCreation { + addr := crypto.CreateAddress(from, nonce) + glog.V(logger.Info).Infof("Tx(%s) created: %s\n", signed.Hash().Hex(), addr.Hex()) + } else { + glog.V(logger.Info).Infof("Tx(%s) to: %s\n", signed.Hash().Hex(), tx.To().Hex()) + } + + return signed.Hash().Hex(), nil +} + +func (self *ethApi) sign(tx *types.Transaction, from common.Address, didUnlock bool) (*types.Transaction, error) { + hash := tx.SigHash() + sig, err := self.doSign(from, hash, didUnlock) + if err != nil { + return tx, err + } + return tx.WithSignature(sig) +} + +func (self *ethApi) doSign(from common.Address, hash common.Hash, didUnlock bool) ([]byte, error) { + sig, err := self.eth.AccountManager().Sign(accounts.Account{Address: from}, hash.Bytes()) + if err == accounts.ErrLocked { + if didUnlock { + return nil, fmt.Errorf("signer account still locked after successful unlock") + } + // retry signing, the account should now be unlocked. + return self.doSign(from, hash, true) + } else if err != nil { + return nil, err + } + return sig, nil +} + +func DefaultGas() *big.Int { return new(big.Int).Set(defaultGas) } + +func (self *ethApi) DefaultGasPrice() *big.Int { + return self.gpo.SuggestPrice() +} + +func (self *ethApi) CurrentBlock() *types.Block { + return self.eth.BlockChain().CurrentBlock() +} + +func (self *ethApi) getBlockByHeight(height int64) *types.Block { + var num uint64 + + switch height { + case -2: + return self.eth.Miner().PendingBlock() + case -1: + return self.CurrentBlock() + default: + if height < 0 { + return nil + } + + num = uint64(height) + } + + return self.eth.BlockChain().GetBlockByNumber(num) +} + +// callmsg is the message type used for call transations. +type callmsg struct { + from *state.StateObject + to *common.Address + gas, gasPrice *big.Int + value *big.Int + data []byte +} + +func isAddress(addr string) bool { + return addrReg.MatchString(addr) +} + +// accessor boilerplate to implement core.Message +func (m callmsg) From() (common.Address, error) { return m.from.Address(), nil } +func (m callmsg) Nonce() uint64 { return m.from.Nonce() } +func (m callmsg) To() *common.Address { return m.to } +func (m callmsg) GasPrice() *big.Int { return m.gasPrice } +func (m callmsg) Gas() *big.Int { return m.gas } +func (m callmsg) Value() *big.Int { return m.value } +func (m callmsg) Data() []byte { return m.data } diff --git a/swarm/api/http.go b/swarm/api/http.go new file mode 100644 index 0000000000..b1cd039cce --- /dev/null +++ b/swarm/api/http.go @@ -0,0 +1,247 @@ +/* +A simple http server interface to Swarm +*/ +package api + +import ( + "bytes" + "io" + "net/http" + "regexp" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +const ( + rawType = "application/octet-stream" +) + +var ( + bzzPrefix = regexp.MustCompile("^/+bzz:/+") + rawUrl = regexp.MustCompile("^/+raw/*") + trailingSlashes = regexp.MustCompile("/+$") + // forever = func() time.Time { return time.Unix(0, 0) } + forever = time.Now +) + +type sequentialReader struct { + reader io.Reader + pos int64 + ahead map[int64](chan bool) + lock sync.Mutex +} + +// browser API for registering bzz url scheme handlers: +// https://developer.mozilla.org/en/docs/Web-based_protocol_handlers +// electron (chromium) api for registering bzz url scheme handlers: +// https://github.com/atom/electron/blob/master/docs/api/protocol.md + +// starts up http server +func StartHttpServer(api *Api, port string) { + serveMux := http.NewServeMux() + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handler(w, r, api) + }) + go http.ListenAndServe(":"+port, serveMux) + glog.V(logger.Info).Infof("[BZZ] Swarm HTTP proxy started on localhost:%s", port) +} + +func handler(w http.ResponseWriter, r *http.Request, api *Api) { + requestURL := r.URL + // This is wrong + // 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 + // } + // } + glog.V(logger.Debug).Infof("[BZZ] 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 + + // HTTP-based URL protocol handler + uri = bzzPrefix.ReplaceAllString(uri, "") + glog.V(logger.Debug).Infof("[BZZ] Swarm: BZZ request URI: '%s'", uri) + + path := rawUrl.ReplaceAllStringFunc(uri, func(string) string { + raw = true + return "" + }) + + glog.V(logger.Debug).Infof("[BZZ] Swarm: %s request '%s' received.", r.Method, uri) + + switch { + case r.Method == "POST" || r.Method == "PUT": + key, err := api.dpa.Store(io.NewSectionReader(&sequentialReader{ + reader: r.Body, + ahead: make(map[int64]chan bool), + }, 0, r.ContentLength), nil) + if err == nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm: Content for %v stored", key.Log()) + } else { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if r.Method == "POST" { + if raw { + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(common.Bytes2Hex(key)))) + } else { + http.Error(w, "No POST to "+uri+" allowed.", http.StatusBadRequest) + return + } + } else { + // PUT + if raw { + http.Error(w, "No PUT to /raw allowed.", http.StatusBadRequest) + return + } else { + path = regularSlashes(path) + mime := r.Header.Get("Content-Type") + // TODO proper root hash separation + glog.V(logger.Debug).Infof("[BZZ] Modify '%s' to store %v as '%s'.", path, key.Log(), mime) + newKey, err := api.Modify(path[:64], path[65:], common.Bytes2Hex(key), mime) + if err == nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey) + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey))) + } else { + http.Error(w, "PUT to "+path+"failed.", http.StatusBadRequest) + return + } + } + } + case r.Method == "DELETE": + if raw { + http.Error(w, "No DELETE to /raw allowed.", http.StatusBadRequest) + return + } else { + path = regularSlashes(path) + glog.V(logger.Debug).Infof("[BZZ] Delete '%s'.", path) + newKey, err := api.Modify(path[:64], path[65:], "", "") + if err == nil { + glog.V(logger.Debug).Infof("[BZZ] Swarm replaced manifest by '%s'", newKey) + w.Header().Set("Content-Type", "text/plain") + http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(newKey))) + } else { + http.Error(w, "DELETE to "+path+"failed.", http.StatusBadRequest) + return + } + } + case r.Method == "GET" || r.Method == "HEAD": + path = trailingSlashes.ReplaceAllString(path, "") + if raw { + // resolving host + key, err := api.Resolve(path) + if err != nil { + glog.V(logger.Error).Infof("[BZZ] Swarm: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // retrieving content + reader := api.dpa.Retrieve(key) + glog.V(logger.Debug).Infof("[BZZ] 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) + glog.V(logger.Debug).Infof("[BZZ] Swarm: Serve raw content '%s' (%d bytes) as '%s'", uri, reader.Size(), mimeType) + + // retrieve path via manifest + } else { + + glog.V(logger.Debug).Infof("[BZZ] 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 { + glog.V(logger.Debug).Infof("[BZZ] Swarm: %v", err) + status = http.StatusBadRequest + } else { + glog.V(logger.Debug).Infof("[BZZ] 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 + } + glog.V(logger.Debug).Infof("[BZZ] 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 { + glog.V(logger.Error).Infof("[BZZ] Swarm: non-sequential read attempted from sequentialReader; %d > %d", + self.pos, off) + panic("Non-sequential read attempt") + } + if self.pos != off { + glog.V(logger.Debug).Infof("[BZZ] 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 + glog.V(logger.Debug).Infof("[BZZ] Swarm: Read %d bytes into buffer size %d from POST, error %v.", + n, len(target), err) + if err != nil { + glog.V(logger.Debug).Infof("[BZZ] 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 { + glog.V(logger.Debug).Infof("[BZZ] 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/swarm/api/manifest.go b/swarm/api/manifest.go new file mode 100644 index 0000000000..c1781ab7d3 --- /dev/null +++ b/swarm/api/manifest.go @@ -0,0 +1,310 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +const ( + manifestType = "application/bzz-manifest+json" +) + +type manifestTrie struct { + dpa *storage.DPA + entries [257]*manifestTrieEntry // indexed by first character of path, entries[256] is the empty path entry + hash storage.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 *storage.DPA, hash storage.Key) (trie *manifestTrie, err error) { // non-recursive, subtrees are downloaded on-demand + + glog.V(logger.Detail).Infof("[BZZ] manifest lookup key: '%v'.", hash.Log()) + // retrieve manifest via DPA + manifestReader := dpa.Retrieve(hash) + return readManifest(manifestReader, hash, dpa) +} + +func readManifest(manifestReader storage.SectionReader, hash storage.Key, dpa *storage.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() { + glog.V(logger.Detail).Infof("[BZZ] Manifest %v not found.", hash.Log()) + if err == nil { + err = fmt.Errorf("Manifest retrieval cut short: read %v, expect %v", size, manifestReader.Size()) + } + return + } + + glog.V(logger.Detail).Infof("[BZZ] Manifest %v retrieved", hash.Log()) + man := manifestJSON{} + err = json.Unmarshal(manifestData, &man) + if err != nil { + err = fmt.Errorf("Manifest %v is malformed: %v", hash.Log(), err) + glog.V(logger.Detail).Infof("[BZZ] %v", err) + return + } + + glog.V(logger.Detail).Infof("[BZZ] Manifest %v has %d entries.", hash.Log(), 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 = entry.subtrie.hash.String() + } + 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)) (err error) { + 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] { + sterr := self.loadSubTrie(entry) + if sterr == nil { + entry.subtrie.listWithPrefixInt(prefix[l:], rp+entry.Path[l:], cb) + } else { + err = sterr + } + } + } else { + if (epl >= plen) && (prefix == entry.Path[:plen]) { + cb(entry, rp+entry.Path[plen:]) + } + } + } + } + return +} + +func (self *manifestTrie) listWithPrefix(prefix string, cb func(entry *manifestTrieEntry, suffix string)) (err error) { + return self.listWithPrefixInt(prefix, "", cb) +} + +func (self *manifestTrie) findPrefixOf(path string) (entry *manifestTrieEntry, pos int) { + + glog.V(logger.Detail).Infof("[BZZ] 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) + glog.V(logger.Detail).Infof("[BZZ] path = %v entry.Path = %v epl = %v", path, entry.Path, epl) + if (len(path) >= epl) && (path[:epl] == entry.Path) { + glog.V(logger.Detail).Infof("[BZZ] 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] + } + return +} + +func (self *manifestTrie) getEntry(spath string) (entry *manifestTrieEntry, fullpath string) { + path := regularSlashes(spath) + var pos int + entry, pos = self.findPrefixOf(path) + return entry, path[:pos] +} diff --git a/swarm/api/manifest_test.go b/swarm/api/manifest_test.go new file mode 100644 index 0000000000..558bdfb51b --- /dev/null +++ b/swarm/api/manifest_test.go @@ -0,0 +1,61 @@ +package api + +import ( + // "encoding/json" + "fmt" + "io" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/swarm/storage" +) + +func manifest(paths ...string) (manifestReader storage.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", "a/ba", "a/b/c") + 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/swarm/api/roundtripper.go b/swarm/api/roundtripper.go new file mode 100644 index 0000000000..3dad7d04ef --- /dev/null +++ b/swarm/api/roundtripper.go @@ -0,0 +1,22 @@ +package api + +import ( + "fmt" + "net/http" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + + // "github.com/ethereum/go-ethereum/common/httpclient" + // "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) + glog.V(logger.Info).Infof("[BZZ] roundtripper: proxying request '%s' to '%s'", req.RequestURI, url) + return http.Get(url) +} diff --git a/swarm/api/roundtripper_test.go b/swarm/api/roundtripper_test.go new file mode 100644 index 0000000000..e0b15658dc --- /dev/null +++ b/swarm/api/roundtripper_test.go @@ -0,0 +1,50 @@ +package api + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common/httpclient" +) + +func TestRoundTripper(t *testing.T) { + serveMux := http.NewServeMux() + serveMux.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(":8600", serveMux) + + rt := &RoundTripper{"8600"} + client := httpclient.New("/") + client.RegisterProtocol("bzz", rt) + + resp, err := client.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/swarm/cmd/bzzhash/bzzhash.go b/swarm/cmd/bzzhash/bzzhash.go new file mode 100644 index 0000000000..d4df5fb50b --- /dev/null +++ b/swarm/cmd/bzzhash/bzzhash.go @@ -0,0 +1,38 @@ +// bzzhash +package main + +import ( + "fmt" + "io" + "os" + "runtime" + + "github.com/ethereum/go-ethereum/swarm/storage" +) + +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 := storage.NewTreeChunker(storage.NewChunkerParams()) + 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/swarm/cmd/bzzup.sh b/swarm/cmd/bzzup.sh new file mode 100755 index 0000000000..dc6490ad70 --- /dev/null +++ b/swarm/cmd/bzzup.sh @@ -0,0 +1,46 @@ +#! /bin/bash + +INDEX='index.html' +port="8500" +delimiter='{"entries":[{' + +if [[ ! -z "$2" ]]; then + port="$2" +fi + +if [ -f "$1" ]; then +hash=`wget -q -O- --post-file="$1" http://localhost:$port/raw` +mime=`mimetype -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:$port/raw` +mime=`mimetype -b "$path"` +if [ "_$name" = '_.' ]; then +echo -n "\"hash\":\"$hash\",\"contentType\":\"$mime\"" +else +echo -n "\"hash\":\"$hash\",\"path\":\"$name\",\"contentType\":\"$mime\"" +fi +delimiter='},{' + +done +echo -n '}]}') | wget -q -O- --post-data=`cat` http://localhost:$port/raw +echo + +popd > /dev/null + +fi diff --git a/swarm/examples/album/add.png b/swarm/examples/album/add.png new file mode 100644 index 0000000000..6d87680603 Binary files /dev/null and b/swarm/examples/album/add.png differ diff --git a/swarm/examples/album/back.png b/swarm/examples/album/back.png new file mode 100644 index 0000000000..d05b45d9bd Binary files /dev/null and b/swarm/examples/album/back.png differ diff --git a/swarm/examples/album/cut-left.png b/swarm/examples/album/cut-left.png new file mode 100644 index 0000000000..f24692daa3 Binary files /dev/null and b/swarm/examples/album/cut-left.png differ diff --git a/swarm/examples/album/cut-mov.png b/swarm/examples/album/cut-mov.png new file mode 100644 index 0000000000..9744870681 Binary files /dev/null and b/swarm/examples/album/cut-mov.png differ diff --git a/swarm/examples/album/cut-right.png b/swarm/examples/album/cut-right.png new file mode 100644 index 0000000000..c712544a5f Binary files /dev/null and b/swarm/examples/album/cut-right.png differ diff --git a/swarm/examples/album/cut-top.png b/swarm/examples/album/cut-top.png new file mode 100644 index 0000000000..61b0fd5978 Binary files /dev/null and b/swarm/examples/album/cut-top.png differ diff --git a/swarm/examples/album/data.json b/swarm/examples/album/data.json new file mode 100644 index 0000000000..20efd5d4f0 --- /dev/null +++ b/swarm/examples/album/data.json @@ -0,0 +1 @@ +{"blur":[600,336],"data":[{"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAEBv/EACEQAAAEBQUAAAAAAAAAAAAAAAABAgMFCBESFSIlMVNh/8QAFwEAAwEAAAAAAAAAAAAAAAAAAAIDBf/EAB8RAAIBAgcAAAAAAAAAAAAAAAADAQIFBhESFBYxUv/aAAwDAQACEQMRAD8AQcxWXhlzcAaStB66uHQy8FF4mfXGekzItq56JpcwDN6thb57AvJneQ2FB//Z","file":["imgs/apron.jpg"],"img":["imgs/apron.jpg",[1600,1059]],"thumb":["data:image/jpeg;base64,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",[169,112]]},{"img":["imgs/bekas1.jpg",[1600,1059]],"thumb":["data:image/jpeg;base64,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",[169,112]],"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAABB//EACAQAAIBBAEFAAAAAAAAAAAAAAECAwAEBREGBxIhMUH/xAAWAQEBAQAAAAAAAAAAAAAAAAAFAwT/xAAeEQABAwQDAAAAAAAAAAAAAAABAAIDBAURIRQyQf/aAAwDAQACEQMRAD8And/zrLQzK9nBFaoGBHnZ19pCW7SmQkHXiPjoGNZhw2h+oefZ2IykignYHb6rIbhUk9lfhxr/2Q=="},{"thumb":["data:image/jpeg;base64,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",[169,112]],"img":["imgs/bekas2.jpg",[1600,1059]],"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAEB//EACAQAAECBQUAAAAAAAAAAAAAAAMABAECBxESFCNSYXH/xAAXAQADAQAAAAAAAAAAAAAAAAABAgMF/8QAHhEAAgIBBQEAAAAAAAAAAAAAAQMAAhEEBRQWIVH/2gAMAwEAAhEDEQA/AIkCpj5sFuDSDMTLcJPx6Ulb++tcWOTM86NNj4IqapjrKNmwbeI9if8AI3CXP//Z"},{"stamp":4,"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAFBv/EACAQAAAGAAcAAAAAAAAAAAAAAAABAgMEBQYRFBdDUpH/xAAWAQEBAQAAAAAAAAAAAAAAAAAEAwX/xAAeEQABBAEFAAAAAAAAAAAAAAAAAQIDBAYRFBYiMv/aAAwDAQACEQMRAD8AhqfGtrXtElyRqMu6QCPILcSJ31Mp9au5fIpuZNLha8COT2iOygP/2Q==","thumb":["data:image/jpeg;base64,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",[169,112]],"img":["imgs/bekas_blanik.jpg",[1600,1059]]},{"stamp":5,"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAEBv/EAB8QAAECBgMAAAAAAAAAAAAAAAECEQADBAUGEiEiM//EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAHREAAgIBBQAAAAAAAAAAAAAAAAIBExESITFRYf/aAAwDAQACEQMRAD8ABZMSmVpBYoCB01mM0T6VVt5zALmxwVCcdu6UgCp4Ab0MK6OgW+H/2Q==","img":["imgs/hangar_inside.jpg",[1600,1059]],"thumb":["data:image/jpeg;base64,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",[169,112]]},{"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAUAAEAAAAAAAAAAAAAAAAAAAAF/8QAIBAAAQIFBQAAAAAAAAAAAAAAAgEEAAMFBhIRFSFhcv/EABUBAQEAAAAAAAAAAAAAAAAAAAQF/8QAHhEAAQMEAwAAAAAAAAAAAAAAAAECAwYREhUWUmL/2gAMAwEAAhEDEQA/AE31/XI4MRZs2R08jyKXOVchTqLzaimvkrSNqY8bIojv6LytNl6+4VyWXoG03o//2Q==","thumb":["data:image/jpeg;base64,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",[169,112]],"img":["imgs/hangar.jpg",[1600,1059]]},{"img":["imgs/ruin.jpg",[1600,1059]],"thumb":["data:image/jpeg;base64,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",[169,112]],"blur":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gADKv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIAAcACgMBEQACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAEB//EAB8QAAICAQQDAAAAAAAAAAAAAAECAwQRAAUIFCJRYf/EABYBAQEBAAAAAAAAAAAAAAAAAAQAAv/EAB8RAAEDAwUAAAAAAAAAAAAAAAABAgQFFSERFDJRUv/aAAwDAQACEQMRAD8AFPyPsWtum72xxTRIPKWBwjD6AdMSuSl4uA26NrhuCZy8hqjSuUvWkUscKYc4HrW7xM7LYRvJ/9k="}],"thumb":{"min":[150,112],"max":[267,200]}} diff --git a/swarm/examples/album/delete.png b/swarm/examples/album/delete.png new file mode 100644 index 0000000000..dfa04c717b Binary files /dev/null and b/swarm/examples/album/delete.png differ diff --git a/swarm/examples/album/down.png b/swarm/examples/album/down.png new file mode 100644 index 0000000000..231645e91a Binary files /dev/null and b/swarm/examples/album/down.png differ diff --git a/swarm/examples/album/download.png b/swarm/examples/album/download.png new file mode 100644 index 0000000000..6a160c8d52 Binary files /dev/null and b/swarm/examples/album/download.png differ diff --git a/swarm/examples/album/eye.png b/swarm/examples/album/eye.png new file mode 100644 index 0000000000..562942a580 Binary files /dev/null and b/swarm/examples/album/eye.png differ diff --git a/swarm/examples/album/imgs/apron.jpg b/swarm/examples/album/imgs/apron.jpg new file mode 100644 index 0000000000..71f0d88adb Binary files /dev/null and b/swarm/examples/album/imgs/apron.jpg differ diff --git a/swarm/examples/album/imgs/bekas1.jpg b/swarm/examples/album/imgs/bekas1.jpg new file mode 100644 index 0000000000..8e38e5f55b Binary files /dev/null and b/swarm/examples/album/imgs/bekas1.jpg differ diff --git a/swarm/examples/album/imgs/bekas2.jpg b/swarm/examples/album/imgs/bekas2.jpg new file mode 100644 index 0000000000..92f9e1db91 Binary files /dev/null and b/swarm/examples/album/imgs/bekas2.jpg differ diff --git a/swarm/examples/album/imgs/bekas_blanik.jpg b/swarm/examples/album/imgs/bekas_blanik.jpg new file mode 100644 index 0000000000..4cd4abb73e Binary files /dev/null and b/swarm/examples/album/imgs/bekas_blanik.jpg differ diff --git a/swarm/examples/album/imgs/hangar.jpg b/swarm/examples/album/imgs/hangar.jpg new file mode 100644 index 0000000000..e8f1e0d610 Binary files /dev/null and b/swarm/examples/album/imgs/hangar.jpg differ diff --git a/swarm/examples/album/imgs/hangar_inside.jpg b/swarm/examples/album/imgs/hangar_inside.jpg new file mode 100644 index 0000000000..3925c6fbd2 Binary files /dev/null and b/swarm/examples/album/imgs/hangar_inside.jpg differ diff --git a/swarm/examples/album/imgs/ruin.jpg b/swarm/examples/album/imgs/ruin.jpg new file mode 100644 index 0000000000..f138eb6f70 Binary files /dev/null and b/swarm/examples/album/imgs/ruin.jpg differ diff --git a/swarm/examples/album/index.css b/swarm/examples/album/index.css new file mode 100644 index 0000000000..b77c3198ce --- /dev/null +++ b/swarm/examples/album/index.css @@ -0,0 +1,291 @@ +/* General reset */ +html, body +{ + overflow: hidden; /* IE<9 */ + padding: 0; + margin: 0; + border: 0; +} + +img +{ + border: none; +} + +/* Main gallery elements */ +#gallery h2 +{ + margin: 1em; + font-family: monospace; +} + +#gallery +{ + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #111; +} + +#gallery.no-cursor * +{ + cursor: none !important; +} + +#gallery a, #gallery a:active, #gallery a:focus +{ + outline: none; +} + +#gallery #background +{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#gallery #noise +{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url(noise.png); + background-repeat: repeat; +} + +#gallery #content +{ + position: absolute; + top: 0; + left: 0; +} + +#gallery #flash +{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; +} + +/* Main image */ +#gallery #content img.current +{ + box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 2.5em rgba(0, 0, 0, 0.5); +} + +/* Header */ +#gallery #content #header +{ + -webkit-user-select: text; + -khtml-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + + position: absolute; + top: 0; + left: 0; + color: #fff; + background: #111; /* IE<9 */ + background: rgba(0, 0, 0, 0.7); + font-family: sans-serif; + padding: 0.5em; +} + +#gallery #content #header img +{ + vertical-align: middle; + height: 1em; +} + +#gallery #content #header #throbber +{ + height: 100%; +} + +#gallery #content #header a +{ + text-decoration: none; + color: #fff; +} + +#gallery #content #header a:hover +{ + text-decoration: underline; +} + +/* Navigation arrows */ +#gallery #content #left, +#gallery #content #right +{ + position: absolute; + width: 5%; + min-width: 2.5em; + top: 0; + bottom: 0; +} + +#gallery #content #left +{ + left: 0; +} + +#gallery #content #right +{ + right: 0; +} + +#gallery #content #left div, +#gallery #content #right div +{ + position: absolute; + cursor: pointer; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0.3; + filter: alpha(opacity=30); +} + +#gallery #content #left div:hover, +#gallery #content #right div:hover +{ + opacity: 0.6; + filter: alpha(opacity=60); +} + +#gallery #content #left img, +#gallery #content #right img +{ + position: absolute; + display: block; + margin: auto; + top: 0; + bottom: 0; +} + +#gallery #content #left img +{ + left: 25%; +} + +#gallery #content #right img +{ + right: 25%; +} + +/* Thumbnail list */ +#gallery #list +{ + position: absolute; + background: rgba(255, 255, 255, 0.1); + padding-top: 0.5em; + padding-left: 0.5em; + + box-shadow: 0 0 1em rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1em rgba(0, 0, 0, 0.5); +} + +#gallery #list:focus +{ + outline: none; +} + +/* Invidivual thumbnails */ +#gallery #list .thumb +{ + display: inline-block; + margin-bottom: 0.5em; + margin-right: 0.5em; + + box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0.25em 0.25em 0.25em rgba(0, 0, 0, 0.5); +} + +#gallery #list .thumb a +{ + display: block; + position: relative; + border: 2px solid #111 +} + +#gallery #list .thumb.current a +{ + border: 2px solid #f00; +} + +#gallery #list .thumb a:hover, +#gallery #list .thumb a:focus +{ + border: 2px solid #fff; +} + +#gallery #list .thumb.current a:hover, +#gallery #list .thumb.current a:focus +{ + border: 2px solid #f00; +} + +/* Thumbnail styles */ +#gallery #list .thumb .ovr +{ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#gallery #list .thumb.cut-left .ovr +{ + background: url(cut-left.png) top left repeat-y; +} + +#gallery #list .thumb.cut-right .ovr +{ + background: url(cut-right.png) repeat-y top right; +} + +#gallery #list .thumb.cut-left.cut-right .ovr +{ + background: url(cut-left.png) top left repeat-y, url(cut-right.png) repeat-y top right; +} + +#gallery #list .thumb.cut-top .ovr +{ + background: url(cut-top.png) top left repeat-x; +} + +#gallery #list .thumb.cut-bottom .ovr +{ + background: url(cut-right.png) repeat-x bottom left; +} + +#gallery #list .thumb.cut-top.cut-bottom .ovr +{ + background: url(cut-left.png) top left repeat-x, url(cut-right.png) repeat-x bottom left; +} + +#gallery #list .thumb.movie .ovr +{ + background: url(cut-mov.png) top left repeat-y, url(cut-mov.png) top right repeat-y; +} diff --git a/swarm/examples/album/index.html b/swarm/examples/album/index.html new file mode 100644 index 0000000000..e42413c030 --- /dev/null +++ b/swarm/examples/album/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/swarm/examples/album/index.js b/swarm/examples/album/index.js new file mode 100644 index 0000000000..5bce32b3e3 --- /dev/null +++ b/swarm/examples/album/index.js @@ -0,0 +1,836 @@ +// fgallery: a modern, minimalist javascript photo gallery +// Copyright(c) 2003-2014 by wave++ "Yuri D'Elia" +// Distributed under GPL2 (see COPYING) WITHOUT ANY WARRANTY. +var datafile = 'data.json'; +var padding = 22; +var duration = 500; +var thrdelay = 1500; +var hidedelay = 3000; +var prefetch = 1; +var minupscale = 640 * 480; +var thumbrt = 16/9 - 5/3; +var cutrt = 0.15; + +Element.Events.hashchange = +{ + onAdd: function() + { + var hash = window.location.hash; + + var hashchange = function() + { + if(hash == window.location.hash) return; + else hash = window.location.hash; + + var value = (!hash.indexOf('#')? hash.substr(1): hash); + window.fireEvent('hashchange', value); + document.fireEvent('hashchange', value); + }; + + if("onhashchange" in window + && (!Browser.ie || Browser.version > 7)) + window.onhashchange = hashchange; + else + hashchange.periodical(50); + } +}; + +// some state variables +var emain; // main object +var eback; // background +var enoise; // additive noise +var eflash; // flashing object +var ehdr; // header +var elist; // thumbnail list +var fscr; // thumbnail list scroll fx +var econt; // picture container +var ebuff; // picture buffer +var eleft; // go left +var eright; // go right +var oimg; // old image +var eimg; // new image +var cthumb; // current thumbnail +var mthumb; // missing thumbnails +var eidx; // current index +var tthr; // throbber timeout +var imgs; // image list +var first; // first image +var idle; // idle timer +var clayout; // current layout +var csr; // current scaling ratio + +function resize() +{ + // best layout + var msize = emain.getSize(); + var rt = (imgs.thumb.min[0] / imgs.thumb.min[1]); + var maxw = msize.x - imgs.thumb.min[0] - padding; + var maxh = msize.y * rt - imgs.thumb.min[1] - padding; + var layout = (maxw >= maxh? 'horizontal': 'vertical'); + + // calculate a good multiplier for the thumbnail size + var m = (layout == 'horizontal'? + (msize.x * window.devicePixelRatio * thumbrt) / imgs.thumb.min[0]: + (msize.y * window.devicePixelRatio * thumbrt) / imgs.thumb.min[1]); + if(m >= 1) + m = Math.pow(2, Math.floor(Math.log(m) / Math.LN2)); + else + m = Math.pow(2, Math.ceil(Math.log(m) / Math.LN2)); + var sr = m / window.devicePixelRatio; + + if(layout != clayout || sr != csr) + { + onLayoutChanged(layout, sr); + if(cthumb) centerThumb(0); + clayout = layout; + csr = sr; + } + + // resize main container + var epos = elist.getPosition(); + if(layout == 'horizontal') + { + econt.setStyles( + { + 'width': epos.x, + 'height': msize.y + }); + } + else + { + econt.setStyles( + { + width: msize.x, + height: epos.y + }); + } + + if(oimg) resizeMainImg(oimg); + if(eimg) resizeMainImg(eimg); +} + +function onLayoutChanged(layout, sr) +{ + elist.setStyle('display', 'none'); + + // refit the thumbnails, cropping edges + imgs.data.each(function(x, i) + { + var crop = x.thumb[1]; + var size = (x.thumb[2]? x.thumb[2]: crop); + var offset = (x.thumb[3]? x.thumb[3]: [0, 0]); + var center = (x.center? [x.center[0] / 1000, x.center[1] / 1000]: [0.5, 0.5]); + + var maxw, maxh; + if(layout == 'horizontal') + { + maxw = imgs.thumb.min[0]; + maxh = Math.round(maxw * (crop[1] / crop[0])); + maxh = Math.max(maxh, imgs.thumb.min[1]); + maxh = Math.min(maxh, imgs.thumb.max[1]); + } + else + { + maxh = imgs.thumb.min[1]; + maxw = Math.round(maxh * (crop[0] / crop[1])); + maxw = Math.max(maxw, imgs.thumb.min[0]); + maxw = Math.min(maxw, imgs.thumb.max[0]); + } + + x.eimg.setStyles( + { + 'width': Math.round(maxw * sr), + 'height': Math.round(maxh * sr), + 'background-size': Math.round(crop[0] * sr) + "px " + Math.round(crop[1] * sr) + "px" + }); + + // center cropped thumbnail + var dx = maxw - crop[0]; + var cx = size[0] * center[0] - offset[0]; + cx = Math.round(crop[0] / 2 - cx + dx / 2); + cx = Math.max(Math.min(0, cx), dx); + + var dy = maxh - crop[1]; + var cy = size[1] * center[1] - offset[1]; + cy = Math.round(crop[1] / 2 - cy + dy / 2); + cy = Math.max(Math.min(0, cy), dy); + + x.eimg.setStyle('background-position', Math.round(cx * sr) + 'px ' + Math.round(cy * sr) + 'px'); + + // border styles + var classes = ['cut-left', 'cut-right', 'cut-top', 'cut-bottom']; + classes.each(function(c) { x.ethumb.removeClass(c); }); + + var wx = Math.round(size[0] * cutrt); + if((offset[0] - cx) > wx) x.ethumb.addClass('cut-left'); + if((cx - offset[0] + size[0] - maxw) > wx) x.ethumb.addClass('cut-right'); + + var wy = Math.round(size[1] * cutrt); + if((offset[1] - cy) > wy) x.ethumb.addClass('cut-top'); + if((cy - offset[1] + size[1] - maxh) > wy) x.ethumb.addClass('cut-bottom'); + }); + + // resize thumbnail list + if(layout == 'horizontal') + { + elist.setStyles( + { + 'top': 0, + 'left': 'auto', + 'right': 0, + 'bottom': 0, + 'overflow-y': 'scroll', + 'overflow-x': 'hidden', + 'white-space': 'pre-line' + }); + } + else + { + elist.setStyles( + { + 'top': 'auto', + 'left': 0, + 'right': 0, + 'bottom': 0, + 'overflow-y': 'hidden', + 'overflow-x': 'scroll', + 'white-space': 'nowrap' + }); + } + + elist.setStyle('display', 'block'); +} + +function resizeMainImg(img) +{ + var contSize = econt.getSize(); + var listSize = elist.getSize(); + var thumbWidth = (clayout == 'horizontal'? listSize.x: listSize.y); + var data = imgs.data[img.idx].img; + var width = data[1][0]; + var height = data[1][1]; + var imgrt = width / height; + var pad = padding * 2; + + if(imgrt > (contSize.x / contSize.y)) + { + img.width = Math.max(thumbWidth + pad, contSize.x - pad); + img.height = img.width / imgrt; + } + else + { + img.height = Math.max(thumbWidth + pad, contSize.y - pad); + img.width = img.height * imgrt; + } + if(width * height <= minupscale && img.width > width) + { + img.width = width; + img.height = height; + } + + img.setStyles( + { + 'position': 'absolute', + 'top': contSize.y / 2 - img.height / 2, + 'left': contSize.x / 2 - img.width / 2 + }); +} + +function ts() +{ + var date = new Date(); + return date.getTime(); +} + +function detectSlowness(start) +{ + var end = ts(); + var delta = end - start; + if(delta > duration * 2) + duration = 0; +} + +function centerThumb(duration) +{ + var thumbPos = cthumb.getPosition(); + var thumbSize = cthumb.getSize(); + var listSize = elist.getSize(); + var listScroll = elist.getScroll(); + + var x = thumbPos.x + listScroll.x - listSize.x / 2 + thumbSize.x / 2; + var y = thumbPos.y + listScroll.y - listSize.y / 2 + thumbSize.y / 2; + + if(fscr) fscr.cancel(); + fscr = new Fx.Scroll(elist, { duration: duration }).start(x, y); +} + +function sendImgs(xhr, uri) { + // set up request + xhr.open("PUT", uri + "data.json", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + // send the collected data as JSON + xhr.send(JSON.stringify(imgs)); +} + +function imageToUrl(img, w, h) { + var can = document.createElement('canvas'); + can.width = w; + can.height = h; + var cntxt = can.getContext("2d"); + cntxt.drawImage(img, 0, 0, w, h); + return can.toDataURL(); +} + +function uploadFile(files, nr, uri) { + if(files.length <= nr) { + if(uri != "") { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { if (xhr.readyState === 4) { + var i = xhr.responseText; + window.location.replace("/" + i + "/"); + }}; + sendImgs(xhr, uri); + } + return; + } + var imageType = /^image\//; + var file = files[nr]; + if(!imageType.test(file.type)) { + uploadFile(files, nr + 1, uri); + return; + } + + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { if (xhr.readyState === 4) { + var i = xhr.responseText; + + // insert image into index + var img = new Image(); + img.onload = function() { + var blur = imageToUrl(img, 5, 5); + var thumbData = []; + var thumbSize = 200; + if(img.naturalWidth > img.naturalHeight) { + // landscape thumbnail + var h = img.naturalHeight * thumbSize / img.naturalWidth; + thumbData[0] = imageToUrl(img, thumbSize, h); + thumbData[1] = [thumbSize, h]; + } else { + // portrait thumbnail + var w = img.naturalWidth * thumbSize / img.naturalHeight; + thumbData[0] = imageToUrl(img, w, thumbsize); + thumbData[1] = [w, thumbSize]; + } + // update index + var imgData = []; + imgData[0] = "imgs/" + file.name; + imgData[1] = [img.naturalWidth, img.naturalHeight]; + imgs.data.splice(eidx, 0, {img: imgData, thumb: thumbData, blur: blur}); + uploadFile(files, nr + 1, "/" + i + "/"); + } + img.src = "/" + i + "/imgs/" + file.name; + return; + }}; + xhr.open("PUT", uri + "imgs/" + file.name, true); + xhr.setRequestHeader('Content-Type', file.type); + + var reader = new FileReader(); + reader.onload = function(evt) { + xhr.send(evt.target.result); + }; + reader.readAsArrayBuffer(file); +} + +function handleFiles(files) { + uploadFile(files, 0, ""); +} + +function deleteImg() +{ + if(imgs.data.length < 2) return; // empty albums not allowed + var fname = imgs.data[eidx].img[0]; + imgs.data.splice(eidx,1); + + // construct an HTTP request + var xhr = new XMLHttpRequest(); + + // set response handler + xhr.onreadystatechange = function () { if (xhr.readyState === 4) { + var i = xhr.responseText; + var xhrd = new XMLHttpRequest(); + xhrd.onreadystatechange = function () { if (xhrd.readyState === 4) { + var j = xhrd.responseText; + window.location.replace("/" + j + "/"); + }}; + xhrd.open("DELETE", "/" + i + "/" + fname, true); + xhrd.send(); + }}; + + sendImgs(xhr, ""); +} + +function moveUpDown(off) +{ + var me = imgs.data[eidx]; + imgs.data[eidx] = imgs.data[eidx + off]; + imgs.data[eidx + off] = me; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { if (xhr.readyState === 4) { + var i = xhr.responseText; + window.location.replace("/" + i + "/#" + (eidx + off)); + }}; + sendImgs(xhr, ""); +} + +function moveUp() +{ + moveUpDown(-1); +} + +function moveDown() +{ + moveUpDown(1); +} + +function onMainReady() +{ + resizeMainImg(eimg); + eimg.setStyle('opacity', 0); + eimg.addClass('current'); + eimg.inject(ebuff); + + // setup header + var dsc = []; + if(imgs.index) + dsc.push(""); + + // delete image + if(imgs.data.length > 1) + dsc.push(""); + + // add image + dsc.push(""); + + // up image + if(eidx > 0) + dsc.push(""); + + // down image + if(eidx < imgs.data.length - 1) + dsc.push(""); + + if(imgs.data[eidx].file) + { + var img = imgs.data[eidx].file[0]; + dsc.push(""); + eimg.addEvent('click', function() { window.location = img; }); + eimg.setStyle('cursor', 'pointer'); // fallback + eimg.setStyle('cursor', 'zoom-in'); + } + if(imgs.download) + dsc.push(""); + if(imgs.data[eidx].date) + dsc.push("Date: " + imgs.data[eidx].date); + ehdr.set('html', dsc.join(' ')); + ehdr.setStyle('display', (dsc.length? 'block': 'none')); + + // setup upload file selector + var fileSelect = document.getElementById("fileSelect"), + fileElem = document.getElementById("fileElem"); + fileSelect.addEventListener("click", function (e) { + if (fileElem) { + fileElem.click(); + } + e.preventDefault(); // prevent navigation to "#" + }, false); + + // complete thumbnails + var d = duration; + if(first !== false) + { + first = false; + loadAllThumbs(); + d = 0; + } + + // start animations + if(oimg) + { + oimg.removeClass('current'); + var fx = oimg.get('tween'); + fx.cancel(); + fx.duration = d; + fx.removeEvents('complete'); + fx.addEvent('complete', function(x) { x.destroy(); }); + fx.start('opacity', 0); + oimg = undefined; + } + + var fx = new Fx.Tween(eimg, { duration: d }); + if(d) + { + var now = ts(); + fx.addEvent('complete', function() + { + detectSlowness(now); + }); + } + eimg.set('tween', fx); + fx.start('opacity', 1); + + var rp = Math.floor(Math.random() * 100); + eback.src = imgs.data[eidx].blur; + enoise.setStyle('background-position', rp + 'px ' + rp + 'px'); + + clearTimeout(tthr); + idle.start(); + showHdr(); + centerThumb(d); + + // prefetch next image + if(prefetch && eidx != imgs.data.length - 1) + { + var data = imgs.data[eidx + 1]; + Asset.images([data.img[0], data.blur]); + } +} + +function showThrobber() +{ + var img = new Element('img', { id: 'throbber' }); + img.src = "throbber.gif"; + ehdr.empty(); + img.inject(ehdr); + ehdr.setStyle('display', 'block'); + idle.stop(); + showHdr(); +} + +function hideHdr() +{ + if(idle.started && ehdr.getStyle('opacity') !== 0) + ehdr.tween('opacity', [1, 0], { link: 'ignore' }); +} + +function hideNav() +{ + emain.addClass('no-cursor'); + eleft.tween('opacity', [1, 0], { link: 'ignore' }); + eright.tween('opacity', [1, 0], { link: 'ignore' }); +} + +function showHdr() +{ + ehdr.get('tween').cancel(); + ehdr.fade('show'); +} + +function showNav() +{ + emain.removeClass('no-cursor'); + eleft.get('tween').cancel(); + eleft.fade('show'); + eright.get('tween').cancel(); + eright.fade('show'); +} + +function flash() +{ + eflash.setStyle('display', 'block'); + eflash.tween('opacity', [1, 0]); +} + +function prev() +{ + if(eidx != 0) + switchTo(eidx - 1); + else + { + flash(); + switchTo(imgs.data.length - 1); + } +} + +function next() +{ + if(eidx != imgs.data.length - 1) + switchTo(eidx + 1); + else + { + flash(); + switchTo(0); + } +} + +function switchTo(i) +{ + window.location.replace("#" + i); +} + +function load(i) +{ + if(i == eidx) return; + doLoad(i); +} + +function doLoad(i) +{ + var data = imgs.data[i]; + var assets = Asset.images([data.img[0], data.blur], + { + onComplete: function() { if(i == eidx) onMainReady(); } + }); + + if(!oimg) oimg = eimg; + eimg = assets[0]; + eimg.idx = eidx = i; + + if(cthumb) cthumb.removeClass('current'); + cthumb = imgs.data[eidx].ethumb; + cthumb.addClass('current'); + + clearTimeout(tthr); + tthr = showThrobber.delay(thrdelay); +} + +function getLocationIndex() +{ + var hash = window.location.hash; + var idx = parseInt(!hash.indexOf('#')? hash.substr(1): hash); + if(isNaN(idx) || idx < 0) + idx = 0; + else if(idx >= imgs.data.length) + idx = imgs.data.length - 1; + return idx; +} + +function change() +{ + load(getLocationIndex()); +} + +function loadThumb(i) +{ + var x = imgs.data[i]; + x.eimg.setStyle('background-image', 'url(' + encodeURI(x.thumb[0]) + ')'); +} + +function loadAllThumbs() +{ + mthumbs.each(loadThumb); + mthumbs = []; +} + +function loadNextThumb() +{ + if(mthumbs.length) + { + var i = mthumbs.shift(); + Asset.image(imgs.data[i].thumb[0], + { + onLoad: function() + { + loadThumb(i); + loadNextThumb(); + } + }); + } +} + +function initGallery(data) +{ + imgs = data; + emain = $('gallery'); + emain.setStyle('display', 'none'); + + eback = new Element('img', { id: 'background' }); + eback.inject(emain); + + enoise = new Element('div', { id: 'noise' }); + enoise.inject(emain); + + econt = new Element('div', { id: 'content' }); + econt.inject(emain); + + ebuff = new Element('div'); + ebuff.inject(econt); + + eflash = new Element('div', { id: 'flash' }); + eflash.setStyles({ 'opacity': 0, 'display': 'none' }); + eflash.set('tween', + { + duration: duration, + link: 'cancel', + onComplete: function() { eflash.setStyle('display', 'none'); } + }); + eflash.inject(econt); + + eleft = new Element('a', { id: 'left' }); + eleft.adopt((new Element('div')).adopt(new Element('img', { 'src': 'left.png' }))); + eleft.inject(econt); + + eright = new Element('a', { id: 'right' }); + eright.adopt((new Element('div')).adopt(new Element('img', { 'src': 'right.png' }))); + eright.inject(econt); + + ehdr = new Element('div', { id: 'header' }); + ehdr.inject(econt); + + elist = new Element('div', { id: 'list' }); + elist.inject(emain); + + imgs.data.each(function(x, i) + { + var ethumb = new Element('div', { 'class': 'thumb' }); + x.ethumb = ethumb; + + var a = new Element('a'); + a.addEvent('click', function() { switchTo(i); }); + a.href = "#" + i; + + var img = new Element('div', { 'class': 'img' }); + x.eimg = img; + img.inject(a); + + var ovr = new Element('div', { 'class': 'ovr' }); + ovr.inject(a); + + a.inject(ethumb); + ethumb.inject(elist); + elist.appendText("\n"); + }); + + emain.setStyles( + { + 'display': 'block', + 'visibility': 'hidden', + 'min-width': imgs.thumb.min[0] + padding * 2, + 'min-height': imgs.thumb.min[1] + padding * 2 + }); + + // events and navigation shortcuts + eleft.addEvent('click', prev); + eright.addEvent('click', next); + window.addEvent('resize', resize); + window.addEvent('hashchange', change); + + window.addEvent('keydown', function(ev) + { + if(ev.key == 'up' || ev.key == 'left') + { + ev.stop(); + prev(); + } + else if(ev.key == 'down' || ev.key == 'right' || ev.key == 'space') + { + ev.stop(); + next(); + } + }); + + econt.addEvent('mousewheel', function(ev) + { + if(ev.alt || ev.control || ev.meta || ev.shift) + return; + + ev.stop(); + if(ev.wheel > 0) + prev(); + else + next(); + }); + + new MooSwipe(econt, + { + onSwipeleft: next, + onSwipedown: next, + onSwiperight: prev, + onSwipeup: prev + }); + + // setup an idle callback for mouse movement only + var idleTmp = new IdleTimer(window, { + timeout: hidedelay, + events: ['mousemove', 'mousedown', 'mousewheel'] + }).start(); + idleTmp.addEvent('idle', hideNav); + idleTmp.addEvent('active', function() { showNav(); showHdr(); }); + + // general idle callback + idle = new IdleTimer(window, { timeout: hidedelay }).start(); + idle.addEvent('idle', hideHdr); + + // prepare first image + first = getLocationIndex(); + resize(); + load(first); + centerThumb(0); + if(imgs.name) document.title = imgs.name; + + // setup thumbnail loading sequence + mthumbs = []; + if(first < 5) + { + // optimize common initial case (viewing from the beginning) + for(var i = 0; i != imgs.data.length; ++i) + mthumbs.push(i); + } + else for(var i = 0; i != imgs.data.length; ++i) + { + // distance from current + var d = (i / 2 >> 0); + var k = first + (i % 2? d + 1: -d); + if(k < 0) + k = imgs.data.length + k; + else if(k >= imgs.data.length) + k = k - imgs.data.length; + mthumbs.push(k); + } + loadNextThumb(); + + emain.setStyle('visibility', 'visible'); +} + +function initFailure() +{ + emain = $('gallery'); + emain.set('html', "

Cannot load gallery data :'(

"); + emain.setStyles( + { + 'background': 'inherit', + 'display': 'block' + }); +} + +function init() +{ + if(!("devicePixelRatio" in window)) + window.devicePixelRatio = 1; + + // read the data + new Request.JSON( + { + url: datafile, + onRequest: function() + { + if(this.xhr.overrideMimeType) + this.xhr.overrideMimeType('application/json'); + }, + isSuccess: function() + { + return (!this.status || (this.status >= 200 && this.status < 300)); + }, + onSuccess: initGallery, + onFailure: initFailure + }).get(); + + // preload some resources + Asset.images(['throbber.gif', + 'left.png', 'right.png', 'delete.png', 'add.png', + 'eye.png', 'download.png', 'back.png', 'up.png', 'down.png', + 'cut-left.png', 'cut-right.png', + 'cut-top.png', 'cut-mov.png']); +} + +window.addEvent('domready', init); diff --git a/swarm/examples/album/left.png b/swarm/examples/album/left.png new file mode 100644 index 0000000000..c4a66f0455 Binary files /dev/null and b/swarm/examples/album/left.png differ diff --git a/swarm/examples/album/mootools-core-1.4.js b/swarm/examples/album/mootools-core-1.4.js new file mode 100644 index 0000000000..569473d1cb --- /dev/null +++ b/swarm/examples/album/mootools-core-1.4.js @@ -0,0 +1,491 @@ +/* +--- +MooTools: the javascript framework + +web build: + - http://mootools.net/core/76bf47062d6c1983d66ce47ad66aa0e0 + +packager build: + - packager build Core/Core Core/Array Core/String Core/Number Core/Function Core/Object Core/Event Core/Browser Core/Class Core/Class.Extras Core/Slick.Parser Core/Slick.Finder Core/Element Core/Element.Style Core/Element.Event Core/Element.Delegation Core/Element.Dimensions Core/Fx Core/Fx.CSS Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request Core/Request.HTML Core/Request.JSON Core/Cookie Core/JSON Core/DOMReady Core/Swiff + +copyrights: + - [MooTools](http://mootools.net) + +licenses: + - [MIT License](http://mootools.net/license.txt) +... +*/ + +(function(){this.MooTools={version:"1.4.5",build:"ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0"};var o=this.typeOf=function(i){if(i==null){return"null";}if(i.$family!=null){return i.$family(); +}if(i.nodeName){if(i.nodeType==1){return"element";}if(i.nodeType==3){return(/\S/).test(i.nodeValue)?"textnode":"whitespace";}}else{if(typeof i.length=="number"){if(i.callee){return"arguments"; +}if("item" in i){return"collection";}}}return typeof i;};var j=this.instanceOf=function(t,i){if(t==null){return false;}var s=t.$constructor||t.constructor; +while(s){if(s===i){return true;}s=s.parent;}if(!t.hasOwnProperty){return false;}return t instanceof i;};var f=this.Function;var p=true;for(var k in {toString:1}){p=null; +}if(p){p=["hasOwnProperty","valueOf","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","constructor"];}f.prototype.overloadSetter=function(s){var i=this; +return function(u,t){if(u==null){return this;}if(s||typeof u!="string"){for(var v in u){i.call(this,v,u[v]);}if(p){for(var w=p.length;w--;){v=p[w];if(u.hasOwnProperty(v)){i.call(this,v,u[v]); +}}}}else{i.call(this,u,t);}return this;};};f.prototype.overloadGetter=function(s){var i=this;return function(u){var v,t;if(typeof u!="string"){v=u;}else{if(arguments.length>1){v=arguments; +}else{if(s){v=[u];}}}if(v){t={};for(var w=0;w>>0; +b>>0;b>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a>>0,b=Array(d);for(var a=0;a>>0; +b-1:String(this).indexOf(a)>-1;},trim:function(){return String(this).replace(/^\s+|\s+$/g,""); +},clean:function(){return String(this).replace(/\s+/g," ").trim();},camelCase:function(){return String(this).replace(/-\D/g,function(a){return a.charAt(1).toUpperCase(); +});},hyphenate:function(){return String(this).replace(/[A-Z]/g,function(a){return("-"+a.charAt(0).toLowerCase());});},capitalize:function(){return String(this).replace(/\b[a-z]/g,function(a){return a.toUpperCase(); +});},escapeRegExp:function(){return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},toInt:function(a){return parseInt(this,a||10);},toFloat:function(){return parseFloat(this); +},hexToRgb:function(b){var a=String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);return(a)?a.slice(1).hexToRgb(b):null;},rgbToHex:function(b){var a=String(this).match(/\d{1,3}/g); +return(a)?a.rgbToHex(b):null;},substitute:function(a,b){return String(this).replace(b||(/\\?\{([^{}]+)\}/g),function(d,c){if(d.charAt(0)=="\\"){return d.slice(1); +}return(a[c]!=null)?a[c]:"";});}});Number.implement({limit:function(b,a){return Math.min(a,Math.max(b,this));},round:function(a){a=Math.pow(10,a||0).toFixed(a<0?-a:0); +return Math.round(this*a)/a;},times:function(b,c){for(var a=0;a1?Array.slice(arguments,1):null,d=function(){};var c=function(){var g=e,h=arguments.length;if(this instanceof c){d.prototype=a.prototype; +g=new d;}var f=(!b&&!h)?a.call(g):a.apply(g,b&&h?b.concat(Array.slice(arguments)):b||arguments);return g==e?f:g;};return c;},pass:function(b,c){var a=this; +if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},delay:function(b,c,a){return setTimeout(this.pass((a==null?[]:a),c),b); +},periodical:function(c,b,a){return setInterval(this.pass((a==null?[]:a),b),c);}});(function(){var a=Object.prototype.hasOwnProperty;Object.extend({subset:function(d,g){var f={}; +for(var e=0,b=g.length;e]*>([\s\S]*?)<\/script>/gi,function(q,r){e+=r+"\n"; +return"";});if(o===true){n.exec(e);}else{if(typeOf(o)=="function"){o(e,p);}}return p;});n.extend({Document:this.Document,Window:this.Window,Element:this.Element,Event:this.Event}); +this.Window=this.$constructor=new Type("Window",function(){});this.$family=Function.from("window").hide();Window.mirror(function(e,o){g[e]=o;});this.Document=j.$constructor=new Type("Document",function(){}); +j.$family=Function.from("document").hide();Document.mirror(function(e,o){j[e]=o;});j.html=j.documentElement;if(!j.head){j.head=j.getElementsByTagName("head")[0]; +}if(j.execCommand){try{j.execCommand("BackgroundImageCache",false,true);}catch(f){}}if(this.attachEvent&&!this.addEventListener){var c=function(){this.detachEvent("onunload",c); +j.head=j.html=j.window=null;};this.attachEvent("onunload",c);}var l=Array.from;try{l(j.html.childNodes);}catch(f){Array.from=function(o){if(typeof o!="string"&&Type.isEnumerable(o)&&typeOf(o)!="array"){var e=o.length,p=new Array(e); +while(e--){p[e]=o[e];}return p;}return l(o);};var k=Array.prototype,m=k.slice;["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice"].each(function(e){var o=k[e]; +Array[e]=function(p){return o.apply(Array.from(p),m.call(arguments,1));};});}})();(function(){var b={};var a=this.DOMEvent=new Type("DOMEvent",function(c,g){if(!g){g=window; +}c=c||g.event;if(c.$extended){return c;}this.event=c;this.$extended=true;this.shift=c.shiftKey;this.control=c.ctrlKey;this.alt=c.altKey;this.meta=c.metaKey; +var i=this.type=c.type;var h=c.target||c.srcElement;while(h&&h.nodeType==3){h=h.parentNode;}this.target=document.id(h);if(i.indexOf("key")==0){var d=this.code=(c.which||c.keyCode); +this.key=b[d];if(i=="keydown"){if(d>111&&d<124){this.key="f"+(d-111);}else{if(d>95&&d<106){this.key=d-96;}}}if(this.key==null){this.key=String.fromCharCode(d).toLowerCase(); +}}else{if(i=="click"||i=="dblclick"||i=="contextmenu"||i=="DOMMouseScroll"||i.indexOf("mouse")==0){var j=g.document;j=(!j.compatMode||j.compatMode=="CSS1Compat")?j.html:j.body; +this.page={x:(c.pageX!=null)?c.pageX:c.clientX+j.scrollLeft,y:(c.pageY!=null)?c.pageY:c.clientY+j.scrollTop};this.client={x:(c.pageX!=null)?c.pageX-g.pageXOffset:c.clientX,y:(c.pageY!=null)?c.pageY-g.pageYOffset:c.clientY}; +if(i=="DOMMouseScroll"||i=="mousewheel"){this.wheel=(c.wheelDelta)?c.wheelDelta/120:-(c.detail||0)/3;}this.rightClick=(c.which==3||c.button==2);if(i=="mouseover"||i=="mouseout"){var k=c.relatedTarget||c[(i=="mouseover"?"from":"to")+"Element"]; +while(k&&k.nodeType==3){k=k.parentNode;}this.relatedTarget=document.id(k);}}else{if(i.indexOf("touch")==0||i.indexOf("gesture")==0){this.rotation=c.rotation; +this.scale=c.scale;this.targetTouches=c.targetTouches;this.changedTouches=c.changedTouches;var f=this.touches=c.touches;if(f&&f[0]){var e=f[0];this.page={x:e.pageX,y:e.pageY}; +this.client={x:e.clientX,y:e.clientY};}}}}if(!this.client){this.client={};}if(!this.page){this.page={};}});a.implement({stop:function(){return this.preventDefault().stopPropagation(); +},stopPropagation:function(){if(this.event.stopPropagation){this.event.stopPropagation();}else{this.event.cancelBubble=true;}return this;},preventDefault:function(){if(this.event.preventDefault){this.event.preventDefault(); +}else{this.event.returnValue=false;}return this;}});a.defineKey=function(d,c){b[d]=c;return this;};a.defineKeys=a.defineKey.overloadSetter(true);a.defineKeys({"38":"up","40":"down","37":"left","39":"right","27":"esc","32":"space","8":"backspace","9":"tab","46":"delete","13":"enter"}); +})();(function(){var a=this.Class=new Type("Class",function(h){if(instanceOf(h,Function)){h={initialize:h};}var g=function(){e(this);if(g.$prototyping){return this; +}this.$caller=null;var i=(this.initialize)?this.initialize.apply(this,arguments):this;this.$caller=this.caller=null;return i;}.extend(this).implement(h); +g.$constructor=a;g.prototype.$constructor=g;g.prototype.parent=c;return g;});var c=function(){if(!this.$caller){throw new Error('The method "parent" cannot be called.'); +}var g=this.$caller.$name,h=this.$caller.$owner.parent,i=(h)?h.prototype[g]:null;if(!i){throw new Error('The method "'+g+'" has no parent.');}return i.apply(this,arguments); +};var e=function(g){for(var h in g){var j=g[h];switch(typeOf(j)){case"object":var i=function(){};i.prototype=j;g[h]=e(new i);break;case"array":g[h]=j.clone(); +break;}}return g;};var b=function(g,h,j){if(j.$origin){j=j.$origin;}var i=function(){if(j.$protected&&this.$caller==null){throw new Error('The method "'+h+'" cannot be called.'); +}var l=this.caller,m=this.$caller;this.caller=m;this.$caller=i;var k=j.apply(this,arguments);this.$caller=m;this.caller=l;return k;}.extend({$owner:g,$origin:j,$name:h}); +return i;};var f=function(h,i,g){if(a.Mutators.hasOwnProperty(h)){i=a.Mutators[h].call(this,i);if(i==null){return this;}}if(typeOf(i)=="function"){if(i.$hidden){return this; +}this.prototype[h]=(g)?i:b(this,h,i);}else{Object.merge(this.prototype,h,i);}return this;};var d=function(g){g.$prototyping=true;var h=new g;delete g.$prototyping; +return h;};a.implement("implement",f.overloadSetter());a.Mutators={Extends:function(g){this.parent=g;this.prototype=d(g);},Implements:function(g){Array.from(g).each(function(j){var h=new j; +for(var i in h){f.call(this,i,h[i],true);}},this);}};})();(function(){this.Chain=new Class({$chain:[],chain:function(){this.$chain.append(Array.flatten(arguments)); +return this;},callChain:function(){return(this.$chain.length)?this.$chain.shift().apply(this,arguments):false;},clearChain:function(){this.$chain.empty(); +return this;}});var a=function(b){return b.replace(/^on([A-Z])/,function(c,d){return d.toLowerCase();});};this.Events=new Class({$events:{},addEvent:function(d,c,b){d=a(d); +this.$events[d]=(this.$events[d]||[]).include(c);if(b){c.internal=true;}return this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this; +},fireEvent:function(e,c,b){e=a(e);var d=this.$events[e];if(!d){return this;}c=Array.from(c);d.each(function(f){if(b){f.delay(b,this,c);}else{f.apply(this,c); +}},this);return this;},removeEvent:function(e,d){e=a(e);var c=this.$events[e];if(c&&!d.internal){var b=c.indexOf(d);if(b!=-1){delete c[b];}}return this; +},removeEvents:function(d){var e;if(typeOf(d)=="object"){for(e in d){this.removeEvent(e,d[e]);}return this;}if(d){d=a(d);}for(e in this.$events){if(d&&d!=e){continue; +}var c=this.$events[e];for(var b=c.length;b--;){if(b in c){this.removeEvent(e,c[b]);}}}return this;}});this.Options=new Class({setOptions:function(){var b=this.options=Object.merge.apply(null,[{},this.options].append(arguments)); +if(this.addEvent){for(var c in b){if(typeOf(b[c])!="function"||!(/^on[A-Z]/).test(c)){continue;}this.addEvent(c,b[c]);delete b[c];}}return this;}});})(); +(function(){var k,n,l,g,a={},c={},m=/\\/g;var e=function(q,p){if(q==null){return null;}if(q.Slick===true){return q;}q=(""+q).replace(/^\s+|\s+$/g,"");g=!!p; +var o=(g)?c:a;if(o[q]){return o[q];}k={Slick:true,expressions:[],raw:q,reverse:function(){return e(this.raw,true);}};n=-1;while(q!=(q=q.replace(j,b))){}k.length=k.expressions.length; +return o[k.raw]=(g)?h(k):k;};var i=function(o){if(o==="!"){return" ";}else{if(o===" "){return"!";}else{if((/^!/).test(o)){return o.replace(/^!/,"");}else{return"!"+o; +}}}};var h=function(u){var r=u.expressions;for(var p=0;p+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(//,"["+f(">+~`!@$%^&={}\\;/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(//g,"(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])")); +function b(x,s,D,z,r,C,q,B,A,y,u,F,G,v,p,w){if(s||n===-1){k.expressions[++n]=[];l=-1;if(s){return"";}}if(D||z||l===-1){D=D||" ";var t=k.expressions[n]; +if(g&&t[l]){t[l].reverseCombinator=i(D);}t[++l]={combinator:D,tag:"*"};}var o=k.expressions[n][l];if(r){o.tag=r.replace(m,"");}else{if(C){o.id=C.replace(m,""); +}else{if(q){q=q.replace(m,"");if(!o.classList){o.classList=[];}if(!o.classes){o.classes=[];}o.classList.push(q);o.classes.push({value:q,regexp:new RegExp("(^|\\s)"+f(q)+"(\\s|$)")}); +}else{if(G){w=w||p;w=w?w.replace(m,""):null;if(!o.pseudos){o.pseudos=[];}o.pseudos.push({key:G.replace(m,""),value:w,type:F.length==1?"class":"element"}); +}else{if(B){B=B.replace(m,"");u=(u||"").replace(m,"");var E,H;switch(A){case"^=":H=new RegExp("^"+f(u));break;case"$=":H=new RegExp(f(u)+"$");break;case"~=":H=new RegExp("(^|\\s)"+f(u)+"(\\s|$)"); +break;case"|=":H=new RegExp("^"+f(u)+"(-|$)");break;case"=":E=function(I){return u==I;};break;case"*=":E=function(I){return I&&I.indexOf(u)>-1;};break; +case"!=":E=function(I){return u!=I;};break;default:E=function(I){return !!I;};}if(u==""&&(/^[*$^]=$/).test(A)){E=function(){return false;};}if(!E){E=function(I){return I&&H.test(I); +};}if(!o.attributes){o.attributes=[];}o.attributes.push({key:B,operator:A,value:u,test:E});}}}}}return"";}var d=(this.Slick||{});d.parse=function(o){return e(o); +};d.escapeRegExp=f;if(!this.Slick){this.Slick=d;}}).apply((typeof exports!="undefined")?exports:this);(function(){var k={},m={},d=Object.prototype.toString; +k.isNativeCode=function(c){return(/\{\s*\[native code\]\s*\}/).test(""+c);};k.isXML=function(c){return(!!c.xmlVersion)||(!!c.xml)||(d.call(c)=="[object XMLDocument]")||(c.nodeType==9&&c.documentElement.nodeName!="HTML"); +};k.setDocument=function(w){var p=w.nodeType;if(p==9){}else{if(p){w=w.ownerDocument;}else{if(w.navigator){w=w.document;}else{return;}}}if(this.document===w){return; +}this.document=w;var A=w.documentElement,o=this.getUIDXML(A),s=m[o],r;if(s){for(r in s){this[r]=s[r];}return;}s=m[o]={};s.root=A;s.isXMLDocument=this.isXML(w); +s.brokenStarGEBTN=s.starSelectsClosedQSA=s.idGetsName=s.brokenMixedCaseQSA=s.brokenGEBCN=s.brokenCheckedQSA=s.brokenEmptyAttributeQSA=s.isHTMLDocument=s.nativeMatchesSelector=false; +var q,u,y,z,t;var x,v="slick_uniqueid";var c=w.createElement("div");var n=w.body||w.getElementsByTagName("body")[0]||A;n.appendChild(c);try{c.innerHTML=''; +s.isHTMLDocument=!!w.getElementById(v);}catch(C){}if(s.isHTMLDocument){c.style.display="none";c.appendChild(w.createComment(""));u=(c.getElementsByTagName("*").length>1); +try{c.innerHTML="foo";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML=''; +s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='';c.getElementsByClassName("b").length; +c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='';y=(c.getElementsByClassName("a").length!=2); +}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/"); +}catch(C){}try{c.innerHTML='';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML=''; +s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0); +}catch(C){}}try{c.innerHTML='
';t=(c.firstChild.getAttribute("action")!="s");}catch(C){}s.nativeMatchesSelector=A.matchesSelector||A.mozMatchesSelector||A.webkitMatchesSelector; +if(s.nativeMatchesSelector){try{s.nativeMatchesSelector.call(A,":slick");s.nativeMatchesSelector=null;}catch(C){}}}try{A.slick_expando=1;delete A.slick_expando; +s.getUID=this.getUIDHTML;}catch(C){s.getUID=this.getUIDXML;}n.removeChild(c);c=x=n=null;s.getAttribute=(s.isHTMLDocument&&t)?function(G,E){var H=this.attributeGetters[E]; +if(H){return H.call(G);}var F=G.getAttributeNode(E);return(F)?F.nodeValue:null;}:function(F,E){var G=this.attributeGetters[E];return(G)?G.call(F):F.getAttribute(E); +};s.hasAttribute=(A&&this.isNativeCode(A.hasAttribute))?function(F,E){return F.hasAttribute(E);}:function(F,E){F=F.getAttributeNode(E);return !!(F&&(F.specified||F.nodeValue)); +};var D=A&&this.isNativeCode(A.contains),B=w&&this.isNativeCode(w.contains);s.contains=(D&&B)?function(E,F){return E.contains(F);}:(D&&!B)?function(E,F){return E===F||((E===w)?w.documentElement:E).contains(F); +}:(A&&A.compareDocumentPosition)?function(E,F){return E===F||!!(E.compareDocumentPosition(F)&16);}:function(E,F){if(F){do{if(F===E){return true;}}while((F=F.parentNode)); +}return false;};s.documentSorter=(A.compareDocumentPosition)?function(F,E){if(!F.compareDocumentPosition||!E.compareDocumentPosition){return 0;}return F.compareDocumentPosition(E)&4?-1:F===E?0:1; +}:("sourceIndex" in A)?function(F,E){if(!F.sourceIndex||!E.sourceIndex){return 0;}return F.sourceIndex-E.sourceIndex;}:(w.createRange)?function(H,F){if(!H.ownerDocument||!F.ownerDocument){return 0; +}var G=H.ownerDocument.createRange(),E=F.ownerDocument.createRange();G.setStart(H,0);G.setEnd(H,0);E.setStart(F,0);E.setEnd(F,0);return G.compareBoundaryPoints(Range.START_TO_END,E); +}:null;A=null;for(r in s){this[r]=s[r];}};var f=/^([#.]?)((?:[\w-]+|\*))$/,h=/\[.+[*$^]=(?:""|'')?\]/,g={};k.search=function(U,z,H,s){var p=this.found=(s)?null:(H||[]); +if(!U){return p;}else{if(U.navigator){U=U.document;}else{if(!U.nodeType){return p;}}}var F,O,V=this.uniques={},I=!!(H&&H.length),y=(U.nodeType==9);if(this.document!==(y?U:U.ownerDocument)){this.setDocument(U); +}if(I){for(O=p.length;O--;){V[this.getUID(p[O])]=true;}}if(typeof z=="string"){var r=z.match(f);simpleSelectors:if(r){var u=r[1],v=r[2],A,E;if(!u){if(v=="*"&&this.brokenStarGEBTN){break simpleSelectors; +}E=U.getElementsByTagName(v);if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{if(u=="#"){if(!this.isHTMLDocument||!y){break simpleSelectors; +}A=U.getElementById(v);if(!A){return p;}if(this.idGetsName&&A.getAttributeNode("id").nodeValue!=v){break simpleSelectors;}if(s){return A||null;}if(!(I&&V[this.getUID(A)])){p.push(A); +}}else{if(u=="."){if(!this.isHTMLDocument||((!U.getElementsByClassName||this.brokenGEBCN)&&U.querySelectorAll)){break simpleSelectors;}if(U.getElementsByClassName&&!this.brokenGEBCN){E=U.getElementsByClassName(v); +if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{var T=new RegExp("(^|\\s)"+e.escapeRegExp(v)+"(\\s|$)");E=U.getElementsByTagName("*"); +for(O=0;A=E[O++];){className=A.className;if(!(className&&T.test(className))){continue;}if(s){return A;}if(!(I&&V[this.getUID(A)])){p.push(A);}}}}}}if(I){this.sort(p); +}return(s)?null:p;}querySelector:if(U.querySelectorAll){if(!this.isHTMLDocument||g[z]||this.brokenMixedCaseQSA||(this.brokenCheckedQSA&&z.indexOf(":checked")>-1)||(this.brokenEmptyAttributeQSA&&h.test(z))||(!y&&z.indexOf(",")>-1)||e.disableQSA){break querySelector; +}var S=z,x=U;if(!y){var C=x.getAttribute("id"),t="slickid__";x.setAttribute("id",t);S="#"+t+" "+S;U=x.parentNode;}try{if(s){return U.querySelector(S)||null; +}else{E=U.querySelectorAll(S);}}catch(Q){g[z]=1;break querySelector;}finally{if(!y){if(C){x.setAttribute("id",C);}else{x.removeAttribute("id");}U=x;}}if(this.starSelectsClosedQSA){for(O=0; +A=E[O++];){if(A.nodeName>"@"&&!(I&&V[this.getUID(A)])){p.push(A);}}}else{for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}if(I){this.sort(p); +}return p;}F=this.Slick.parse(z);if(!F.length){return p;}}else{if(z==null){return p;}else{if(z.Slick){F=z;}else{if(this.contains(U.documentElement||U,z)){(p)?p.push(z):p=z; +return p;}else{return p;}}}}this.posNTH={};this.posNTHLast={};this.posNTHType={};this.posNTHTypeLast={};this.push=(!I&&(s||(F.length==1&&F.expressions[0].length==1)))?this.pushArray:this.pushUID; +if(p==null){p=[];}var M,L,K;var B,J,D,c,q,G,W;var N,P,o,w,R=F.expressions;search:for(O=0;(P=R[O]);O++){for(M=0;(o=P[M]);M++){B="combinator:"+o.combinator; +if(!this[B]){continue search;}J=(this.isXMLDocument)?o.tag:o.tag.toUpperCase();D=o.id;c=o.classList;q=o.classes;G=o.attributes;W=o.pseudos;w=(M===(P.length-1)); +this.bitUniques={};if(w){this.uniques=V;this.found=p;}else{this.uniques={};this.found=[];}if(M===0){this[B](U,J,D,q,G,W,c);if(s&&w&&p.length){break search; +}}else{if(s&&w){for(L=0,K=N.length;L1)){this.sort(p);}return(s)?(p[0]||null):p;};k.uidx=1;k.uidk="slick-uniqueid";k.getUIDXML=function(n){var c=n.getAttribute(this.uidk); +if(!c){c=this.uidx++;n.setAttribute(this.uidk,c);}return c;};k.getUIDHTML=function(c){return c.uniqueNumber||(c.uniqueNumber=this.uidx++);};k.sort=function(c){if(!this.documentSorter){return c; +}c.sort(this.documentSorter);return c;};k.cacheNTH={};k.matchNTH=/^([+-]?\d*)?([a-z]+)?([+-]\d+)?$/;k.parseNTHArgument=function(q){var o=q.match(this.matchNTH); +if(!o){return false;}var p=o[2]||false;var n=o[1]||1;if(n=="-"){n=-1;}var c=+o[3]||0;o=(p=="n")?{a:n,b:c}:(p=="odd")?{a:2,b:1}:(p=="even")?{a:2,b:0}:{a:0,b:n}; +return(this.cacheNTH[q]=o);};k.createNTHPseudo=function(p,n,c,o){return function(s,q){var u=this.getUID(s);if(!this[c][u]){var A=s.parentNode;if(!A){return false; +}var r=A[p],t=1;if(o){var z=s.nodeName;do{if(r.nodeName!=z){continue;}this[c][this.getUID(r)]=t++;}while((r=r[n]));}else{do{if(r.nodeType!=1){continue; +}this[c][this.getUID(r)]=t++;}while((r=r[n]));}}q=q||"n";var v=this.cacheNTH[q]||this.parseNTHArgument(q);if(!v){return false;}var y=v.a,x=v.b,w=this[c][u]; +if(y==0){return x==w;}if(y>0){if(w":function(p,c,r,o,n,q){if((p=p.firstChild)){do{if(p.nodeType==1){this.push(p,c,r,o,n,q); +}}while((p=p.nextSibling));}},"+":function(p,c,r,o,n,q){while((p=p.nextSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);break;}}},"^":function(p,c,r,o,n,q){p=p.firstChild; +if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:+"](p,c,r,o,n,q);}}},"~":function(q,c,s,p,n,r){while((q=q.nextSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}},"++":function(p,c,r,o,n,q){this["combinator:+"](p,c,r,o,n,q); +this["combinator:!+"](p,c,r,o,n,q);},"~~":function(p,c,r,o,n,q){this["combinator:~"](p,c,r,o,n,q);this["combinator:!~"](p,c,r,o,n,q);},"!":function(p,c,r,o,n,q){while((p=p.parentNode)){if(p!==this.document){this.push(p,c,r,o,n,q); +}}},"!>":function(p,c,r,o,n,q){p=p.parentNode;if(p!==this.document){this.push(p,c,r,o,n,q);}},"!+":function(p,c,r,o,n,q){while((p=p.previousSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q); +break;}}},"!^":function(p,c,r,o,n,q){p=p.lastChild;if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:!+"](p,c,r,o,n,q);}}},"!~":function(q,c,s,p,n,r){while((q=q.previousSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}}};for(var i in j){k["combinator:"+i]=j[i];}var l={empty:function(c){var n=c.firstChild; +return !(n&&n.nodeType==1)&&!(c.innerText||c.textContent||"").length;},not:function(c,n){return !this.matchNode(c,n);},contains:function(c,n){return(c.innerText||c.textContent||"").indexOf(n)>-1; +},"first-child":function(c){while((c=c.previousSibling)){if(c.nodeType==1){return false;}}return true;},"last-child":function(c){while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"only-child":function(o){var n=o;while((n=n.previousSibling)){if(n.nodeType==1){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"nth-child":k.createNTHPseudo("firstChild","nextSibling","posNTH"),"nth-last-child":k.createNTHPseudo("lastChild","previousSibling","posNTHLast"),"nth-of-type":k.createNTHPseudo("firstChild","nextSibling","posNTHType",true),"nth-last-of-type":k.createNTHPseudo("lastChild","previousSibling","posNTHTypeLast",true),index:function(n,c){return this["pseudo:nth-child"](n,""+(c+1)); +},even:function(c){return this["pseudo:nth-child"](c,"2n");},odd:function(c){return this["pseudo:nth-child"](c,"2n+1");},"first-of-type":function(c){var n=c.nodeName; +while((c=c.previousSibling)){if(c.nodeName==n){return false;}}return true;},"last-of-type":function(c){var n=c.nodeName;while((c=c.nextSibling)){if(c.nodeName==n){return false; +}}return true;},"only-of-type":function(o){var n=o,p=o.nodeName;while((n=n.previousSibling)){if(n.nodeName==p){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeName==p){return false; +}}return true;},enabled:function(c){return !c.disabled;},disabled:function(c){return c.disabled;},checked:function(c){return c.checked||c.selected;},focus:function(c){return this.isHTMLDocument&&this.document.activeElement===c&&(c.href||c.type||this.hasAttribute(c,"tabindex")); +},root:function(c){return(c===this.root);},selected:function(c){return c.selected;}};for(var b in l){k["pseudo:"+b]=l[b];}var a=k.attributeGetters={"for":function(){return("htmlFor" in this)?this.htmlFor:this.getAttribute("for"); +},href:function(){return("href" in this)?this.getAttribute("href",2):this.getAttribute("href");},style:function(){return(this.style)?this.style.cssText:this.getAttribute("style"); +},tabindex:function(){var c=this.getAttributeNode("tabindex");return(c&&c.specified)?c.nodeValue:null;},type:function(){return this.getAttribute("type"); +},maxlength:function(){var c=this.getAttributeNode("maxLength");return(c&&c.specified)?c.nodeValue:null;}};a.MAXLENGTH=a.maxLength=a.maxlength;var e=k.Slick=(this.Slick||{}); +e.version="1.1.7";e.search=function(n,o,c){return k.search(n,o,c);};e.find=function(c,n){return k.search(c,n,null,true);};e.contains=function(c,n){k.setDocument(c); +return k.contains(c,n);};e.getAttribute=function(n,c){k.setDocument(n);return k.getAttribute(n,c);};e.hasAttribute=function(n,c){k.setDocument(n);return k.hasAttribute(n,c); +};e.match=function(n,c){if(!(n&&c)){return false;}if(!c||c===n){return true;}k.setDocument(n);return k.matchNode(n,c);};e.defineAttributeGetter=function(c,n){k.attributeGetters[c]=n; +return this;};e.lookupAttributeGetter=function(c){return k.attributeGetters[c];};e.definePseudo=function(c,n){k["pseudo:"+c]=function(p,o){return n.call(p,o); +};return this;};e.lookupPseudo=function(c){var n=k["pseudo:"+c];if(n){return function(o){return n.call(this,o);};}return null;};e.override=function(n,c){k.override(n,c); +return this;};e.isXML=k.isXML;e.uidOf=function(c){return k.getUIDHTML(c);};if(!this.Slick){this.Slick=e;}}).apply((typeof exports!="undefined")?exports:this); +var Element=function(b,g){var h=Element.Constructors[b];if(h){return h(g);}if(typeof b!="string"){return document.id(b).set(g);}if(!g){g={};}if(!(/^[\w-]+$/).test(b)){var e=Slick.parse(b).expressions[0][0]; +b=(e.tag=="*")?"div":e.tag;if(e.id&&g.id==null){g.id=e.id;}var d=e.attributes;if(d){for(var a,f=0,c=d.length;f=this.length){delete this[g--]; +}return e;}.protect());}Array.forEachMethod(function(g,e){Elements.implement(e,g);});Array.mirror(Elements);var d;try{d=(document.createElement("").name=="x"); +}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked; +}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g); +}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this; +},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null; +},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G); +};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l; +};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document); +});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements); +},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m); +}if(!document.createElement("div").contains){Element.implement(m);}var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions; +for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e)); +});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e)); +});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast()); +},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1"))); +},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){if(arguments.length==1){if(typeof e=="string"){return Slick.search(this.document,e,new Elements); +}else{if(Type.isEnumerable(e)){return new Elements(e);}}}return new Elements(arguments);});}var w={before:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e); +}},after:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e.nextSibling);}},bottom:function(l,e){e.appendChild(l);},top:function(l,e){e.insertBefore(l,e.firstChild); +}};w.inside=w.bottom;var j={},d={};var k={};Array.forEach(["type","value","defaultValue","accessKey","cellPadding","cellSpacing","colSpan","frameBorder","rowSpan","tabIndex","useMap"],function(e){k[e.toLowerCase()]=e; +});k.html="innerHTML";k.text=(document.createElement("div").textContent==null)?"innerText":"textContent";Object.forEach(k,function(l,e){d[e]=function(D,E){D[l]=E; +};j[e]=function(D){return D[l];};});var x=["compact","nowrap","ismap","declare","noshade","checked","disabled","readOnly","multiple","selected","noresize","defer","defaultChecked","autofocus","controls","autoplay","loop"]; +var h={};Array.forEach(x,function(e){var l=e.toLowerCase();h[l]=e;d[l]=function(D,E){D[e]=!!E;};j[l]=function(D){return !!D[e];};});Object.append(d,{"class":function(e,l){("className" in e)?e.className=(l||""):e.setAttribute("class",l); +},"for":function(e,l){("htmlFor" in e)?e.htmlFor=l:e.setAttribute("for",l);},style:function(e,l){(e.style)?e.style.cssText=l:e.setAttribute("style",l); +},value:function(e,l){e.value=(l!=null)?l:"";}});j["class"]=function(e){return("className" in e)?e.className||null:e.getAttribute("class");};var f=document.createElement("button"); +try{f.type="button";}catch(z){}if(f.type!="button"){d.type=function(e,l){e.setAttribute("type",l);};}f=null;var p=document.createElement("input");p.value="t"; +p.type="submit";if(p.value!="t"){d.type=function(l,e){var D=l.value;l.type=e;l.value=D;};}p=null;var q=(function(e){e.random="attribute";return(e.getAttribute("random")=="attribute"); +})(document.createElement("div"));Element.implement({setProperty:function(l,D){var E=d[l.toLowerCase()];if(E){E(this,D);}else{if(q){var e=this.retrieve("$attributeWhiteList",{}); +}if(D==null){this.removeAttribute(l);if(q){delete e[l];}}else{this.setAttribute(l,""+D);if(q){e[l]=true;}}}return this;},setProperties:function(e){for(var l in e){this.setProperty(l,e[l]); +}return this;},getProperty:function(F){var D=j[F.toLowerCase()];if(D){return D(this);}if(q){var l=this.getAttributeNode(F),E=this.retrieve("$attributeWhiteList",{}); +if(!l){return null;}if(l.expando&&!E[F]){var G=this.outerHTML;if(G.substr(0,G.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F); +return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e); +},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D]; +(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l); +}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," "); +},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1"); +return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length; +if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D"; +var a=(t.childNodes.length==1);if(!a){var s="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),b=document.createDocumentFragment(),u=s.length; +while(u--){b.createElement(s[u]);}}t=null;var g=Function.attempt(function(){var e=document.createElement("table");e.innerHTML="";return true; +});var c=document.createElement("tr"),o="";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"","
"],select:[1,""],tbody:[2,"","
"],tr:[3,"","
"]}; +e.thead=e.tfoot=e.tbody;return function(D){var E=e[this.get("tag")];if(!E&&!a){E=[0,"",""];}if(!E){return l.call(this,D);}var H=E[0],G=document.createElement("div"),F=G; +if(!a){b.appendChild(G);}G.innerHTML=[E[1],D,E[2]].flatten().join("");while(H--){F=F.firstChild;}this.empty().adopt(F.childNodes);if(!a){b.removeChild(G); +}G=null;};})(Element.Properties.html.set);}var n=document.createElement("form");n.innerHTML="";if(n.firstChild.value!="s"){Element.Properties.value={set:function(G){var l=this.get("tag"); +if(l!="select"){return this.setProperty("value",G);}var D=this.getElements("option");for(var E=0;E0||k==null?"visible":"hidden";};var f=(h?function(l,k){l.style.opacity=k;}:(e?function(l,k){var n=l.style; +if(!l.currentStyle||!l.currentStyle.hasLayout){n.zoom=1;}if(k==null||k==1){k="";}else{k="alpha(opacity="+(k*100).limit(0,100).round()+")";}var m=n.filter||l.getComputedStyle("filter")||""; +n.filter=j.test(m)?m.replace(j,k):m+k;if(!n.filter){n.removeAttribute("filter");}}:a));var g=(h?function(l){var k=l.style.opacity||l.getComputedStyle("opacity"); +return(k=="")?1:k.toFloat();}:(e?function(l){var m=(l.style.filter||l.getComputedStyle("filter")),k;if(m){k=m.match(j);}return(k==null||m==null)?1:(k[1]/100); +}:function(l){var k=l.retrieve("$opacity");if(k==null){k=(l.style.visibility=="hidden"?0:1);}return k;}));var b=(i.style.cssFloat==null)?"styleFloat":"cssFloat"; +Element.implement({getComputedStyle:function(m){if(this.currentStyle){return this.currentStyle[m.camelCase()];}var l=Element.getDocument(this).defaultView,k=l?l.getComputedStyle(this,null):null; +return(k)?k.getPropertyValue((m==b)?"float":m.hyphenate()):null;},setStyle:function(l,k){if(l=="opacity"){if(k!=null){k=parseFloat(k);}f(this,k);return this; +}l=(l=="float"?b:l).camelCase();if(typeOf(k)!="string"){var m=(Element.Styles[l]||"@").split(" ");k=Array.from(k).map(function(o,n){if(!m[n]){return""; +}return(typeOf(o)=="number")?m[n].replace("@",Math.round(o)):o;}).join(" ");}else{if(k==String(Number(k))){k=Math.round(k);}}this.style[l]=k;if((k==""||k==null)&&c&&this.style.removeAttribute){this.style.removeAttribute(l); +}return this;},getStyle:function(q){if(q=="opacity"){return g(this);}q=(q=="float"?b:q).camelCase();var k=this.style[q];if(!k||q=="zIndex"){k=[];for(var p in Element.ShortStyles){if(q!=p){continue; +}for(var o in Element.ShortStyles[p]){k.push(this.getStyle(o));}return k.join(" ");}k=this.getComputedStyle(q);}if(k){k=String(k);var m=k.match(/rgba?\([\d\s,]+\)/); +if(m){k=k.replace(m[0],m[0].rgbToHex());}}if(Browser.opera||Browser.ie){if((/^(height|width)$/).test(q)&&!(/px$/.test(k))){var l=(q=="width")?["left","right"]:["top","bottom"],n=0; +l.each(function(r){n+=this.getStyle("border-"+r+"-width").toInt()+this.getStyle("padding-"+r).toInt();},this);return this["offset"+q.capitalize()]-n+"px"; +}if(Browser.ie&&(/^border(.+)Width|margin|padding/).test(q)&&isNaN(parseFloat(k))){return"0px";}}return k;},setStyles:function(l){for(var k in l){this.setStyle(k,l[k]); +}return this;},getStyles:function(){var k={};Array.flatten(arguments).each(function(l){k[l]=this.getStyle(l);},this);return k;}});Element.Styles={left:"@px",top:"@px",bottom:"@px",right:"@px",width:"@px",height:"@px",maxWidth:"@px",maxHeight:"@px",minWidth:"@px",minHeight:"@px",backgroundColor:"rgb(@, @, @)",backgroundPosition:"@px @px",color:"rgb(@, @, @)",fontSize:"@px",letterSpacing:"@px",lineHeight:"@px",clip:"rect(@px @px @px @px)",margin:"@px @px @px @px",padding:"@px @px @px @px",border:"@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)",borderWidth:"@px @px @px @px",borderStyle:"@ @ @ @",borderColor:"rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)",zIndex:"@",zoom:"@",fontWeight:"@",textIndent:"@px",opacity:"@"}; +Element.ShortStyles={margin:{},padding:{},border:{},borderWidth:{},borderStyle:{},borderColor:{}};["Top","Right","Bottom","Left"].each(function(q){var p=Element.ShortStyles; +var l=Element.Styles;["margin","padding"].each(function(r){var s=r+q;p[r][s]=l[s]="@px";});var o="border"+q;p.border[o]=l[o]="@px @ rgb(@, @, @)";var n=o+"Width",k=o+"Style",m=o+"Color"; +p[o]={};p.borderWidth[n]=p[o][n]=l[n]="@px";p.borderStyle[k]=p[o][k]=l[k]="@";p.borderColor[m]=p[o][m]=l[m]="rgb(@, @, @)";});})();(function(){Element.Properties.events={set:function(b){this.addEvents(b); +}};[Element,Window,Document].invoke("implement",{addEvent:function(f,h){var i=this.retrieve("events",{});if(!i[f]){i[f]={keys:[],values:[]};}if(i[f].keys.contains(h)){return this; +}i[f].keys.push(h);var g=f,b=Element.Events[f],d=h,j=this;if(b){if(b.onAdd){b.onAdd.call(this,h,f);}if(b.condition){d=function(k){if(b.condition.call(this,k,f)){return h.call(this,k); +}return true;};}if(b.base){g=Function.from(b.base).call(this,f);}}var e=function(){return h.call(j);};var c=Element.NativeEvents[g];if(c){if(c==2){e=function(k){k=new DOMEvent(k,j.getWindow()); +if(d.call(j,k)===false){k.stop();}};}this.addListener(g,e,arguments[2]);}i[f].values.push(e);return this;},removeEvent:function(e,d){var c=this.retrieve("events"); +if(!c||!c[e]){return this;}var h=c[e];var b=h.keys.indexOf(d);if(b==-1){return this;}var g=h.values[b];delete h.keys[b];delete h.values[b];var f=Element.Events[e]; +if(f){if(f.onRemove){f.onRemove.call(this,d,e);}if(f.base){e=Function.from(f.base).call(this,e);}}return(Element.NativeEvents[e])?this.removeListener(e,g,arguments[2]):this; +},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this;},removeEvents:function(b){var d;if(typeOf(b)=="object"){for(d in b){this.removeEvent(d,b[d]); +}return this;}var c=this.retrieve("events");if(!c){return this;}if(!b){for(d in c){this.removeEvents(d);}this.eliminate("events");}else{if(c[b]){c[b].keys.each(function(e){this.removeEvent(b,e); +},this);delete c[b];}}return this;},fireEvent:function(e,c,b){var d=this.retrieve("events");if(!d||!d[e]){return this;}c=Array.from(c);d[e].keys.each(function(f){if(b){f.delay(b,this,c); +}else{f.apply(this,c);}},this);return this;},cloneEvents:function(e,d){e=document.id(e);var c=e.retrieve("events");if(!c){return this;}if(!d){for(var b in c){this.cloneEvents(e,b); +}}else{if(c[d]){c[d].keys.each(function(f){this.addEvent(d,f);},this);}}return this;}});Element.NativeEvents={click:2,dblclick:2,mouseup:2,mousedown:2,contextmenu:2,mousewheel:2,DOMMouseScroll:2,mouseover:2,mouseout:2,mousemove:2,selectstart:2,selectend:2,keydown:2,keypress:2,keyup:2,orientationchange:2,touchstart:2,touchmove:2,touchend:2,touchcancel:2,gesturestart:2,gesturechange:2,gestureend:2,focus:2,blur:2,change:2,reset:2,select:2,submit:2,paste:2,input:2,load:2,unload:1,beforeunload:2,resize:1,move:1,DOMContentLoaded:1,readystatechange:1,error:1,abort:1,scroll:1}; +Element.Events={mousewheel:{base:(Browser.firefox)?"DOMMouseScroll":"mousewheel"}};if("onmouseenter" in document.documentElement){Element.NativeEvents.mouseenter=Element.NativeEvents.mouseleave=2; +}else{var a=function(b){var c=b.relatedTarget;if(c==null){return true;}if(!c){return false;}return(c!=this&&c.prefix!="xul"&&typeOf(this)!="document"&&!this.contains(c)); +};Element.Events.mouseenter={base:"mouseover",condition:a};Element.Events.mouseleave={base:"mouseout",condition:a};}if(!window.addEventListener){Element.NativeEvents.propertychange=2; +Element.Events.change={base:function(){var b=this.type;return(this.get("tag")=="input"&&(b=="radio"||b=="checkbox"))?"propertychange":"change";},condition:function(b){return this.type!="radio"||(b.event.propertyName=="checked"&&this.checked); +}};}})();(function(){var c=!!window.addEventListener;Element.NativeEvents.focusin=Element.NativeEvents.focusout=2;var k=function(l,m,n,o,p){while(p&&p!=l){if(m(p,o)){return n.call(p,o,p); +}p=document.id(p.parentNode);}};var a={mouseenter:{base:"mouseover"},mouseleave:{base:"mouseout"},focus:{base:"focus"+(c?"":"in"),capture:true},blur:{base:c?"blur":"focusout",capture:true}}; +var b="$delegation:";var i=function(l){return{base:"focusin",remove:function(m,o){var p=m.retrieve(b+l+"listeners",{})[o];if(p&&p.forms){for(var n=p.forms.length; +n--;){p.forms[n].removeEvent(l,p.fns[n]);}}},listen:function(x,r,v,n,t,s){var o=(t.get("tag")=="form")?t:n.target.getParent("form");if(!o){return;}var u=x.retrieve(b+l+"listeners",{}),p=u[s]||{forms:[],fns:[]},m=p.forms,w=p.fns; +if(m.indexOf(o)!=-1){return;}m.push(o);var q=function(y){k(x,r,v,y,t);};o.addEvent(l,q);w.push(q);u[s]=p;x.store(b+l+"listeners",u);}};};var d=function(l){return{base:"focusin",listen:function(m,n,p,q,r){var o={blur:function(){this.removeEvents(o); +}};o[l]=function(s){k(m,n,p,s,r);};q.target.addEvents(o);}};};if(!c){Object.append(a,{submit:i("submit"),reset:i("reset"),change:d("change"),select:d("select")}); +}var h=Element.prototype,f=h.addEvent,j=h.removeEvent;var e=function(l,m){return function(r,q,n){if(r.indexOf(":relay")==-1){return l.call(this,r,q,n); +}var o=Slick.parse(r).expressions[0][0];if(o.pseudos[0].key!="relay"){return l.call(this,r,q,n);}var p=o.tag;o.pseudos.slice(1).each(function(s){p+=":"+s.key+(s.value?"("+s.value+")":""); +});l.call(this,r,q);return m.call(this,p,o.pseudos[0].value,q);};};var g={addEvent:function(v,q,x){var t=this.retrieve("$delegates",{}),r=t[v];if(r){for(var y in r){if(r[y].fn==x&&r[y].match==q){return this; +}}}var p=v,u=q,o=x,n=a[v]||{};v=n.base||p;q=function(B){return Slick.match(B,u);};var w=Element.Events[p];if(w&&w.condition){var l=q,m=w.condition;q=function(C,B){return l(C,B)&&m.call(C,B,v); +};}var z=this,s=String.uniqueID();var A=n.listen?function(B,C){if(!C&&B&&B.target){C=B.target;}if(C){n.listen(z,q,x,B,C,s);}}:function(B,C){if(!C&&B&&B.target){C=B.target; +}if(C){k(z,q,x,B,C);}};if(!r){r={};}r[s]={match:u,fn:o,delegator:A};t[p]=r;return f.call(this,v,A,n.capture);},removeEvent:function(r,n,t,u){var q=this.retrieve("$delegates",{}),p=q[r]; +if(!p){return this;}if(u){var m=r,w=p[u].delegator,l=a[r]||{};r=l.base||m;if(l.remove){l.remove(this,u);}delete p[u];q[m]=p;return j.call(this,r,w);}var o,v; +if(t){for(o in p){v=p[o];if(v.match==n&&v.fn==t){return g.removeEvent.call(this,r,n,t,o);}}}else{for(o in p){v=p[o];if(v.match==n){g.removeEvent.call(this,r,n,v.fn,o); +}}}return this;}};[Element,Window,Document].invoke("implement",{addEvent:e(f,g.addEvent),removeEvent:e(j,g.removeEvent)});})();(function(){var h=document.createElement("div"),e=document.createElement("div"); +h.style.height="0";h.appendChild(e);var d=(e.offsetParent===h);h=e=null;var l=function(m){return k(m,"position")!="static"||a(m);};var i=function(m){return l(m)||(/^(?:table|td|th)$/i).test(m.tagName); +};Element.implement({scrollTo:function(m,n){if(a(this)){this.getWindow().scrollTo(m,n);}else{this.scrollLeft=m;this.scrollTop=n;}return this;},getSize:function(){if(a(this)){return this.getWindow().getSize(); +}return{x:this.offsetWidth,y:this.offsetHeight};},getScrollSize:function(){if(a(this)){return this.getWindow().getScrollSize();}return{x:this.scrollWidth,y:this.scrollHeight}; +},getScroll:function(){if(a(this)){return this.getWindow().getScroll();}return{x:this.scrollLeft,y:this.scrollTop};},getScrolls:function(){var n=this.parentNode,m={x:0,y:0}; +while(n&&!a(n)){m.x+=n.scrollLeft;m.y+=n.scrollTop;n=n.parentNode;}return m;},getOffsetParent:d?function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}var n=(k(m,"position")=="static")?i:l;while((m=m.parentNode)){if(n(m)){return m;}}return null;}:function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}try{return m.offsetParent;}catch(n){}return null;},getOffsets:function(){if(this.getBoundingClientRect&&!Browser.Platform.ios){var r=this.getBoundingClientRect(),o=document.id(this.getDocument().documentElement),q=o.getScroll(),t=this.getScrolls(),s=(k(this,"position")=="fixed"); +return{x:r.left.toInt()+t.x+((s)?0:q.x)-o.clientLeft,y:r.top.toInt()+t.y+((s)?0:q.y)-o.clientTop};}var n=this,m={x:0,y:0};if(a(this)){return m;}while(n&&!a(n)){m.x+=n.offsetLeft; +m.y+=n.offsetTop;if(Browser.firefox){if(!c(n)){m.x+=b(n);m.y+=g(n);}var p=n.parentNode;if(p&&k(p,"overflow")!="visible"){m.x+=b(p);m.y+=g(p);}}else{if(n!=this&&Browser.safari){m.x+=b(n); +m.y+=g(n);}}n=n.offsetParent;}if(Browser.firefox&&!c(this)){m.x-=b(this);m.y-=g(this);}return m;},getPosition:function(p){var q=this.getOffsets(),n=this.getScrolls(); +var m={x:q.x-n.x,y:q.y-n.y};if(p&&(p=document.id(p))){var o=p.getPosition();return{x:m.x-o.x-b(p),y:m.y-o.y-g(p)};}return m;},getCoordinates:function(o){if(a(this)){return this.getWindow().getCoordinates(); +}var m=this.getPosition(o),n=this.getSize();var p={left:m.x,top:m.y,width:n.x,height:n.y};p.right=p.left+p.width;p.bottom=p.top+p.height;return p;},computePosition:function(m){return{left:m.x-j(this,"margin-left"),top:m.y-j(this,"margin-top")}; +},setPosition:function(m){return this.setStyles(this.computePosition(m));}});[Document,Window].invoke("implement",{getSize:function(){var m=f(this);return{x:m.clientWidth,y:m.clientHeight}; +},getScroll:function(){var n=this.getWindow(),m=f(this);return{x:n.pageXOffset||m.scrollLeft,y:n.pageYOffset||m.scrollTop};},getScrollSize:function(){var o=f(this),n=this.getSize(),m=this.getDocument().body; +return{x:Math.max(o.scrollWidth,m.scrollWidth,n.x),y:Math.max(o.scrollHeight,m.scrollHeight,n.y)};},getPosition:function(){return{x:0,y:0};},getCoordinates:function(){var m=this.getSize(); +return{top:0,left:0,bottom:m.y,right:m.x,height:m.y,width:m.x};}});var k=Element.getComputedStyle;function j(m,n){return k(m,n).toInt()||0;}function c(m){return k(m,"-moz-box-sizing")=="border-box"; +}function g(m){return j(m,"border-top-width");}function b(m){return j(m,"border-left-width");}function a(m){return(/^(?:body|html)$/i).test(m.tagName); +}function f(m){var n=m.getDocument();return(!n.compatMode||n.compatMode=="CSS1Compat")?n.html:n.body;}})();Element.alias({position:"setPosition"});[Window,Document,Element].invoke("implement",{getHeight:function(){return this.getSize().y; +},getWidth:function(){return this.getSize().x;},getScrollTop:function(){return this.getScroll().y;},getScrollLeft:function(){return this.getScroll().x; +},getScrollHeight:function(){return this.getScrollSize().y;},getScrollWidth:function(){return this.getScrollSize().x;},getTop:function(){return this.getPosition().y; +},getLeft:function(){return this.getPosition().x;}});(function(){var f=this.Fx=new Class({Implements:[Chain,Events,Options],options:{fps:60,unit:false,duration:500,frames:null,frameSkip:true,link:"ignore"},initialize:function(g){this.subject=this.subject||this; +this.setOptions(g);},getTransition:function(){return function(g){return -(Math.cos(Math.PI*g)-1)/2;};},step:function(g){if(this.options.frameSkip){var h=(this.time!=null)?(g-this.time):0,i=h/this.frameInterval; +this.time=g;this.frame+=i;}else{this.frame++;}if(this.frame=(7-4*d)/11){e=c*c-Math.pow((11-6*d-11*f)/4,2);break;}}return e; +},Elastic:function(b,a){return Math.pow(2,10*--b)*Math.cos(20*b*Math.PI*(a&&a[0]||1)/3);}});["Quad","Cubic","Quart","Quint"].each(function(b,a){Fx.Transitions[b]=new Fx.Transition(function(c){return Math.pow(c,a+2); +});});(function(){var d=function(){},a=("onprogress" in new Browser.Request);var c=this.Request=new Class({Implements:[Chain,Events,Options],options:{url:"",data:"",headers:{"X-Requested-With":"XMLHttpRequest",Accept:"text/javascript, text/html, application/xml, text/xml, */*"},async:true,format:false,method:"post",link:"ignore",isSuccess:null,emulation:true,urlEncoded:true,encoding:"utf-8",evalScripts:false,evalResponse:false,timeout:0,noCache:false},initialize:function(e){this.xhr=new Browser.Request(); +this.setOptions(e);this.headers=this.options.headers;},onStateChange:function(){var e=this.xhr;if(e.readyState!=4||!this.running){return;}this.running=false; +this.status=0;Function.attempt(function(){var f=e.status;this.status=(f==1223)?204:f;}.bind(this));e.onreadystatechange=d;if(a){e.onprogress=e.onloadstart=d; +}clearTimeout(this.timer);this.response={text:this.xhr.responseText||"",xml:this.xhr.responseXML};if(this.options.isSuccess.call(this,this.status)){this.success(this.response.text,this.response.xml); +}else{this.failure();}},isSuccess:function(){var e=this.status;return(e>=200&&e<300);},isRunning:function(){return !!this.running;},processScripts:function(e){if(this.options.evalResponse||(/(ecma|java)script/).test(this.getHeader("Content-type"))){return Browser.exec(e); +}return e.stripScripts(this.options.evalScripts);},success:function(f,e){this.onSuccess(this.processScripts(f),e);},onSuccess:function(){this.fireEvent("complete",arguments).fireEvent("success",arguments).callChain(); +},failure:function(){this.onFailure();},onFailure:function(){this.fireEvent("complete").fireEvent("failure",this.xhr);},loadstart:function(e){this.fireEvent("loadstart",[e,this.xhr]); +},progress:function(e){this.fireEvent("progress",[e,this.xhr]);},timeout:function(){this.fireEvent("timeout",this.xhr);},setHeader:function(e,f){this.headers[e]=f; +return this;},getHeader:function(e){return Function.attempt(function(){return this.xhr.getResponseHeader(e);}.bind(this));},check:function(){if(!this.running){return true; +}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));return false;}return false;},send:function(o){if(!this.check(o)){return this; +}this.options.isSuccess=this.options.isSuccess||this.isSuccess;this.running=true;var l=typeOf(o);if(l=="string"||l=="element"){o={data:o};}var h=this.options; +o=Object.append({data:h.data,url:h.url,method:h.method},o);var j=o.data,f=String(o.url),e=o.method.toLowerCase();switch(typeOf(j)){case"element":j=document.id(j).toQueryString(); +break;case"object":case"hash":j=Object.toQueryString(j);}if(this.options.format){var m="format="+this.options.format;j=(j)?m+"&"+j:m;}if(this.options.emulation&&!["get","post"].contains(e)){var k="_method="+e; +j=(j)?k+"&"+j:k;e="post";}if(this.options.urlEncoded&&["post","put"].contains(e)){var g=(this.options.encoding)?"; charset="+this.options.encoding:"";this.headers["Content-type"]="application/x-www-form-urlencoded"+g; +}if(!f){f=document.location.pathname;}var i=f.lastIndexOf("/");if(i>-1&&(i=f.indexOf("#"))>-1){f=f.substr(0,i);}if(this.options.noCache){f+=(f.contains("?")?"&":"?")+String.uniqueID(); +}if(j&&e=="get"){f+=(f.contains("?")?"&":"?")+j;j=null;}var n=this.xhr;if(a){n.onloadstart=this.loadstart.bind(this);n.onprogress=this.progress.bind(this); +}n.open(e.toUpperCase(),f,this.options.async,this.options.user,this.options.password);if(this.options.user&&"withCredentials" in n){n.withCredentials=true; +}n.onreadystatechange=this.onStateChange.bind(this);Object.each(this.headers,function(q,p){try{n.setRequestHeader(p,q);}catch(r){this.fireEvent("exception",[p,q]); +}},this);this.fireEvent("request");n.send(j);if(!this.options.async){this.onStateChange();}else{if(this.options.timeout){this.timer=this.timeout.delay(this.options.timeout,this); +}}return this;},cancel:function(){if(!this.running){return this;}this.running=false;var e=this.xhr;e.abort();clearTimeout(this.timer);e.onreadystatechange=d; +if(a){e.onprogress=e.onloadstart=d;}this.xhr=new Browser.Request();this.fireEvent("cancel");return this;}});var b={};["get","post","put","delete","GET","POST","PUT","DELETE"].each(function(e){b[e]=function(g){var f={method:e}; +if(g!=null){f.data=g;}return this.send(f);};});c.implement(b);Element.Properties.send={set:function(e){var f=this.get("send").cancel();f.setOptions(e); +return this;},get:function(){var e=this.retrieve("send");if(!e){e=new c({data:this,link:"cancel",method:this.get("method")||"post",url:this.get("action")}); +this.store("send",e);}return e;}};Element.implement({send:function(e){var f=this.get("send");f.send({data:this,url:e||f.options.url});return this;}});})(); +Request.HTML=new Class({Extends:Request,options:{update:false,append:false,evalScripts:true,filter:false,headers:{Accept:"text/html, application/xml, text/xml, */*"}},success:function(f){var e=this.options,c=this.response; +c.html=f.stripScripts(function(h){c.javascript=h;});var d=c.html.match(/]*>([\s\S]*?)<\/body>/i);if(d){c.html=d[1];}var b=new Element("div").set("html",c.html); +c.tree=b.childNodes;c.elements=b.getElements(e.filter||"*");if(e.filter){c.tree=c.elements;}if(e.update){var g=document.id(e.update).empty();if(e.filter){g.adopt(c.elements); +}else{g.set("html",c.html);}}else{if(e.append){var a=document.id(e.append);if(e.filter){c.elements.reverse().inject(a);}else{a.adopt(b.getChildren());}}}if(e.evalScripts){Browser.exec(c.javascript); +}this.onSuccess(c.tree,c.elements,c.html,c.javascript);}});Element.Properties.load={set:function(a){var b=this.get("load").cancel();b.setOptions(a);return this; +},get:function(){var a=this.retrieve("load");if(!a){a=new Request.HTML({data:this,link:"cancel",update:this,method:"get"});this.store("load",a);}return a; +}};Element.implement({load:function(){this.get("load").send(Array.link(arguments,{data:Type.isObject,url:Type.isString}));return this;}});if(typeof JSON=="undefined"){this.JSON={}; +}(function(){var special={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};var escape=function(chr){return special[chr]||"\\u"+("0000"+chr.charCodeAt(0).toString(16)).slice(-4); +};JSON.validate=function(string){string=string.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""); +return(/^[\],:{}\s]*$/).test(string);};JSON.encode=JSON.stringify?function(obj){return JSON.stringify(obj);}:function(obj){if(obj&&obj.toJSON){obj=obj.toJSON(); +}switch(typeOf(obj)){case"string":return'"'+obj.replace(/[\x00-\x1f\\"]/g,escape)+'"';case"array":return"["+obj.map(JSON.encode).clean()+"]";case"object":case"hash":var string=[]; +Object.each(obj,function(value,key){var json=JSON.encode(value);if(json){string.push(JSON.encode(key)+":"+json);}});return"{"+string+"}";case"number":case"boolean":return""+obj; +case"null":return"null";}return null;};JSON.decode=function(string,secure){if(!string||typeOf(string)!="string"){return null;}if(secure||JSON.secure){if(JSON.parse){return JSON.parse(string); +}if(!JSON.validate(string)){throw new Error("JSON could not decode the input; security is enabled and the value is not secure.");}}return eval("("+string+")"); +};})();Request.JSON=new Class({Extends:Request,options:{secure:true},initialize:function(a){this.parent(a);Object.append(this.headers,{Accept:"application/json","X-Request":"JSON"}); +},success:function(c){var b;try{b=this.response.json=JSON.decode(c,this.options.secure);}catch(a){this.fireEvent("error",[c,a]);return;}if(b==null){this.onFailure(); +}else{this.onSuccess(b,c);}}});var Cookie=new Class({Implements:Options,options:{path:"/",domain:false,duration:false,secure:false,document:document,encode:true},initialize:function(b,a){this.key=b; +this.setOptions(a);},write:function(b){if(this.options.encode){b=encodeURIComponent(b);}if(this.options.domain){b+="; domain="+this.options.domain;}if(this.options.path){b+="; path="+this.options.path; +}if(this.options.duration){var a=new Date();a.setTime(a.getTime()+this.options.duration*24*60*60*1000);b+="; expires="+a.toGMTString();}if(this.options.secure){b+="; secure"; +}this.options.document.cookie=this.key+"="+b;return this;},read:function(){var a=this.options.document.cookie.match("(?:^|;)\\s*"+this.key.escapeRegExp()+"=([^;]*)"); +return(a)?decodeURIComponent(a[1]):null;},dispose:function(){new Cookie(this.key,Object.merge({},this.options,{duration:-1})).write("");return this;}}); +Cookie.write=function(b,c,a){return new Cookie(b,a).write(c);};Cookie.read=function(a){return new Cookie(a).read();};Cookie.dispose=function(b,a){return new Cookie(b,a).dispose(); +};(function(i,k){var l,f,e=[],c,b,d=k.createElement("div");var g=function(){clearTimeout(b);if(l){return;}Browser.loaded=l=true;k.removeListener("DOMContentLoaded",g).removeListener("readystatechange",a); +k.fireEvent("domready");i.fireEvent("domready");};var a=function(){for(var m=e.length;m--;){if(e[m]()){g();return true;}}return false;};var j=function(){clearTimeout(b); +if(!a()){b=setTimeout(j,10);}};k.addListener("DOMContentLoaded",g);var h=function(){try{d.doScroll();return true;}catch(m){}return false;};if(d.doScroll&&!h()){e.push(h); +c=true;}if(k.readyState){e.push(function(){var m=k.readyState;return(m=="loaded"||m=="complete");});}if("onreadystatechange" in k){k.addListener("readystatechange",a); +}else{c=true;}if(c){j();}Element.Events.domready={onAdd:function(m){if(l){m.call(this);}}};Element.Events.load={base:"load",onAdd:function(m){if(f&&this==i){m.call(this); +}},condition:function(){if(this==i){g();delete Element.Events.load;}return true;}};i.addEvent("load",function(){f=true;});})(window,document);(function(){var Swiff=this.Swiff=new Class({Implements:Options,options:{id:null,height:1,width:1,container:null,properties:{},params:{quality:"high",allowScriptAccess:"always",wMode:"window",swLiveConnect:true},callBacks:{},vars:{}},toElement:function(){return this.object; +},initialize:function(path,options){this.instance="Swiff_"+String.uniqueID();this.setOptions(options);options=this.options;var id=this.id=options.id||this.instance; +var container=document.id(options.container);Swiff.CallBacks[this.instance]={};var params=options.params,vars=options.vars,callBacks=options.callBacks; +var properties=Object.append({height:options.height,width:options.width},options.properties);var self=this;for(var callBack in callBacks){Swiff.CallBacks[this.instance][callBack]=(function(option){return function(){return option.apply(self.object,arguments); +};})(callBacks[callBack]);vars[callBack]="Swiff.CallBacks."+this.instance+"."+callBack;}params.flashVars=Object.toQueryString(vars);if(Browser.ie){properties.classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; +params.movie=path;}else{properties.type="application/x-shockwave-flash";}properties.data=path;var build='';}}build+="";this.object=((container)?container.empty():new Element("div")).set("html",build).firstChild; +},replaces:function(element){element=document.id(element,true);element.parentNode.replaceChild(this.toElement(),element);return this;},inject:function(element){document.id(element,true).appendChild(this.toElement()); +return this;},remote:function(){return Swiff.remote.apply(Swiff,[this.toElement()].append(arguments));}});Swiff.CallBacks={};Swiff.remote=function(obj,fn){var rs=obj.CallFunction(''+__flash__argumentsToXML(arguments,2)+""); +return eval(rs);};})(); \ No newline at end of file diff --git a/swarm/examples/album/mootools-idle.js b/swarm/examples/album/mootools-idle.js new file mode 100644 index 0000000000..eb4651c3e5 --- /dev/null +++ b/swarm/examples/album/mootools-idle.js @@ -0,0 +1,198 @@ +/* +--- +description: Determines when the user is idle (not interacting with the page) so that you can respond appropriately. + +license: +- MIT-style license + +authors: +- Espen 'Rexxars' Hovlandsdal (http://rexxars.com/) + +requires: +core/1.2.4: +- Class.Extras +- Element.Event + +provides: +- IdleTimer + +inspiration: +- Inspired by Nicholas C. Zakas' Idle Timer (http://yuilibrary.com/gallery/show/idletimer) Copyright (c) 2009 Nicholas C. Zakas, [YUI BSD](http://developer.yahoo.com/yui/license.html) +- Also inspired by Paul Irish's jQuery idleTimer (http://paulirish.com/2009/jquery-idletimer-plugin/) Copyright (c) 2009 Paul Irish, [MIT License](http://opensource.org/licenses/mit-license.php) +... +*/ + +IdleTimer = new Class({ + + Implements: [Events, Options], + + options: { + /* + onStart: function(){}, + onStop: function(){}, + onIdle: function(){}, + onActive: function(){}, + onTimeoutChanged: function(){}, + */ + timeout: 60000, + events: ['mousemove', 'keydown', 'mousewheel', 'mousedown', 'touchstart', 'touchmove'] + }, + + initialize: function(element, options) { + this.setOptions(options); + this.element = document.id(element); + this.activeBound = this.active.bind(this); + this.isIdle = false; + this.started = false; + this.lastPos = false; + }, + + /** + * Stops any existing timeouts and removes the bound events + */ + stop: function() { + clearTimeout(this.timer); + + // Remove bound events + for(var i = 0; i < this.options.events.length; i++) { + this.element.removeEvent(this.options.events[i], this.activeBound); + } + this.bound = false; + this.started = false; + this.lastPos = false; + this.fireEvent('stop'); + return this; + }, + + /** + * Triggered when the user becomes active. May also be launched manually by scripts + * if implementing some sort of custom events etc. An example would be flash files + * which does not trigger the documents onmousemove event, you could have the flash + * call this method to prevent the idle event from being triggered. + */ + active: function(e) { + if(e.event.type == 'mousemove') + { + // Fix https://code.google.com/p/chromium/issues/detail?id=103041 + var pos = [e.event.clientX, e.event.clientY]; + if(this.lastPos === false || + (this.lastPos[0] != pos[0] && this.lastPos[1] != pos[1])) + this.lastPos = pos; + else + return; + } + clearTimeout(this.timer); + if(this.isIdle) this.fireEvent('active'); + this.isIdle = false; + this.start(); + }, + + /** + * Fired when the user becomes idle + */ + idle: function() { + if(this.timer) clearTimeout(this.timer); // If called manually, timer will have to be removed + this.isIdle = true; + this.fireEvent('idle'); + }, + + /** + * Starts the timer which eventually will reach idle() if the user is inactive + */ + start: function() { + if(this.timer) clearTimeout(this.timer); // If called twice, timer will have to be removed + this.timer = this.idle.delay(this.options.timeout, this); + this.lastActive = Date.now(); + if(!this.bound) this.bind(); + this.started = true; + return this; + }, + + /** + * Bind events to the element + */ + bind: function() { + for(var i = 0; i < this.options.events.length; i++) { + this.element.addEvent(this.options.events[i], this.activeBound); + } + this.bound = true; + this.fireEvent('start'); + }, + + /** + * Returns how many seconds/milliseconds have passed since the user was last idle + */ + getIdleTime: function(returnSeconds) { + return returnSeconds ? Math.round((Date.now() - this.lastActive) / 1000) : Date.now() - this.lastActive; + }, + + /** + * Sets the number of milliseconds is concidered "idle". + * Will also attempt to fix any difference in the old and new timeout values, + * unless you pass true as whenActive - in this case the new timeout will be + * in play the next time the user is active again. + */ + setTimeout: function(newTime, whenActive) { + var old = this.options.timeout; + this.options.timeout = newTime; + + if(whenActive) return this; // The developer wants to wait until the next time the user is active before setting the new timeout + + // In all cases, we need a new timer + clearTimeout(this.timer); + + // Fire a new timeout event + this.fireEvent('timeoutChanged', newTime); + + // How much time has ellapsed since we were last active? + var elapsed = this.getIdleTime(); + + if(elapsed < newTime && this.isIdle) { + // Set as active, cause the new "idle" time has not been reached + this.fireEvent('active'); + this.isIdle = false; + } else if(elapsed >= newTime) { + // We've not reached the limit before, but with the new timeout, we now have. + this.idle(); + return this; + } + + // Set new timer + this.timer = this.idle.delay(newTime - elapsed, this); + return this; + } + +}); + +Element.Properties.idle = { + + set: function(options) { + var idle = this.retrieve('idle'); + if (idle) idle.stop(); + return this.eliminate('idle').store('idle:options', options); + }, + + get: function(options) { + if (options || !this.retrieve('idle')) { + if (options || !this.retrieve('idle:options')) this.set('idle', options); + this.store('idle', new IdleTimer(this, this.retrieve('idle:options'))); + } + return this.retrieve('idle'); + } + +}; + +Element.Events.idle = { + onAdd: function(fn) { + var global = this.get ? false : true; + var idler = global ? window.idleTimer : this.get('idle'); + if(global && !idler) { idler = window.idleTimer = new IdleTimer(Browser.ie ? document : this); } + if(!idler.started) idler.start(); + idler.addEvent('idle', fn); + } +}; +Element.Events.active = { + onAdd: function(fn) { + (this.get ? this.get('idle') : window.idleTimer).addEvent('active', fn); + } +}; diff --git a/swarm/examples/album/mootools-mooswipe.js b/swarm/examples/album/mootools-mooswipe.js new file mode 100644 index 0000000000..7d3f76e7f9 --- /dev/null +++ b/swarm/examples/album/mootools-mooswipe.js @@ -0,0 +1,77 @@ +/* +--- +description: Swipe events for touch devices. + +license: MIT-style. + +authors: +- Caleb Troughton + +requires: + core/1.2.4: + - Element.Event + - Class + - Class.Extras + +provides: + MooSwipe +*/ +var MooSwipe = MooSwipe || new Class({ + Implements: [Options, Events], + + options: { + //onSwipeleft: $empty, + //onSwiperight: $empty, + //onSwipeup: $empty, + //onSwipedown: $empty, + tolerance: 50, + preventDefaults: true + }, + + element: null, + startX: null, + startY: null, + isMoving: false, + + initialize: function(el, options) { + this.setOptions(options); + this.element = $(el); + this.element.addListener('touchstart', this.onTouchStart.bind(this)); + }, + + cancelTouch: function() { + this.element.removeListener('touchmove', this.onTouchMove); + this.startX = null; + this.startY = null; + this.isMoving = false; + }, + + onTouchMove: function(e) { + if (e.touches.length == 1) { + this.options.preventDefaults && e.preventDefault(); + if (this.isMoving) { + var dx = this.startX - e.touches[0].pageX; + var dy = this.startY - e.touches[0].pageY; + if (Math.abs(dx) >= this.options.tolerance) { + this.cancelTouch(); + this.fireEvent(dx > 0 ? 'swipeleft' : 'swiperight'); + } else if (Math.abs(dy) >= this.options.tolerance) { + this.cancelTouch(); + this.fireEvent(dy > 0 ? 'swipedown' : 'swipeup'); + } + } + } + else if (this.isMoving) { + this.cancelTouch(); + } + }, + + onTouchStart: function(e) { + if (e.touches.length == 1) { + this.startX = e.touches[0].pageX; + this.startY = e.touches[0].pageY; + this.isMoving = true; + this.element.addListener('touchmove', this.onTouchMove.bind(this)); + } + } +}); diff --git a/swarm/examples/album/mootools-more-1.4.js b/swarm/examples/album/mootools-more-1.4.js new file mode 100644 index 0000000000..43394a7e10 --- /dev/null +++ b/swarm/examples/album/mootools-more-1.4.js @@ -0,0 +1,45 @@ +// MooTools: the javascript framework. +// Load this file's selection again by visiting: http://mootools.net/more/79cd71c11847ba8e70a662f66829987d +// Or build this file again with packager using: packager build More/Element.Measure More/Fx.Scroll More/Assets +/* +--- +copyrights: + - [MooTools](http://mootools.net) + +licenses: + - [MIT License](http://mootools.net/license.txt) +... +*/ +MooTools.More={version:"1.4.0.1",build:"a4244edf2aa97ac8a196fc96082dd35af1abab87"};(function(){var b=function(e,d){var f=[];Object.each(d,function(g){Object.each(g,function(h){e.each(function(i){f.push(i+"-"+h+(i=="border"?"-width":"")); +});});});return f;};var c=function(f,e){var d=0;Object.each(e,function(h,g){if(g.test(f)){d=d+h.toInt();}});return d;};var a=function(d){return !!(!d||d.offsetHeight||d.offsetWidth); +};Element.implement({measure:function(h){if(a(this)){return h.call(this);}var g=this.getParent(),e=[];while(!a(g)&&g!=document.body){e.push(g.expose()); +g=g.getParent();}var f=this.expose(),d=h.call(this);f();e.each(function(i){i();});return d;},expose:function(){if(this.getStyle("display")!="none"){return function(){}; +}var d=this.style.cssText;this.setStyles({display:"block",position:"absolute",visibility:"hidden"});return function(){this.style.cssText=d;}.bind(this); +},getDimensions:function(d){d=Object.merge({computeSize:false},d);var i={x:0,y:0};var h=function(j,e){return(e.computeSize)?j.getComputedSize(e):j.getSize(); +};var f=this.getParent("body");if(f&&this.getStyle("display")=="none"){i=this.measure(function(){return h(this,d);});}else{if(f){try{i=h(this,d);}catch(g){}}}return Object.append(i,(i.x||i.x===0)?{width:i.x,height:i.y}:{x:i.width,y:i.height}); +},getComputedSize:function(d){d=Object.merge({styles:["padding","border"],planes:{height:["top","bottom"],width:["left","right"]},mode:"both"},d);var g={},e={width:0,height:0},f; +if(d.mode=="vertical"){delete e.width;delete d.planes.width;}else{if(d.mode=="horizontal"){delete e.height;delete d.planes.height;}}b(d.styles,d.planes).each(function(h){g[h]=this.getStyle(h).toInt(); +},this);Object.each(d.planes,function(i,h){var k=h.capitalize(),j=this.getStyle(h);if(j=="auto"&&!f){f=this.getDimensions();}j=g[h]=(j=="auto")?f[h]:j.toInt(); +e["total"+k]=j;i.each(function(m){var l=c(m,g);e["computed"+m.capitalize()]=l;e["total"+k]+=l;});},this);return Object.append(e,g);}});})();(function(){Fx.Scroll=new Class({Extends:Fx,options:{offset:{x:0,y:0},wheelStops:true},initialize:function(c,b){this.element=this.subject=document.id(c); +this.parent(b);if(typeOf(this.element)!="element"){this.element=document.id(this.element.getDocument().body);}if(this.options.wheelStops){var d=this.element,e=this.cancel.pass(false,this); +this.addEvent("start",function(){d.addEvent("mousewheel",e);},true);this.addEvent("complete",function(){d.removeEvent("mousewheel",e);},true);}},set:function(){var b=Array.flatten(arguments); +if(Browser.firefox){b=[Math.round(b[0]),Math.round(b[1])];}this.element.scrollTo(b[0],b[1]);return this;},compute:function(d,c,b){return[0,1].map(function(e){return Fx.compute(d[e],c[e],b); +});},start:function(c,d){if(!this.check(c,d)){return this;}var b=this.element.getScroll();return this.parent([b.x,b.y],[c,d]);},calculateScroll:function(g,f){var d=this.element,b=d.getScrollSize(),h=d.getScroll(),j=d.getSize(),c=this.options.offset,i={x:g,y:f}; +for(var e in i){if(!i[e]&&i[e]!==0){i[e]=h[e];}if(typeOf(i[e])!="number"){i[e]=b[e]-j[e];}i[e]+=c[e];}return[i.x,i.y];},toTop:function(){return this.start.apply(this,this.calculateScroll(false,0)); +},toLeft:function(){return this.start.apply(this,this.calculateScroll(0,false));},toRight:function(){return this.start.apply(this,this.calculateScroll("right",false)); +},toBottom:function(){return this.start.apply(this,this.calculateScroll(false,"bottom"));},toElement:function(d,e){e=e?Array.from(e):["x","y"];var c=a(this.element)?{x:0,y:0}:this.element.getScroll(); +var b=Object.map(document.id(d).getPosition(this.element),function(g,f){return e.contains(f)?g+c[f]:false;});return this.start.apply(this,this.calculateScroll(b.x,b.y)); +},toElementEdge:function(d,g,e){g=g?Array.from(g):["x","y"];d=document.id(d);var i={},f=d.getPosition(this.element),j=d.getSize(),h=this.element.getScroll(),b=this.element.getSize(),c={x:f.x+j.x,y:f.y+j.y}; +["x","y"].each(function(k){if(g.contains(k)){if(c[k]>h[k]+b[k]){i[k]=c[k]-b[k];}if(f[k] + + + Register Swarm protocol handler + + + +

Register Swarm protocol handler

+

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

+ + diff --git a/swarm/network/depo.go b/swarm/network/depo.go new file mode 100644 index 0000000000..eeb50f4595 --- /dev/null +++ b/swarm/network/depo.go @@ -0,0 +1,190 @@ +package network + +import ( + "bytes" + "encoding/binary" + "time" + + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +// Handler for storage/retrieval related protocol requests +// implements the StorageHandler interface used by the bzz protocol +type Depo struct { + hashfunc storage.Hasher + localStore storage.ChunkStore + netStore storage.ChunkStore +} + +func NewDepo(hash storage.Hasher, localStore, remoteStore storage.ChunkStore) *Depo { + return &Depo{ + hashfunc: hash, + localStore: localStore, + netStore: remoteStore, // entrypoint internal + } +} + +// Handles UnsyncedKeysMsg after msg decoding - unsynced hashes upto sync state +// * the remote sync state is just stored and handled in protocol +// * filters through the new syncRequests and send the ones missing +// * back immediately as a deliveryRequest message +// * empty message just pings back for more (is this needed?) +// * strict signed sync states may be needed. +func (self *Depo) HandleUnsyncedKeysMsg(req *unsyncedKeysMsgData, p *peer) error { + unsynced := req.Unsynced + var missing []*syncRequest + var chunk *storage.Chunk + var err error + for _, req := range unsynced { + // skip keys that are found, + chunk, err = self.localStore.Get(storage.Key(req.Key[:])) + if err != nil || chunk.SData == nil { + missing = append(missing, req) + } + } + glog.V(logger.Debug).Infof("[BZZ] Depo.HandleUnsyncedKeysMsg: received %v unsynced keys: %v missing. new state: %v", len(unsynced), len(missing), req.State) + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleUnsyncedKeysMsg: received %v", unsynced) + // send delivery request with missing keys + err = p.deliveryRequest(missing) + if err != nil { + return err + } + p.syncState = req.State + return nil +} + +// Handles deliveryRequestMsg +// * serves actual chunks asked by the remote peer +// by pushing to the delivery queue (sync db) of the correct priority +// (remote peer is free to reprioritize) +// * the message implies remote peer wants more, so trigger for +// * new outgoing unsynced keys message is fired +func (self *Depo) HandleDeliveryRequestMsg(req *deliveryRequestMsgData, p *peer) error { + deliver := req.Deliver + // queue the actual delivery of a chunk () + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleDeliveryRequestMsg: received %v delivery requests: %v", len(deliver), deliver) + for _, sreq := range deliver { + // TODO: look up in cache here or in deliveries + // priorities are taken from the message so the remote party can + // reprioritise to at their leisure + // r = self.pullCached(sreq.Key) // pulls and deletes from cache + Push(p, sreq.Key, sreq.Priority) + } + + // sends it out as unsyncedKeysMsg + p.syncer.sendUnsyncedKeys() + return nil +} + +// the entrypoint for store requests coming from the bzz wire protocol +// if key found locally, return. otherwise +// remote is untrusted, so hash is verified and chunk passed on to NetStore +func (self *Depo) HandleStoreRequestMsg(req *storeRequestMsgData, p *peer) { + req.from = p + chunk, err := self.localStore.Get(req.Key) + switch { + case err != nil: + glog.V(logger.Detail).Infof("[BZZ] Depo.handleStoreRequest: %v not found locally. create new chunk/request", req.Key) + // not found in memory cache, ie., a genuine store request + // create chunk + chunk = storage.NewChunk(req.Key, nil) + + case chunk.SData == nil: + // found chunk in memory store, needs the data, validate now + hasher := self.hashfunc() + hasher.Write(req.SData) + if !bytes.Equal(hasher.Sum(nil), req.Key) { + // data does not validate, ignore + // TODO: peer should be penalised/dropped? + glog.V(logger.Warn).Infof("[BZZ] Depo.HandleStoreRequest: chunk invalid. store request ignored: %v", req) + return + } + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleStoreRequest: %v. request entry found", req) + + default: + // data is found, store request ignored + // this should update access count? + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleStoreRequest: %v found locally. ignore.", req) + return + } + + // update chunk with size and data + chunk.SData = req.SData + chunk.Size = int64(binary.LittleEndian.Uint64(req.SData[0:8])) + glog.V(logger.Detail).Infof("[BZZ] delivery of %p from %v", chunk, p) + chunk.Source = p + self.netStore.Put(chunk) +} + +// entrypoint for retrieve requests coming from the bzz wire protocol +// checks swap balance - return if peer has no credit +func (self *Depo) HandleRetrieveRequestMsg(req *retrieveRequestMsgData, p *peer) { + req.from = p + // swap - record credit for 1 request + // note that only charge actual reqsearches + if err := p.swap.Add(1); err != nil { + glog.V(logger.Warn).Infof("[BZZ] Depo.HandleRetrieveRequest: %v - cannot process request: %v", req.Key.Log(), err) + return + } + + // call storage.NetStore#Get which + // blocks until local retrieval finished + // launches cloud retrieval in a separate go routine + chunk, _ := self.netStore.Get(req.Key) + req = self.strategyUpdateRequest(chunk.Req, req) + // check if we can immediately deliver + if chunk.SData != nil { + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleRetrieveRequest: %v - content found, delivering...", req.Key.Log()) + + if req.MaxSize == 0 || int64(req.MaxSize) >= chunk.Size { + sreq := &storeRequestMsgData{ + Id: req.Id, + Key: chunk.Key, + SData: chunk.SData, + requestTimeout: req.timeout, // + } + p.syncer.addRequest(sreq, DeliverReq) + } else { + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleRetrieveRequest: %v - content found, not wanted", req.Key.Log()) + } + } else { + glog.V(logger.Detail).Infof("[BZZ] Depo.HandleRetrieveRequest: %v - content not found locally. asked swarm for help. will get back", req.Key.Log()) + } +} + +// add peer request the chunk and decides the timeout for the response if still searching +func (self *Depo) strategyUpdateRequest(rs *storage.RequestStatus, origReq *retrieveRequestMsgData) (req *retrieveRequestMsgData) { + glog.V(logger.Detail).Infof("[BZZ] Depo.strategyUpdateRequest: key %v", origReq.Key.Log()) + // we do not create an alternative one + req = origReq + if rs != nil { + self.addRequester(rs, req) + req.setTimeout(self.searchTimeout(rs, req)) + } + return +} + +// decides the timeout promise sent with the immediate peers response to a retrieve request +// if timeout is explicitly set and expired +func (self *Depo) searchTimeout(rs *storage.RequestStatus, req *retrieveRequestMsgData) (timeout *time.Time) { + reqt := req.getTimeout() + t := time.Now().Add(searchTimeout) + if reqt != nil && reqt.Before(t) { + return reqt + } else { + return &t + } +} + +/* +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 *Depo) addRequester(rs *storage.RequestStatus, req *retrieveRequestMsgData) { + glog.V(logger.Detail).Infof("[BZZ] Depo.addRequester: key %v - add peer [%v] to req.Id %v", req.Key.Log(), req.from, req.Id) + list := rs.Requesters[req.Id] + rs.Requesters[req.Id] = append(list, req) +} diff --git a/swarm/network/forwarding.go b/swarm/network/forwarding.go new file mode 100644 index 0000000000..46c7808bd1 --- /dev/null +++ b/swarm/network/forwarding.go @@ -0,0 +1,131 @@ +package network + +import ( + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +const requesterCount = 3 + +/* +forwarder implements the CloudStore interface (use by storage.NetStore) +and serves as the cloud store backend orchestrating storage/retrieval/delivery +via the native bzz protocol +which uses an MSB logarithmic distance-based semi-permanent Kademlia table for +* recursive forwarding style routing for retrieval +* smart syncronisation +* TODO: beeline delivery, IPFS, IPΞS +*/ + +type forwarder struct { + hive *Hive +} + +func NewForwarder(hive *Hive) *forwarder { + return &forwarder{hive: hive} +} + +// generate a unique id uint64 +func generateId() uint64 { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return uint64(r.Int63()) +} + +var searchTimeout = 3 * time.Second + +// forwarding logic +// logic propagating retrieve requests to peers given by the kademlia hive +func (self *forwarder) Retrieve(chunk *storage.Chunk) { + peers := self.hive.getPeers(chunk.Key, 0) + glog.V(logger.Detail).Infof("[BZZ] forwarder.Retrieve: %v - received %d peers from KΛÐΞMLIΛ...", chunk.Key.Log(), len(peers)) + for _, p := range peers { + glog.V(logger.Detail).Infof("[BZZ] forwarder.Retrieve: sending retrieveRequest %v to peer [%v]", chunk.Key.Log(), p) + var req *retrieveRequestMsgData + OUT: + for _, recipients := range chunk.Req.Requesters { + for _, recipient := range recipients { + req := recipient.(*retrieveRequestMsgData) + if req.from.Addr() == p.Addr() { + break OUT + } + } + } + if req != nil { + if err := p.swap.Add(-1); err == nil { + p.retrieve(req) + break + } else { + glog.V(logger.Warn).Infof("[BZZ] forwarder.Retrieve: unable to send retrieveRequest to peer [%v]: %v", chunk.Key.Log(), err) + } + } + } +} + +// requests to specific peers given by the kademlia hive +// except for peers that the store request came from (if any) +// delivery queueing taken care of by syncer +func (self *forwarder) Store(chunk *storage.Chunk) { + var n int + msg := &storeRequestMsgData{ + Key: chunk.Key, + SData: chunk.SData, + } + var source *peer + if chunk.Source != nil { + source = chunk.Source.(*peer) + } + for _, p := range self.hive.getPeers(chunk.Key, 0) { + glog.V(logger.Detail).Infof("[BZZ] %v %v", p, chunk) + + if source == nil || p.Addr() != source.Addr() { + n++ + Deliver(p, msg, PropagateReq) + } + } + glog.V(logger.Detail).Infof("[BZZ] forwarder.Store: sent to %v ps (chunk = %v)", n, chunk) +} + +// once a chunk is found deliver it to its requesters unless timed out +func (self *forwarder) Deliver(chunk *storage.Chunk) { + // iterate over request entries + for id, requesters := range chunk.Req.Requesters { + counter := requesterCount + msg := &storeRequestMsgData{ + Key: chunk.Key, + SData: chunk.SData, + } + var n int + var req *retrieveRequestMsgData + // iterate over requesters with the same id + for id, r := range requesters { + req = r.(*retrieveRequestMsgData) + if req.timeout == nil || req.timeout.After(time.Now()) { + glog.V(logger.Ridiculousness).Infof("[BZZ] forwarder.Deliver: %v -> %v", req.Id, req.from) + msg.Id = uint64(id) + Deliver(req.from, msg, DeliverReq) + n++ + counter-- + if counter <= 0 { + break + } + } + } + glog.V(logger.Detail).Infof("[BZZ] NetStore.Deliver: submit chunk %v (request id %v) for delivery to %v peers", chunk.Key.Log(), id, n) + } +} + +// initiate delivery of a chunk to a particular peer via syncer#addRequest +// depending on syncer mode and priority settings and sync request type +// this either goes via confirmation roundtrip or queued or pushed directly +func Deliver(p *peer, req interface{}, ty int) { + p.syncer.addRequest(req, ty) +} + +// push chunk over to peer +func Push(p *peer, key storage.Key, priority uint) { + p.syncer.doDelivery(key, priority, p.syncer.quit) +} diff --git a/swarm/network/hive.go b/swarm/network/hive.go new file mode 100644 index 0000000000..1850f8fa57 --- /dev/null +++ b/swarm/network/hive.go @@ -0,0 +1,322 @@ +package network + +import ( + "fmt" + "math/rand" + "path/filepath" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/kademlia" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +// 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 { + listenAddr func() string + callInterval uint64 + id discover.NodeID + addr kademlia.Address + kad *kademlia.Kademlia + path string + toggle chan bool + more chan bool +} + +const ( + callInterval = 10000000000 + bucketSize = 3 + maxProx = 10 + proxBinSize = 8 +) + +type HiveParams struct { + CallInterval uint64 + KadDbPath string + *kademlia.KadParams +} + +func NewHiveParams(path string) *HiveParams { + kad := kademlia.NewKadParams() + kad.BucketSize = bucketSize + kad.MaxProx = maxProx + kad.ProxBinSize = proxBinSize + + return &HiveParams{ + CallInterval: callInterval, + KadDbPath: filepath.Join(path, "bzz-peers.json"), + KadParams: kad, + } +} + +func NewHive(addr common.Hash, params *HiveParams) *Hive { + kad := kademlia.New(kademlia.Address(addr), params.KadParams) + return &Hive{ + callInterval: params.CallInterval, + kad: kad, + addr: kad.Addr(), + path: params.KadDbPath, + } +} + +// public accessor to the hive base address +func (self *Hive) Addr() kademlia.Address { + return self.addr +} + +// Start receives network info only at startup +// listedAddr is a function to retrieve listening address to advertise to peers +// connectPeer is a function to connect to a peer based on its NodeID or enode URL +// there are called on the p2p.Server which runs on the node +func (self *Hive) Start(id discover.NodeID, listenAddr func() string, connectPeer func(string) error) (err error) { + self.toggle = make(chan bool) + self.more = make(chan bool) + self.id = id + self.listenAddr = listenAddr + err = self.kad.Load(self.path, nil) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] KΛÐΞMLIΛ Warning: error reading kaddb '%s' (skipping): %v", self.path, err) + err = nil + } + // this loop is doing bootstrapping and maintains a healthy table + go self.keepAlive() + go func() { + // whenever toggled ask kademlia about most preferred peer + for alive := range self.more { + if !alive { + // receiving false closes the loop while allowing parallel routines + // to attempt to write to more (remove Peer when shutting down) + return + } + node, proxLimit := self.kad.FindBest() + if node != nil && len(node.Url) > 0 { + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: call for bee %v", node.Url) + // enode or any lower level connection address is unnecessary in future + // discovery table is used to look it up. + connectPeer(node.Url) + } else if proxLimit > -1 { + // a random peer is taken from the table + peers := self.kad.FindClosest(kademlia.RandomAddressAt(self.addr, rand.Intn(self.kad.MaxProx)), 1) + if len(peers) > 0 { + // a random address at prox bin 0 is sent for lookup + randAddr := kademlia.RandomAddressAt(self.addr, proxLimit) + req := &retrieveRequestMsgData{ + Key: storage.Key(randAddr[:]), + } + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: call any bee in area %v messenger bee %v", randAddr, peers[0]) + peers[0].(*peer).retrieve(req) + } + self.toggle <- true + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: buzz kept alive") + } else { + self.toggle <- false + } + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: queen's address: %v, population: %d (%d)\n%v", self.addr, self.kad.Count(), self.kad.DBCount(), self.kad) + } + }() + return +} + +// keepAlive is a forever loop +// in its awake state it periodically triggers connection attempts +// by writing to self.more until Kademlia Table is saturated +// wake state is toggled by writing to self.toggle +// it restarts if the table becomes non-full again due to disconnections +func (self *Hive) keepAlive() { + var alarm <-chan time.Time + for { + select { + case <-alarm: + if self.kad.DBCount() > 0 { + select { + case self.more <- true: + default: + } + } + case need, alive := <-self.toggle: + if !alive { + self.more <- false + return + } + if alarm == nil && need { + alarm = time.NewTicker(time.Duration(self.callInterval)).C + } + if alarm != nil && !need { + alarm = nil + + } + } + } +} + +func (self *Hive) Stop() error { + // closing toggle channel quits the updateloop + close(self.toggle) + return self.kad.Save(self.path, saveSync) +} + +// called at the end of a successful protocol handshake +func (self *Hive) addPeer(p *peer) { + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: hi new bee %v", p) + self.kad.On(p, loadSync) + // self lookup (can be encoded as nil/zero key since peers addr known) + no id () + // the most common way of saying hi in bzz is initiation of gossip + // let me know about anyone new from my hood , here is the storageradius + // to send the 6 byte self lookup + // we do not record as request or forward it, just reply with peers + p.retrieve(&retrieveRequestMsgData{}) + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: 'whatsup wheresdaparty' sent to %v", p) + select { + case self.more <- true: + default: + } +} + +// called after peer disconnected +func (self *Hive) removePeer(p *peer) { + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: bee %v gone offline", p) + self.kad.Off(p, saveSync) + select { + case self.more <- true: + default: + } + if self.kad.Count() == 0 { + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: empty, all bees gone", p) + } +} + +// Retrieve a list of live peers that are closer to target than us +func (self *Hive) getPeers(target storage.Key, max int) (peers []*peer) { + var addr kademlia.Address + copy(addr[:], target[:]) + for _, node := range self.kad.FindClosest(addr, max) { + peers = append(peers, node.(*peer)) + } + return +} + +// disconnects all the peers +func (self *Hive) DropAll() { + glog.V(logger.Detail).Infof("[BZZ] KΛÐΞMLIΛ hive: dropping all bees") + for _, node := range self.kad.FindClosest(kademlia.Address{}, 0) { + node.Drop() + } +} + +// contructor for kademlia.NodeRecord based on peer address alone +// TODO: should go away and only addr passed to kademlia +func newNodeRecord(addr *peerAddr) *kademlia.NodeRecord { + now := kademlia.Time(time.Now()) + return &kademlia.NodeRecord{ + Addr: addr.Addr, + Url: addr.String(), + Seen: now, + After: now, + } +} + +// called by the protocol when 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) HandlePeersMsg(req *peersMsgData, from *peer) { + var nrs []*kademlia.NodeRecord + for _, p := range req.Peers { + nrs = append(nrs, newNodeRecord(p)) + } + self.kad.Add(nrs) +} + +// peer wraps the protocol instance to represent a connected peer +// it implements kademlia.Node interface +type peer struct { + *bzz // protocol instance running on peer connection +} + +// protocol instance implements kademlia.Node interface (embedded peer) +func (self *peer) Addr() kademlia.Address { + return self.remoteAddr.Addr +} + +func (self *peer) Url() string { + return self.remoteAddr.String() +} + +// TODO take into account traffic +func (self *peer) LastActive() time.Time { + return time.Now() +} + +// reads the serialised form of sync state persisted as the 'Meta' attribute +// and sets the decoded syncState on the online node +func loadSync(record *kademlia.NodeRecord, node kademlia.Node) error { + if p, ok := node.(*peer); ok { + if record.Meta == nil { + glog.V(logger.Debug).Infof("no sync state for node record %v", record) + return nil + } + state, err := decodeSync(record.Meta) + glog.V(logger.Debug).Infof("sync state for node record %v: %s -> %v", record, string(*(record.Meta)), state) + p.syncState = state + return err + } + return fmt.Errorf("invalid type") +} + +// callback when saving a sync state +func saveSync(record *kademlia.NodeRecord, node kademlia.Node) { + if p, ok := node.(*peer); ok { + meta, err := encodeSync(p.syncState) + if err != nil { + glog.V(logger.Warn).Infof("error saving sync state for %v: %v", node, err) + return + } + glog.V(logger.Warn).Infof("saving sync state for %v: %s", node, string(*meta)) + + record.Meta = meta + } +} + +// the immediate response to a retrieve request, +// sends relevant peer data given by the kademlia hive to the requester +// TODO: remember peers sent for duration of the session, only new peers sent +func (self *Hive) peers(req *retrieveRequestMsgData) { + // FIXME: should check req.MaxPeers but then should not default to zero or make sure we set it when sending retrieveRequests + // we might need chunk.req to cache relevant peers response, + // hive change would expire it + if req != nil && req.MaxPeers >= 0 { + var addrs []*peerAddr + if req.timeout == nil || time.Now().Before(*(req.timeout)) { + key := req.Key + // self lookup from remote peer + if storage.IsZeroKey(key) { + addr := req.from.Addr() + key = storage.Key(addr[:]) + req.Key = nil + } + // get peer addresses from hive + for _, peer := range self.getPeers(key, int(req.MaxPeers)) { + addrs = append(addrs, peer.remoteAddr) + } + glog.V(logger.Detail).Infof("[BZZ] Hive sending %d peer addresses to %v. req.Id: %v, req.Key: %x", len(addrs), req.from, req.Id, req.Key.Log()) + + peersData := &peersMsgData{ + Peers: addrs, + Key: req.Key, + Id: req.Id, + } + peersData.setTimeout(req.timeout) + req.from.peers(peersData) + } + } +} diff --git a/swarm/network/messages.go b/swarm/network/messages.go new file mode 100644 index 0000000000..4e49be4dbf --- /dev/null +++ b/swarm/network/messages.go @@ -0,0 +1,294 @@ +package network + +import ( + "fmt" + "net" + "time" + + "github.com/ethereum/go-ethereum/swarm/services/swap" + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/common/kademlia" +) + +/* +BZZ protocol Message Types and Message Data Types +*/ + +// bzz protocol message codes +const ( + statusMsg = iota // 0x01 + storeRequestMsg // 0x02 + retrieveRequestMsg // 0x03 + peersMsg // 0x04 + syncRequestMsg // 0x05 + deliveryRequestMsg // 0x06 + unsyncedKeysMsg // 0x07 + paymentMsg // 0x08 +) + +/* + Handshake + +* Version: 8 byte integer version of the protocol +* ID: arbitrary byte sequence client identifier human readable +* Addr: the address advertised by the node, format similar to DEVp2p wire protocol +* Swap: info for the swarm accounting protocol +* NetworkID: 8 byte integer network identifier +* Caps: swarm-specific capabilities, format identical to devp2p +* SyncState: syncronisation state (db iterator key and address space etc) persisted about the peer + +*/ +type statusMsgData struct { + Version uint64 + ID string + Addr *peerAddr + Swap *swap.SwapProfile + NetworkId uint64 +} + +func (self *statusMsgData) String() string { + return fmt.Sprintf("Status: Version: %v, ID: %v, Addr: %v, Swap: %v, NetworkId: %v", self.Version, self.ID, self.Addr, self.Swap, self.NetworkId) +} + +/* + store requests are forwarded to the peers in their kademlia 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 proxLimit, i. e., the + last row of the routing table) +*/ +type storeRequestMsgData struct { + Key storage.Key // hash of datasize | data + SData []byte // the actual chunk Data + // optional + Id uint64 // request ID. if delivery, the ID is retrieve request ID + requestTimeout *time.Time // expiry for forwarding - [not serialised][not currently used] + storageTimeout *time.Time // expiry of content - [not serialised][not currently used] + from *peer // [not serialised] protocol registers the requester +} + +func (self storeRequestMsgData) String() string { + var from string + if self.from == nil { + from = "self" + } else { + from = self.from.Addr().String() + } + return fmt.Sprintf("from: %v, Key: %v; ID: %v, requestTimeout: %v, storageTimeout: %v, SData %x", from, self.Key, self.Id, self.requestTimeout, self.storageTimeout, self.SData[:10]) +} + +/* +Retrieve request + +Timeout in milliseconds. Note that zero timeout retrieval requests do not request forwarding, but prompt for a peers message response. therefore they 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. + +Request ID can be newly generated or kept from the request originator. +If request ID Is missing or zero, the request is handled as a lookup only +prompting a peers response but not launching a search. Lookup requests are meant +to be used to bootstrap kademlia tables. + +In the special case that the key is the zero value as well, the remote peer's +address is assumed (the message is to be handled as a self lookup request). +The response is a PeersMsg with the peers in the kademlia proximity bin +corresponding to the address. +*/ + +type retrieveRequestMsgData struct { + Key storage.Key // target Key address of chunk to be retrieved + Id uint64 // request id, request is a lookup if missing or zero + MaxSize uint64 // maximum size of delivery accepted + MaxPeers uint64 // maximum number of peers returned + Timeout uint64 // the longest time we are expecting a response + timeout *time.Time // [not serialied]} + from *peer // +} + +func (self retrieveRequestMsgData) String() string { + var from string + if self.from == nil { + from = "ourselves" + } else { + from = self.from.Addr().String() + } + var target []byte + if len(self.Key) > 3 { + target = self.Key[:4] + } + return fmt.Sprintf("from: %v, Key: %x; ID: %v, MaxSize: %v, MaxPeers: %d", from, target, self.Id, self.MaxSize, self.MaxPeers) +} + +// lookups are encoded by missing request ID +func (self retrieveRequestMsgData) isLookup() bool { + return self.Id == 0 +} + +// sets timeout fields +func (self retrieveRequestMsgData) setTimeout(t *time.Time) { + self.timeout = t + if t != nil { + self.Timeout = uint64(t.UnixNano()) + } else { + self.Timeout = 0 + } +} + +func (self retrieveRequestMsgData) getTimeout() (t *time.Time) { + if self.Timeout > 0 && self.timeout == nil { + timeout := time.Unix(int64(self.Timeout), 0) + t = &timeout + self.timeout = t + } + return +} + +// peerAddr is sent in StatusMsg as part of the handshake +type peerAddr struct { + IP net.IP + Port uint16 + ID []byte // the 64 byte NodeID (ECDSA Public Key) + Addr kademlia.Address +} + +// peerAddr pretty prints as enode +func (self peerAddr) String() string { + return fmt.Sprintf("enode://%x@%v:%d", self.ID, self.IP, self.Port) +} + +/* +peers Msg is one response to retrieval; it is always encouraged after a retrieval +request to respond with a list of peers in the same kademlia 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. + +NodeID serves as the owner of payment contracts and signer of proofs of transfer. + +The Key is the target (if response to a retrieval request) or missing (zero value) +peers address (hash of NodeID) if retrieval request was a self lookup. + +Peers message is requested by retrieval requests with a missing or zero value request ID +*/ +type peersMsgData struct { + Peers []*peerAddr // + Timeout uint64 // + timeout *time.Time // indicate whether responder is expected to deliver content + Key storage.Key // present if a response to a retrieval request + Id uint64 // present if a response to a retrieval request + from *peer +} + +// peers msg pretty printer +func (self peersMsgData) String() string { + var from string + if self.from == nil { + from = "ourselves" + } else { + from = self.from.Addr().String() + } + var target []byte + if len(self.Key) > 3 { + target = self.Key[:4] + } + return fmt.Sprintf("from: %v, Key: %x; ID: %v, Peers: %v", from, target, self.Id, self.Peers) +} + +func (self peersMsgData) setTimeout(t *time.Time) { + self.timeout = t + if t != nil { + self.Timeout = uint64(t.UnixNano()) + } else { + self.Timeout = 0 + } +} + +func (self peersMsgData) getTimeout() (t *time.Time) { + if self.Timeout > 0 && self.timeout == nil { + timeout := time.Unix(int64(self.Timeout), 0) + t = &timeout + self.timeout = t + } + return +} + +/* +syncRequest + +is sent after the handshake to initiate syncing +the syncState of the remote node is persisted in kaddb and set on the +peer/protocol instance when the node is registered by hive as online{ +*/ + +type syncRequestMsgData struct { + SyncState *syncState `rlp:"nil"` +} + +func (self *syncRequestMsgData) String() string { + return fmt.Sprintf("%v", self.SyncState) +} + +/* +deliveryRequest + +is sent once a batch of sync keys is filtered. The ones not found are +sent as a list of syncReuest (hash, priority) in the Deliver field. +When the source receives the sync request it continues to iterate +and fetch at most N items as yet unsynced. +At the same time responds with deliveries of the items. +*/ +type deliveryRequestMsgData struct { + Deliver []*syncRequest +} + +func (self *deliveryRequestMsgData) String() string { + return fmt.Sprintf("sync request for new chunks\ndelivery request for %v chunks", len(self.Deliver)) +} + +/* +unsyncedKeys + +is sent first after the handshake if SyncState iterator brings up hundreds, thousands? +and subsequently sent as a response to deliveryRequestMsgData. + +Syncing is the iterative process of exchanging unsyncedKeys and deliveryRequestMsgs +both ways. + +State contains the sync state sent by the source. When the source receives the +sync state it continues to iterate and fetch at most N items as yet unsynced. +At the same time responds with deliveries of the items. +*/ +type unsyncedKeysMsgData struct { + Unsynced []*syncRequest + State *syncState +} + +func (self *unsyncedKeysMsgData) String() string { + return fmt.Sprintf("sync: keys of %d new chunks (state %v) => synced: %v", len(self.Unsynced), self.State, self.State.Synced) +} + +/* +payment + +is sent when the swap balance is tilted in favour of the remote peer +and in absolute units exceeds the PayAt parameter in the remote peer's profile +*/ + +type paymentMsgData struct { + Units uint // units actually paid for (checked against amount by swap) + Promise *chequebook.Cheque // payment with cheque +} + +func (self *paymentMsgData) String() string { + return fmt.Sprintf("payment for %d units: %v", self.Units, self.Promise) +} diff --git a/swarm/network/protocol.go b/swarm/network/protocol.go new file mode 100644 index 0000000000..ccf5f119ef --- /dev/null +++ b/swarm/network/protocol.go @@ -0,0 +1,505 @@ +package network + +/* +BZZ implements the bzz wire protocol of swarm +the protocol instance is launched on each peer by the network layer if the +BZZ protocol handler is registered on the p2p server. + +The protocol takes care of actually communicating the bzz protocol +* encoding and decoding requests for storage and retrieval +* handling the s§protocol handshake +* dispaching to netstore for handling the DHT logic +* registering peers in the KΛÐΞMLIΛ table via the hive logistic manager +* handling sync protocol messages via the syncer +* talks the SWAP payent protocol (swap accounting is done within NetStore) +*/ + +import ( + "fmt" + "net" + "strconv" + + bzzswap "github.com/ethereum/go-ethereum/swarm/services/swap" + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/common/swap" + "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" +) + +const ( + Version = 0 + ProtocolLength = uint64(8) + ProtocolMaxMsgSize = 10 * 1024 * 1024 + NetworkId = 322 +) + +const ( + ErrMsgTooLarge = iota + ErrDecode + ErrInvalidMsgCode + ErrVersionMismatch + ErrNetworkIdMismatch + ErrNoStatusMsg + ErrExtraStatusMsg + ErrSwap + ErrSync +) + +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", + ErrSwap: "SWAP error", + ErrSync: "Sync error", +} + +// bzz represents the swarm wire protocol +// an instance is running on each peer +type bzz struct { + selfID discover.NodeID // peer's node id used in peer advertising in handshake + key storage.Key // baseaddress as storage.Key + storage StorageHandler // handler storage/retrieval related requests coming via the bzz wire protocol + hive *Hive // the logistic manager, peerPool, routing servicec and peer handler + dbAccess *DbAccess // access to db storage counter and iterator for syncing + requestDb *storage.LDBDatabase // db to persist backlog of deliveries to aid syncing + remoteAddr *peerAddr // remote peers address + peer *p2p.Peer // the p2p peer object + rw p2p.MsgReadWriter // messageReadWriter to send messages to + errors *errs.Errors // errors table + + swap *swap.Swap // swap instance for the peer connection + swapParams *bzzswap.SwapParams // swap settings both local and remote + swapEnabled bool // flag to switch off SWAP (will be via Caps in handshake) + syncer *syncer // syncer instance for the peer connection + syncParams *SyncParams // syncer params + syncState *syncState // outgoing syncronisation state (contains reference to remote peers db counter) + syncEnabled bool // flag to enable syncing +} + +// interface type for handler of storage/retrieval related requests coming +// via the bzz wire protocol +// messages: UnsyncedKeys, DeliveryRequest, StoreRequest, RetrieveRequest +type StorageHandler interface { + HandleUnsyncedKeysMsg(req *unsyncedKeysMsgData, p *peer) error + HandleDeliveryRequestMsg(req *deliveryRequestMsgData, p *peer) error + HandleStoreRequestMsg(req *storeRequestMsgData, p *peer) + HandleRetrieveRequestMsg(req *retrieveRequestMsgData, p *peer) +} + +/* +main entrypoint, wrappers starting a server that will run the bzz protocol +use this constructor to attach the protocol ("class") to server caps +This is done by node.Node#Register(func(node.ServiceContext) (Service, error)) +Service implements Protocols() which is an array of protocol constructors +at node startup the protocols are initialised +the Dev p2p layer then calls Run(p *p2p.Peer, rw p2p.MsgReadWriter) error +on each peer connection +The Run function of the Bzz protocol class creates a bzz instance +which will represent the peer for the swarm hive and all peer-aware components +*/ +func Bzz(cloud StorageHandler, hive *Hive, dbaccess *DbAccess, sp *bzzswap.SwapParams, sy *SyncParams) (p2p.Protocol, error) { + + // a single global request db is created for all peer connections + // this is to persist delivery backlog and aid syncronisation + requestDb, err := storage.NewLDBDatabase(sy.RequestDbPath) + if err != nil { + return p2p.Protocol{}, fmt.Errorf("error setting up request db: %v", err) + } + + return p2p.Protocol{ + Name: "bzz", + Version: Version, + Length: ProtocolLength, + Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + return run(requestDb, cloud, hive, dbaccess, sp, sy, p, rw) + }, + }, nil +} + +/* +the main protocol loop that + * does the handshake by exchanging statusMsg + * if peer is valid and accepted, registers with the hive + * then enters into a forever loop handling incoming messages + * storage and retrieval related queries coming via bzz are dispatched to StorageHandler + * peer-related messages are dispatched to the hive + * payment related messages are relayed to SWAP service + * on disconnect, unregister the peer in the hive (note RemovePeer in the post-disconnect hook) + * whenever the loop terminates, the peer will disconnect with Subprotocol error + * whenever handlers return an error the loop terminates +*/ +func run(requestDb *storage.LDBDatabase, depo StorageHandler, hive *Hive, dbaccess *DbAccess, sp *bzzswap.SwapParams, sy *SyncParams, p *p2p.Peer, rw p2p.MsgReadWriter) (err error) { + + self := &bzz{ + storage: depo, + hive: hive, + dbAccess: dbaccess, + requestDb: requestDb, + peer: p, + rw: rw, + errors: &errs.Errors{ + Package: "BZZ", + Errors: errorToString, + }, + swapParams: sp, + syncParams: sy, + swapEnabled: true, + syncEnabled: true, + } + + // handle handshake + err = self.handleStatus() + if err != nil { + return err + } + defer func() { + // if the handler loop exits, the peer is disconnecting + // deregister the peer in the hive + self.hive.removePeer(&peer{bzz: self}) + if self.syncer != nil { + self.syncer.stop() // quits request db and delivery loops, save requests + } + if self.swap != nil { + self.swap.Stop() // quits chequebox autocash etc + } + }() + + // the main forever loop that handles incoming requests + for { + err = self.handle() + if err != nil { + return + } + } + return +} + +// may need to implement protocol drop only? don't want to kick off the peer +// if they are useful for other protocols +func (self *bzz) Drop() { + self.peer.Disconnect(p2p.DiscSubprotocolError) +} + +// one cycle of the main forever loop that handles and dispatches incoming messages +func (self *bzz) handle() error { + msg, err := self.rw.ReadMsg() + glog.V(logger.Debug).Infof("[BZZ] <- %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() + + switch msg.Code { + + case statusMsg: + // no extra status message allowed. The one needed already handled by + // handleStatus + glog.V(logger.Debug).Infof("[BZZ] Status message: %v", msg) + return self.protoError(ErrExtraStatusMsg, "") + + case storeRequestMsg: + // store requests are dispatched to netStore + var req storeRequestMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "msg %v: %v", msg, err) + } + glog.V(logger.Debug).Infof("[BZZ] incoming store request: %s", req.String()) + // swap accounting is done within forwarding + self.storage.HandleStoreRequestMsg(&req, &peer{bzz: self}) + + case retrieveRequestMsg: + // retrieve Requests are dispatched to netStore + var req retrieveRequestMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + req.from = &peer{bzz: self} + // if request is lookup and not to be delivered + if req.isLookup() { + glog.V(logger.Detail).Infof("[BZZ] self lookup for %v: responding with peers only...", req.from) + } else if req.Key == nil { + return self.protoError(ErrDecode, "protocol handler: req.Key == nil || req.Timeout == nil") + } else { + // swap accounting is done within netStore + self.storage.HandleRetrieveRequestMsg(&req, &peer{bzz: self}) + } + // direct response with peers, TODO: sort this out + self.hive.peers(&req) + + case peersMsg: + // response to lookups and immediate response to retrieve requests + // dispatches new peer data to the hive that adds them to KADDB + var req peersMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + req.from = &peer{bzz: self} + glog.V(logger.Debug).Infof("[BZZ] incoming peer addresses: %v", req) + self.hive.HandlePeersMsg(&req, &peer{bzz: self}) + + case syncRequestMsg: + var req syncRequestMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + glog.V(logger.Debug).Infof("[BZZ] sync request received: %v", req) + self.sync(req.SyncState) + + case unsyncedKeysMsg: + // coming from parent node offering + var req unsyncedKeysMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + glog.V(logger.Debug).Infof("[BZZ] incoming unsynced keys msg: %s", req.String()) + err := self.storage.HandleUnsyncedKeysMsg(&req, &peer{bzz: self}) + if err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + // set peers state to persist + self.syncState = req.State + + case deliveryRequestMsg: + // response to syncKeysMsg hashes filtered not existing in db + // also relays the last synced state to the source + var req deliveryRequestMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + glog.V(logger.Debug).Infof("[BZZ] incoming delivery request: %s", req.String()) + err := self.storage.HandleDeliveryRequestMsg(&req, &peer{bzz: self}) + if err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + + case paymentMsg: + // swap protocol message for payment, Units paid for, Cheque paid with + var req paymentMsgData + if err := msg.Decode(&req); err != nil { + return self.protoError(ErrDecode, "->msg %v: %v", msg, err) + } + glog.V(logger.Debug).Infof("[BZZ] incoming payment: %s", req.String()) + self.swap.Receive(int(req.Units), req.Promise) + + default: + // no other message is allowed + return self.protoError(ErrInvalidMsgCode, "%v", msg.Code) + } + return nil +} + +func (self *bzz) handleStatus() (err error) { + + handshake := &statusMsgData{ + Version: uint64(Version), + ID: "honey", + Addr: self.selfAddr(), + NetworkId: uint64(NetworkId), + Swap: &bzzswap.SwapProfile{ + Profile: self.swapParams.Profile, + PayProfile: self.swapParams.PayProfile, + }, + } + + err = p2p.Send(self.rw, statusMsg, handshake) + if err != nil { + self.protoError(ErrNoStatusMsg, err.Error()) + } + + // read and handle remote status + var msg p2p.Msg + msg, err = self.rw.ReadMsg() + if err != nil { + return err + } + + if msg.Code != statusMsg { + 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) + } + + self.remoteAddr = self.peerAddr(status.Addr) + glog.V(logger.Detail).Infof("[BZZ] self: advertised IP: %v, peer advertised: %v, local address: %v\npeer: advertised IP: %v, remote address: %v\n", self.selfAddr(), self.remoteAddr, self.peer.LocalAddr(), status.Addr.IP, self.peer.RemoteAddr()) + + if self.swapEnabled { + // set remote profile for accounting + self.swap, err = bzzswap.NewSwap(self.swapParams, status.Swap, self) + if err != nil { + return self.protoError(ErrSwap, "%v", err) + } + } + + glog.V(logger.Info).Infof("[BZZ] Peer %08x is [bzz] capable (%d/%d)", self.remoteAddr.Addr[:4], status.Version, status.NetworkId) + self.hive.addPeer(&peer{bzz: self}) + + // hive sets syncstate so sync should start after node added + if self.syncEnabled { + glog.V(logger.Info).Infof("[BZZ] syncronisation request sent with %v", self.syncState) + self.syncRequest() + } + return nil +} + +func (self *bzz) sync(state *syncState) error { + // syncer setup + if self.syncer != nil { + return self.protoError(ErrSync, "sync request can only be sent once") + } + + cnt := self.dbAccess.counter() + remoteaddr := self.remoteAddr.Addr + start, stop := self.hive.kad.KeyRange(remoteaddr) + if state == nil { + state = newSyncState(start, stop, cnt) + glog.V(logger.Warn).Infof("[BZZ] peer %08x provided no sync state, setting up full sync: %v\n", remoteaddr[:4], state) + } else { + state.synced = make(chan bool) + state.SessionAt = cnt + if storage.IsZeroKey(state.Stop) && state.Synced { + state.Start = storage.Key(start[:]) + state.Stop = storage.Key(stop[:]) + } + } + var err error + self.syncer, err = newSyncer( + self.requestDb, + storage.Key(remoteaddr[:]), + self.dbAccess, + self.unsyncedKeys, self.store, + self.syncParams, state, + ) + if err != nil { + return self.protoError(ErrSync, "%v", err) + } + return nil +} + +func (self *bzz) String() string { + return self.remoteAddr.String() +} + +// repair reported address if IP missing +func (self *bzz) peerAddr(base *peerAddr) *peerAddr { + if base.IP.IsUnspecified() { + host, _, _ := net.SplitHostPort(self.peer.RemoteAddr().String()) + base.IP = net.ParseIP(host) + } + return base +} + +// returns self advertised node connection info (listening address w enodes) +// IP will get repaired on the other end if missing +// or resolved via ID by discovery at dialout +func (self *bzz) selfAddr() *peerAddr { + id := self.hive.id + host, port, _ := net.SplitHostPort(self.hive.listenAddr()) + intport, _ := strconv.Atoi(port) + addr := &peerAddr{ + Addr: self.hive.addr, + ID: id[:], + IP: net.ParseIP(host), + Port: uint16(intport), + } + return addr +} + +// outgoing messages +// send retrieveRequestMsg +func (self *bzz) retrieve(req *retrieveRequestMsgData) error { + return self.send(retrieveRequestMsg, req) +} + +// send storeRequestMsg +func (self *bzz) store(req *storeRequestMsgData) error { + return self.send(storeRequestMsg, req) +} + +func (self *bzz) syncRequest() error { + req := &syncRequestMsgData{ + SyncState: self.syncState, + } + return self.send(syncRequestMsg, req) +} + +// queue storeRequestMsg in request db +func (self *bzz) deliveryRequest(reqs []*syncRequest) error { + req := &deliveryRequestMsgData{ + Deliver: reqs, + } + return self.send(deliveryRequestMsg, req) +} + +// batch of syncRequests to send off +func (self *bzz) unsyncedKeys(reqs []*syncRequest, state *syncState) error { + req := &unsyncedKeysMsgData{ + Unsynced: reqs, + State: state, + } + return self.send(unsyncedKeysMsg, req) +} + +// send paymentMsg +func (self *bzz) Pay(units int, promise swap.Promise) { + req := &paymentMsgData{uint(units), promise.(*chequebook.Cheque)} + self.payment(req) +} + +// send paymentMsg +func (self *bzz) payment(req *paymentMsgData) error { + return self.send(paymentMsg, req) +} + +// sends peersMsg +func (self *bzz) peers(req *peersMsgData) error { + return self.send(peersMsg, req) +} + +func (self *bzz) 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 *bzz) protoErrorDisconnect(err *errs.Error) { + err.Log(glog.V(logger.Info)) + if err.Fatal() { + self.peer.Disconnect(p2p.DiscSubprotocolError) + } +} + +func (self *bzz) send(msg uint64, data interface{}) error { + glog.V(logger.Debug).Infof("[BZZ] -> %v: %v (%T) to %v", msg, data, data, self) + err := p2p.Send(self.rw, msg, data) + if err != nil { + self.Drop() + } + return err +} diff --git a/swarm/network/protocol_test.go b/swarm/network/protocol_test.go new file mode 100644 index 0000000000..1ae2e9d505 --- /dev/null +++ b/swarm/network/protocol_test.go @@ -0,0 +1 @@ +package network diff --git a/swarm/network/syncdb.go b/swarm/network/syncdb.go new file mode 100644 index 0000000000..39d5f1bffc --- /dev/null +++ b/swarm/network/syncdb.go @@ -0,0 +1,371 @@ +package network + +import ( + "encoding/binary" + "fmt" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" +) + +const counterKeyPrefix = 0x01 + +/* +syncDb is a queueing service for outgoing deliveries. +One instance per priority queue for each peer + +a syncDb instance maintains an in-memory buffer (of capacity bufferSize) +once its in-memory buffer is full it switches to persisting in db +and dbRead iterator iterates through the items keeping their order +once the db read catches up (there is no more items in the db) then +it switches back to in-memory buffer. + +when syncdb is stopped all items in the buffer are saved to the db +*/ +type syncDb struct { + start []byte // this syncdb starting index in requestdb + key storage.Key // remote peers address key + counterKey []byte // db key to persist counter + priority uint // priotity High|Medium|Low + buffer chan interface{} // incoming request channel + db *storage.LDBDatabase // underlying db (TODO should be interface) + done chan bool // chan to signal goroutines finished quitting + quit chan bool // chan to signal quitting to goroutines + total, dbTotal int // counts for one session + batch chan chan int // channel for batch requests + dbBatchSize uint // number of items before batch is saved +} + +// constructor needs a shared request db (leveldb) +// priority is used in the index key +// uses a buffer and a leveldb for persistent storage +// bufferSize, dbBatchSize are config parameters +func newSyncDb(db *storage.LDBDatabase, key storage.Key, priority uint, bufferSize, dbBatchSize uint, deliver func(interface{}, chan bool) bool) *syncDb { + start := make([]byte, 42) + start[1] = byte(priorities - priority) + copy(start[2:34], key) + + counterKey := make([]byte, 34) + counterKey[0] = counterKeyPrefix + copy(counterKey[1:], start[1:34]) + + syncdb := &syncDb{ + start: start, + key: key, + counterKey: counterKey, + priority: priority, + buffer: make(chan interface{}, bufferSize), + db: db, + done: make(chan bool), + quit: make(chan bool), + batch: make(chan chan int), + dbBatchSize: dbBatchSize, + } + glog.V(logger.Debug).Infof("[BZZ] syncDb[peer: %v, priority: %v] - initialised", key.Log(), priority) + + // starts the main forever loop reading from buffer + go syncdb.bufferRead(deliver) + return syncdb +} + +/* +bufferRead is a forever iterator loop that takes care of delivering +outgoing store requests reads from incoming buffer + +its argument is the deliver function taking the item as first argument +and a quit channel as second. +Closing of this channel is supposed to abort all waiting for delivery +(typically network write) + +The iteration switches between 2 modes, +* buffer mode reads the in-memory buffer and delivers the items directly +* db mode reads from the buffer and writes to the db, parallelly another +routine is started that reads from the db and delivers items + +If there is buffer contention in buffer mode (slow network, high upload volume) +syncdb switches to db mode and starts dbRead +Once db backlog is delivered, it reverts back to in-memory buffer + +It is automatically started when syncdb is initialised. + +It saves the buffer to db upon receiving quit signal. syncDb#stop() +*/ +func (self *syncDb) bufferRead(deliver func(interface{}, chan bool) bool) { + var buffer, db chan interface{} // channels representing the two read modes + var more bool + var req interface{} + var entry *syncDbEntry + var inBatch, inDb int + batch := new(leveldb.Batch) + var dbSize chan int + quit := self.quit + counterValue := make([]byte, 8) + + // counter is used for keeping the items in order, persisted to db + // start counter where db was at, 0 if not found + data, err := self.db.Get(self.counterKey) + var counter uint64 + if err == nil { + counter = binary.BigEndian.Uint64(data) + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v/%v] - counter read from db at %v", self.key.Log(), self.priority, counter) + } else { + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v/%v] - counter starts at %v", self.key.Log(), self.priority, counter) + } + +LOOP: + for { + // waiting for item next in the buffer, or quit signal or batch request + select { + // buffer only closes when writing to db + case req = <-buffer: + // deliver request : this is blocking on network write so + // it is passed the quit channel as argument, so that it returns + // if syncdb is stopped. In this case we need to save the item to the db + more = deliver(req, self.quit) + if !more { + glog.V(logger.Debug).Infof("[BZZ] syncDb[%v] quit: switching to db. session tally (db/total): %v/%v", self.priority, self.dbTotal, self.total) + // received quit signal, save request currently waiting delivery + // by switching to db mode and closing the buffer + buffer = nil + db = self.buffer + close(db) + quit = nil // needs to block the quit case in select + break // break from select, this item will be written to the db + } + self.total++ + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] deliver (db/total): %v/%v", self.priority, self.dbTotal, self.total) + // by the time deliver returns, there were new writes to the buffer + // if buffer contention is detected, switch to db mode which drains + // the buffer so no process will block on pushing store requests + if len(buffer) == cap(buffer) { + glog.V(logger.Debug).Infof("[BZZ] syncDb[%v] buffer full %v: switching to db. session tally (db/total): %v/%v", self.priority, cap(buffer), self.dbTotal, self.total) + buffer = nil + db = self.buffer + } + continue LOOP + + // incoming entry to put into db + case req, more = <-db: + if !more { + // only if quit is called, saved all the buffer + binary.BigEndian.PutUint64(counterValue, counter) + batch.Put(self.counterKey, counterValue) // persist counter in batch + self.writeSyncBatch(batch) // save batch + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] quitting: save current batch to db", self.priority) + break LOOP + } + self.dbTotal++ + self.total++ + // otherwise break after select + + case dbSize = <-self.batch: + // explicit request for batch + if inBatch == 0 && quit != nil { + // there was no writes since the last batch so db depleted + // switch to buffer mode + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] empty db: switching to buffer", self.priority) + db = nil + buffer = self.buffer + dbSize <- 0 // indicates to 'caller' that batch has been written + inDb = 0 + continue LOOP + } + binary.BigEndian.PutUint64(counterValue, counter) + batch.Put(self.counterKey, counterValue) + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] write batch %v/%v - %x - %x", self.priority, inBatch, counter, self.counterKey, counterValue) + batch = self.writeSyncBatch(batch) + dbSize <- inBatch + inBatch = 0 + continue LOOP + + // closing syncDb#quit channel is used to signal to all goroutines to quit + case <-quit: + // need to save backlog, so switch to db mode + db = self.buffer + buffer = nil + quit = nil + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] quitting: save buffer to db", self.priority) + close(db) + continue LOOP + } + + // only get here if we put req into db + entry, err = self.newSyncDbEntry(req, counter) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] syncDb[%v] saving request %v (#%v/%v) failed: %v", self.priority, req, inBatch, inDb, err) + continue LOOP + } + batch.Put(entry.key, entry.val) + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] to batch %v '%v' (#%v/%v/%v)", self.priority, req, entry, inBatch, inDb, counter) + // if just switched to db mode and not quitting, then launch dbRead + // in a parallel go routine to send deliveries from db + if inDb == 0 && quit != nil { + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] start dbRead") + go self.dbRead(true, counter, deliver) + } + inDb++ + inBatch++ + counter++ + // need to save the batch if it gets too large (== dbBatchSize) + if inBatch%int(self.dbBatchSize) == 0 { + batch = self.writeSyncBatch(batch) + } + } + glog.V(logger.Info).Infof("[BZZ] syncDb[%v:%v]: saved %v keys (saved counter at %v)", self.key.Log(), self.priority, inBatch, counter) + close(self.done) +} + +// writes the batch to the db and returns a new batch object +func (self *syncDb) writeSyncBatch(batch *leveldb.Batch) *leveldb.Batch { + err := self.db.Write(batch) + if err != nil { + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] saving batch to db failed: %v", self.priority, err) + return batch + } + return new(leveldb.Batch) +} + +// abstract type for db entries (TODO could be a feature of Receipts) +type syncDbEntry struct { + key, val []byte +} + +func (self syncDbEntry) String() string { + return fmt.Sprintf("key: %x, value: %x", self.key, self.val) +} + +/* + dbRead is iterating over store requests to be sent over to the peer + this is mainly to prevent crashes due to network output buffer contention (???) + as well as to make syncronisation resilient to disconnects + the messages are supposed to be sent in the p2p priority queue. + + the request DB is shared between peers, but domains for each syncdb + are disjoint. dbkeys (42 bytes) are structured: + * 0: 0x00 (0x01 reserved for counter key) + * 1: priorities - priority (so that high priority can be replayed first) + * 2-33: peers address + * 34-41: syncdb counter to preserve order (this field is missing for the counter key) + + values (40 bytes) are: + * 0-31: key + * 32-39: request id + +dbRead needs a boolean to indicate if on first round all the historical +record is synced. Second argument to indicate current db counter +The third is the function to apply +*/ +func (self *syncDb) dbRead(useBatches bool, counter uint64, fun func(interface{}, chan bool) bool) { + key := make([]byte, 42) + copy(key, self.start) + binary.BigEndian.PutUint64(key[34:], counter) + var batches, n, cnt, total int + var more bool + var entry *syncDbEntry + var it iterator.Iterator + var del *leveldb.Batch + batchSizes := make(chan int) + + for { + // if useBatches is false, cnt is not set + if useBatches { + // this could be called before all cnt items sent out + // so that loop is not blocking while delivering + // only relevant if cnt is large + self.batch <- batchSizes + // wait for the write to finish and get the item count in the next batch + cnt = <-batchSizes + batches++ + if cnt == 0 { + // empty + return + } + } + it = self.db.NewIterator() + it.Seek(key) + if !it.Valid() { + copy(key, self.start) + useBatches = true + continue + } + del = new(leveldb.Batch) + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v]: new iterator: %x (batch %v, count %v)", self.priority, key, batches, cnt) + + for n = 0; !useBatches || n < cnt; it.Next() { + copy(key, it.Key()) + if len(key) == 0 || key[0] != 0 { + copy(key, self.start) + useBatches = true + break + } + val := make([]byte, 40) + copy(val, it.Value()) + entry = &syncDbEntry{key, val} + // glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] - %v, batches: %v, total: %v, session total from db: %v/%v", self.priority, self.key.Log(), batches, total, self.dbTotal, self.total) + more = fun(entry, self.quit) + if !more { + // quit received when waiting to deliver entry, the entry will not be deleted + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v] batch %v quit after %v/%v items", self.priority, batches, n, cnt) + break + } + // since subsequent batches of the same db session are indexed incrementally + // deleting earlier batches can be delayed and parallelised + // this could be batch delete when db is idle (but added complexity esp when quitting) + del.Delete(key) + n++ + total++ + } + glog.V(logger.Detail).Infof("[BZZ] syncDb[%v/%v] - db session closed, batches: %v, total: %v, session total from db: %v/%v", self.key.Log(), self.priority, batches, total, self.dbTotal, self.total) + self.db.Write(del) // this could be async called only when db is idle + it.Release() + } +} + +// +func (self *syncDb) stop() { + close(self.quit) + <-self.done +} + +// calculate a dbkey for the request, for the db to work +// see syncdb for db key structure +// polimorphic: accepted types, see syncer#addRequest +func (self *syncDb) newSyncDbEntry(req interface{}, counter uint64) (entry *syncDbEntry, err error) { + var key storage.Key + var chunk *storage.Chunk + var id uint64 + var ok bool + var sreq *storeRequestMsgData + + if key, ok = req.(storage.Key); ok { + id = generateId() + } else if chunk, ok = req.(*storage.Chunk); ok { + key = chunk.Key + id = generateId() + } else if sreq, ok = req.(*storeRequestMsgData); ok { + key = sreq.Key + id = sreq.Id + } else if entry, ok = req.(*syncDbEntry); !ok { + return nil, fmt.Errorf("type not allowed: %v (%T)", req, req) + } + + // order by peer > priority > seqid + // value is request id if exists + if entry == nil { + dbkey := make([]byte, 42) + dbval := make([]byte, 40) + + // encode key + copy(dbkey[:], self.start[:34]) // db peer + binary.BigEndian.PutUint64(dbkey[34:], counter) + // encode value + copy(dbval, key[:]) + binary.BigEndian.PutUint64(dbval[32:], id) + + entry = &syncDbEntry{dbkey, dbval} + } + return +} diff --git a/swarm/network/syncdb_test.go b/swarm/network/syncdb_test.go new file mode 100644 index 0000000000..33c50df262 --- /dev/null +++ b/swarm/network/syncdb_test.go @@ -0,0 +1,205 @@ +package network + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +func init() { + glog.SetV(0) + glog.SetToStderr(true) +} + +type testSyncDb struct { + *syncDb + c int + t *testing.T + fromDb chan bool + delivered [][]byte + sent []int + dbdir string + at int +} + +func newTestSyncDb(priority, bufferSize, batchSize int, dbdir string, t *testing.T) *testSyncDb { + if len(dbdir) == 0 { + tmp, err := ioutil.TempDir(os.TempDir(), "syncdb-test") + if err != nil { + t.Fatalf("unable to create temporary direcory %v: %v", tmp, err) + } + dbdir = tmp + } + db, err := storage.NewLDBDatabase(filepath.Join(dbdir, "requestdb")) + if err != nil { + t.Fatalf("unable to create db: %v", err) + } + self := &testSyncDb{ + fromDb: make(chan bool), + dbdir: dbdir, + t: t, + } + h := crypto.Sha3Hash([]byte{0}) + key := storage.Key(h[:]) + self.syncDb = newSyncDb(db, key, uint(priority), uint(bufferSize), uint(batchSize), self.deliver) + // kick off db iterator right away, if no items on db this will allow + // reading from the buffer + return self + +} + +func (self *testSyncDb) close() { + self.db.Close() + os.RemoveAll(self.dbdir) +} + +func (self *testSyncDb) push(n int) { + for i := 0; i < n; i++ { + self.buffer <- storage.Key(crypto.Sha3([]byte{byte(self.c)})) + self.sent = append(self.sent, self.c) + self.c++ + } + glog.V(logger.Debug).Infof("pushed %v requests", n) +} + +func (self *testSyncDb) draindb() { + it := self.db.NewIterator() + defer it.Release() + for { + it.Seek(self.start) + if !it.Valid() { + return + } + k := it.Key() + if len(k) == 0 || k[0] == 1 { + return + } + it.Release() + it = self.db.NewIterator() + } +} + +func (self *testSyncDb) deliver(req interface{}, quit chan bool) bool { + _, db := req.(*syncDbEntry) + key, _, _, _, err := parseRequest(req) + if err != nil { + self.t.Fatalf("unexpected error of key %v: %v", key, err) + } + self.delivered = append(self.delivered, key) + select { + case self.fromDb <- db: + return true + case <-quit: + return false + } +} + +func (self *testSyncDb) expect(n int, db bool) { + var ok bool + // for n items + for i := 0; i < n; i++ { + ok = <-self.fromDb + if self.at+1 > len(self.delivered) { + self.t.Fatalf("expected %v, got %v", self.at+1, len(self.delivered)) + } + if len(self.sent) > self.at && !bytes.Equal(crypto.Sha3([]byte{byte(self.sent[self.at])}), self.delivered[self.at]) { + self.t.Fatalf("expected delivery %v/%v/%v to be hash of %v, from db: %v = %v", i, n, self.at, self.sent[self.at], ok, db) + glog.V(logger.Debug).Infof("%v/%v/%v to be hash of %v, from db: %v = %v", i, n, self.at, self.sent[self.at], ok, db) + } + if !ok && db { + self.t.Fatalf("expected delivery %v/%v/%v from db", i, n, self.at) + } + if ok && !db { + self.t.Fatalf("expected delivery %v/%v/%v from cache", i, n, self.at) + } + self.at++ + } +} + +func TestSyncDb(t *testing.T) { + priority := High + bufferSize := 5 + batchSize := 2 * bufferSize + s := newTestSyncDb(priority, bufferSize, batchSize, "", t) + defer s.close() + defer s.stop() + s.dbRead(false, 0, s.deliver) + s.draindb() + + s.push(4) + s.expect(1, false) + // 3 in buffer + time.Sleep(100 * time.Millisecond) + s.push(3) + // push over limit + s.expect(1, false) + // one popped from the buffer, then contention detected + s.expect(4, true) + s.push(4) + s.expect(5, true) + // depleted db, switch back to buffer + s.draindb() + s.push(5) + s.expect(4, false) + s.push(3) + s.expect(4, false) + // buffer depleted + time.Sleep(100 * time.Millisecond) + s.push(6) + s.expect(1, false) + // push into buffer full, switch to db + s.expect(5, true) + s.draindb() + s.push(1) + s.expect(1, false) +} + +func TestSaveSyncDb(t *testing.T) { + amount := 30 + priority := High + bufferSize := amount + batchSize := 10 + s := newTestSyncDb(priority, bufferSize, batchSize, "", t) + go s.dbRead(false, 0, s.deliver) + s.push(amount) + s.stop() + s.db.Close() + + s = newTestSyncDb(priority, bufferSize, batchSize, s.dbdir, t) + go s.dbRead(false, 0, s.deliver) + s.expect(amount, true) + for i, key := range s.delivered { + expKey := crypto.Sha3([]byte{byte(i)}) + if !bytes.Equal(key, expKey) { + t.Fatalf("delivery %v expected to be key %x, got %x", i, expKey, key) + } + } + s.push(amount) + s.expect(amount, false) + for i := amount; i < 2*amount; i++ { + key := s.delivered[i] + expKey := crypto.Sha3([]byte{byte(i - amount)}) + if !bytes.Equal(key, expKey) { + t.Fatalf("delivery %v expected to be key %x, got %x", i, expKey, key) + } + } + s.stop() + s.db.Close() + + s = newTestSyncDb(priority, bufferSize, batchSize, s.dbdir, t) + defer s.close() + defer s.stop() + + go s.dbRead(false, 0, s.deliver) + s.push(1) + s.expect(1, false) + +} diff --git a/swarm/network/syncer.go b/swarm/network/syncer.go new file mode 100644 index 0000000000..70c7e433ab --- /dev/null +++ b/swarm/network/syncer.go @@ -0,0 +1,762 @@ +package network + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "path/filepath" + + "github.com/ethereum/go-ethereum/common/kademlia" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +// syncer parameters (global, not peer specific) default values +const ( + requestDbBatchSize = 512 // size of batch before written to request db + keyBufferSize = 1024 // size of buffer for unsynced keys + syncBatchSize = 128 // maximum batchsize for outgoing requests + syncBufferSize = 128 // size of buffer for delivery requests + syncCacheSize = 1024 // cache capacity to store request queue in memory +) + +// priorities +const ( + Low = iota // 0 + Medium // 1 + High // 2 + priorities // 3 number of priority levels +) + +// request types +const ( + DeliverReq = iota // 0 + PushReq // 1 + PropagateReq // 2 + HistoryReq // 3 + BacklogReq // 4 +) + +// json serialisable struct to record the syncronisation state between 2 peers +type syncState struct { + *storage.DbSyncState // embeds the following 4 fields: + // Start Key // lower limit of address space + // Stop Key // upper limit of address space + // First uint64 // counter taken from last sync state + // Last uint64 // counter of remote peer dbStore at the time of last connection + SessionAt uint64 // set at the time of connection + LastSeenAt uint64 // set at the time of connection + Latest storage.Key // cursor of dbstore when last (continuously set by syncer) + Synced bool // true iff Sync is done up to the last disconnect + synced chan bool // signal that sync stage finished +} + +// wrapper of db-s to provide mockable custom local chunk store access to syncer +type DbAccess struct { + db *storage.DbStore + loc *storage.LocalStore +} + +func NewDbAccess(loc *storage.LocalStore) *DbAccess { + return &DbAccess{loc.DbStore.(*storage.DbStore), loc} +} + +// to obtain the chunks from key or request db entry only +func (self *DbAccess) get(key storage.Key) (*storage.Chunk, error) { + return self.loc.Get(key) +} + +// current storage counter of chunk db +func (self *DbAccess) counter() uint64 { + return self.db.Counter() +} + +// implemented by dbStoreSyncIterator +type keyIterator interface { + Next() storage.Key +} + +// generator function for iteration by address range and storage counter +func (self *DbAccess) iterator(s *syncState) keyIterator { + it, err := self.db.NewSyncIterator(*(s.DbSyncState)) + if err != nil { + return nil + } + return keyIterator(it) +} + +func (self syncState) String() string { + if self.Synced { + return fmt.Sprintf( + "session started at: %v, last seen at: %v, latest key: %v", + self.SessionAt, self.LastSeenAt, + self.Latest.Log(), + ) + } else { + return fmt.Sprintf( + "address: %v-%v, index: %v-%v, session started at: %v, last seen at: %v, latest key: %v", + self.Start.Log(), self.Stop.Log(), + self.First, self.Last, + self.SessionAt, self.LastSeenAt, + self.Latest.Log(), + ) + } +} + +// syncer parameters (global, not peer specific) +type SyncParams struct { + RequestDbPath string // path for request db (leveldb) + RequestDbBatchSize uint // nuber of items before batch is saved to requestdb + KeyBufferSize uint // size of key buffer + SyncBatchSize uint // maximum batchsize for outgoing requests + SyncBufferSize uint // size of buffer for + SyncCacheSize uint // cache capacity to store request queue in memory + SyncPriorities []uint // list of priority levels for req types 0-3 + SyncModes []bool // list of sync modes for for req types 0-3 +} + +// constructor with default values +func NewSyncParams(bzzdir string) *SyncParams { + return &SyncParams{ + RequestDbPath: filepath.Join(bzzdir, "requests"), + RequestDbBatchSize: requestDbBatchSize, + KeyBufferSize: keyBufferSize, + SyncBufferSize: syncBufferSize, + SyncBatchSize: syncBatchSize, + SyncCacheSize: syncCacheSize, + SyncPriorities: []uint{High, Medium, Medium, Low, Low}, + SyncModes: []bool{true, true, true, true, false}, + } +} + +// syncer is the agent that manages content distribution/storage replication/chunk storeRequest forwarding +type syncer struct { + *SyncParams // sync parameters + key storage.Key // remote peers address key + state *syncState // sync state for our dbStore + syncStates chan *syncState // different stages of sync + deliveryRequest chan bool // one of two triggers needed to send unsyncedKeys + newUnsyncedKeys chan bool // one of two triggers needed to send unsynced keys + quit chan bool // signal to quit loops + + // DB related fields + dbAccess *DbAccess // access to dbStore + db *storage.LDBDatabase // delivery msg db + + // native fields + queues [priorities]*syncDb // in-memory cache / queues for sync reqs + keys [priorities]chan interface{} // buffer for unsynced keys + deliveries [priorities]chan *storeRequestMsgData // delivery + + // bzz protocol instance outgoing message callbacks (mockable for testing) + unsyncedKeys func([]*syncRequest, *syncState) error // send unsyncedKeysMsg + store func(*storeRequestMsgData) error // send storeRequestMsg +} + +// a syncer instance is linked to each peer connection +// constructor is called from protocol after successful handshake +// the returned instance is attached to the peer and can be called +// by the forwarder +func newSyncer( + db *storage.LDBDatabase, remotekey storage.Key, + dbAccess *DbAccess, + unsyncedKeys func([]*syncRequest, *syncState) error, + store func(*storeRequestMsgData) error, + params *SyncParams, + state *syncState, +) (*syncer, error) { + + syncBufferSize := params.SyncBufferSize + keyBufferSize := params.KeyBufferSize + dbBatchSize := params.RequestDbBatchSize + + self := &syncer{ + key: remotekey, + dbAccess: dbAccess, + syncStates: make(chan *syncState, 20), + deliveryRequest: make(chan bool, 1), + newUnsyncedKeys: make(chan bool, 1), + SyncParams: params, + state: state, + quit: make(chan bool), + unsyncedKeys: unsyncedKeys, + store: store, + } + + // initialising + for i := 0; i < priorities; i++ { + self.keys[i] = make(chan interface{}, keyBufferSize) + self.deliveries[i] = make(chan *storeRequestMsgData) + // initialise a syncdb instance for each priority queue + self.queues[i] = newSyncDb(db, remotekey, uint(i), syncBufferSize, dbBatchSize, self.deliver(uint(i))) + } + self.state = state + glog.V(logger.Info).Infof("[BZZ] syncer started: %v", state) + // launch chunk delivery service + go self.syncDeliveries() + // launch sync task manager + go self.sync() + // process unsynced keys to broadcast + go self.syncUnsyncedKeys() + + return self, nil +} + +// newSyncState returns a default sync state given local and remote +// addresses and db count +func newSyncState(start, stop kademlia.Address, count uint64) *syncState { + // -> (] keyrange for db iterator -- interval open from the left + // storage count range --- interval open from the right + return &syncState{ + DbSyncState: &storage.DbSyncState{ + Start: storage.Key(start[:]), + Stop: storage.Key(stop[:]), + First: 0, + Last: count, + }, + SessionAt: count, + synced: make(chan bool), + } +} + +// metadata serialisation +func encodeSync(state *syncState) (*json.RawMessage, error) { + data, err := json.MarshalIndent(state, "", " ") + if err != nil { + return nil, err + } + meta := json.RawMessage(data) + return &meta, nil +} + +func decodeSync(meta *json.RawMessage) (*syncState, error) { + if meta == nil { + return nil, fmt.Errorf("unable to deserialise sync state from ") + } + data := []byte(*(meta)) + if len(data) == 0 { + return nil, fmt.Errorf("unable to deserialise sync state from ") + } + state := newSyncState(kademlia.Address{}, kademlia.Address{}, 0) + err := json.Unmarshal(data, state) + return state, err +} + +/* + sync implements the syncing script + * first all items left in the request Db are replayed + * type = StaleSync + * Mode: by default once again via confirmation roundtrip + * Priority: the items are replayed as the proirity specified for StaleSync + * but within the order respects earlier priority level of request + * after all items are consumed for a priority level, the the respective + queue for delivery requests is open (this way new reqs not written to db) + (TODO: this should be checked) + * the sync state provided by the remote peer is used to sync history + * all the backlog from earlier (aborted) syncing is completed starting from latest + * if Last < LastSeenAt then all items in between then process all + backlog from upto last disconnect + * if Last > 0 && + + sync is called from the syncer constructor and is not supposed to be used externally +*/ +func (self *syncer) sync() { + state := self.state + // sync finished + defer close(self.syncStates) + + // 0. first replay stale requests from request db + if state.SessionAt == 0 { + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: nothing to sync", self.key.Log()) + return + } + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: start replaying stale requests from request db", self.key.Log()) + for p := priorities - 1; p >= 0; p-- { + self.queues[p].dbRead(false, 0, self.replay()) + } + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: done replaying stale requests from request db", self.key.Log()) + + // unless peer is synced sync unfinished history beginning on + if !state.Synced { + start := state.Start + + if !storage.IsZeroKey(state.Latest) { + // 1. there is unfinished earlier sync + state.Start = state.Latest + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: start syncronising backlog (unfinished sync: %v)", self.key.Log(), state) + // blocks while the entire history upto state is synced + self.syncState(state) + if state.Last < state.SessionAt { + state.First = state.Last + 1 + } + } + state.Latest = storage.ZeroKey + state.Start = start + // 2. sync up to last disconnect1 + if state.First < state.LastSeenAt { + state.Last = state.LastSeenAt + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: start syncronising history upto last disconnect at %v: %v", self.key.Log(), state.LastSeenAt, state) + self.syncState(state) + state.First = state.LastSeenAt + } + state.Latest = storage.ZeroKey + + } else { + // synchronisation starts at end of last session + state.First = state.LastSeenAt + } + + // 3. sync up to current session start + // if there have been new chunks since last session + if state.LastSeenAt < state.SessionAt { + state.Last = state.SessionAt + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: start syncronising history since last disconnect at %v up until session start at %v: %v", self.key.Log(), state.LastSeenAt, state.SessionAt, state) + self.syncState(state) + } + glog.V(logger.Info).Infof("[BZZ] syncer[%v]: syncing all history complete", self.key.Log()) + +} + +// wait till syncronised block uptil state is synced +func (self *syncer) syncState(state *syncState) { + self.syncStates <- state + select { + case <-state.synced: + case <-self.quit: + } +} + +// stop quits both request processor and saves the request cache to disk +func (self *syncer) stop() { + close(self.quit) + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: save sync request db backlog", self.key.Log()) + for _, db := range self.queues { + db.stop() + } +} + +// rlp serialisable sync request +type syncRequest struct { + Key storage.Key + Priority uint +} + +func (self *syncRequest) String() string { + return fmt.Sprintf("", self.Key.Log(), self.Priority) +} + +func (self *syncer) newSyncRequest(req interface{}, p int) (*syncRequest, error) { + key, _, _, _, err := parseRequest(req) + // TODO: if req has chunk, it should be put in a cache + // create + if err != nil { + return nil, err + } + return &syncRequest{key, uint(p)}, nil +} + +// serves historical items from the DB +// * read is on demand, blocking unless history channel is read +// * accepts sync requests (syncStates) to create new db iterator +// * closes the channel one iteration finishes +func (self *syncer) syncHistory(state *syncState) chan interface{} { + var n uint + history := make(chan interface{}) + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: syncing history between %v - %v for chunk addresses %v - %v", self.key.Log(), state.First, state.Last, state.Start, state.Stop) + it := self.dbAccess.iterator(state) + if it != nil { + go func() { + // signal end of the iteration ended + defer close(history) + IT: + for { + key := it.Next() + if key != nil { + select { + // blocking until history channel is read from + case history <- storage.Key(key): + n++ + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: history: %v (%v keys)", self.key.Log(), key.Log(), n) + state.Latest = key + case <-self.quit: + return + } + } else { + break IT + } + } + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: finished syncing history between %v - %v for chunk addresses %v - %v (at %v) (chunks = %v)", self.key.Log(), state.First, state.Last, state.Start, state.Stop, state.Latest, n) + }() + } + return history +} + +// triggers key syncronisation +func (self *syncer) sendUnsyncedKeys() { + select { + case self.deliveryRequest <- true: + default: + } +} + +// assembles a new batch of unsynced keys +// * keys are drawn from the key buffers in order of priority queue +// * if the queues of priority for History (HistoryReq) or higher are depleted, +// historical data is used so historical items are lower priority within +// their priority group. +// * Order of historical data is unspecified +func (self *syncer) syncUnsyncedKeys() { + // send out new + var unsynced []*syncRequest + var more, justSynced bool + var keyCount, historyCnt int + var history chan interface{} + + priority := High + keys := self.keys[priority] + var newUnsyncedKeys, deliveryRequest chan bool + keyCounts := make([]int, priorities) + histPrior := self.SyncPriorities[HistoryReq] + syncStates := self.syncStates + state := self.state + +LOOP: + for { + + var req interface{} + // select the highest priority channel to read from + // keys channels are buffered so the highest priority ones + // are checked first - integrity can only be guaranteed if writing + // is locked while selecting + if priority != High || len(keys) == 0 { + keys = nil + for priority = High; priority >= 0; priority-- { + if len(self.keys[priority]) > 0 { + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: reading request with priority %v", self.key.Log(), priority) + keys = self.keys[priority] + break + } + if uint(priority) == histPrior && history != nil { + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: reading history for %v", self.key.Log(), self.key) + keys = history + break + } + } + // if peer ready to receive but nothing to send + if keys == nil && deliveryRequest == nil { + // if no items left and switch to waiting mode + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: buffers consumed. Waiting", self.key.Log()) + newUnsyncedKeys = self.newUnsyncedKeys + } + } + + // send msg iff + // * peer is ready to receive keys AND ( + // * all queues and history are depleted OR + // * batch full OR + // * all history have been consumed, synced) + if deliveryRequest == nil && + (justSynced || + len(unsynced) > 0 && keys == nil || + len(unsynced) == int(self.SyncBatchSize)) { + justSynced = false + // listen to requests again + deliveryRequest = self.deliveryRequest + newUnsyncedKeys = nil // not care about data until next req comes in + // set sync to current counter + // (all nonhistorical outgoing traffic sheduled and persisted + state.LastSeenAt = self.dbAccess.counter() + state.Latest = storage.ZeroKey + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: sending %v", self.key.Log(), unsynced) + // send the unsynced keys + stateCopy := *state + err := self.unsyncedKeys(unsynced, &stateCopy) + self.state = state + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: --> %v keys sent: (total: %v (%v), history: %v), sent sync state: %v", self.key.Log(), len(unsynced), keyCounts, keyCount, historyCnt, stateCopy) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] syncer[%v]: unable to send unsynced keys: %v", err) + } + unsynced = nil + } + + // process item and add it to the batch + select { + case <-self.quit: + break LOOP + case req, more = <-keys: + if keys == history && !more { + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: syncing history segment complete", self.key.Log()) + // history channel is closed, waiting for new state (called from sync()) + syncStates = self.syncStates + state.Synced = true // this signals that the current segment is complete + state.synced <- false + justSynced = true + history = nil + } + case <-deliveryRequest: + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: peer ready to receive", self.key.Log()) + + // this 1 cap channel can wake up the loop + // signaling that peer is ready to receive unsynced Keys + // the channel is set to nil any further writes will be ignored + deliveryRequest = nil + + case <-newUnsyncedKeys: + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: new unsynked keys available", self.key.Log()) + // this 1 cap channel can wake up the loop + // signals that data is available to send if peer is ready to receive + newUnsyncedKeys = nil + // this can only happen if keys is + + case state, more = <-syncStates: + // this resets the state + if !more { + state = self.state + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: (priority %v) syncing complete upto %v)", self.key.Log(), priority, state) + state.Synced = true + syncStates = nil + } else { + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: (priority %v) syncing history upto %v priority %v)", self.key.Log(), priority, state, histPrior) + state.Synced = false + history = self.syncHistory(state) + // only one history at a time, only allow another one once the + // history channel is closed + syncStates = nil + } + } + if req == nil { + continue LOOP + } + + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: (priority %v) added to unsynced keys: %v", self.key.Log(), priority, req) + keyCounts[priority]++ + keyCount++ + if keys == history { + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: (priority %v) history item %v (synced = %v)", self.key.Log(), priority, req, state.Synced) + historyCnt++ + } + if sreq, err := self.newSyncRequest(req, priority); err == nil { + // extract key from req + glog.V(logger.Detail).Infof("[BZZ] syncer[%v] (priority %v): request %v (synced = %v)", self.key.Log(), priority, req, state.Synced) + unsynced = append(unsynced, sreq) + } else { + glog.V(logger.Warn).Infof("[BZZ] syncer[%v] (priority %v): error creating request for %v: %v)", self.key.Log(), priority, req, state.Synced, err) + } + + } +} + +// delivery loop +// takes into account priority, send store Requests with chunk (delivery) +// idle blocking if no new deliveries in any of the queues +func (self *syncer) syncDeliveries() { + var req *storeRequestMsgData + p := High + var deliveries chan *storeRequestMsgData + var msg *storeRequestMsgData + var err error + var c = [priorities]int{} + var n = [priorities]int{} + var total, success uint + + for { + deliveries = self.deliveries[p] + select { + case req = <-deliveries: + n[p]++ + c[p]++ + default: + if p == Low { + // blocking, depletion on all channels, no preference for priority + select { + case req = <-self.deliveries[High]: + n[High]++ + case req = <-self.deliveries[Medium]: + n[Medium]++ + case req = <-self.deliveries[Low]: + n[Low]++ + case <-self.quit: + return + } + p = High + } else { + p-- + continue + } + } + total++ + msg, err = self.newStoreRequestMsgData(req) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] syncer[%v]: failed to deliver %v: %v", self.key.Log(), req, err) + } else { + err = self.store(msg) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] syncer[%v]: failed to deliver %v: %v", self.key.Log(), req, err) + } else { + success++ + glog.V(logger.Detail).Infof("[BZZ] syncer[%v]: %v successfully delivered", self.key.Log(), req) + } + } + if total%self.SyncBatchSize == 0 { + glog.V(logger.Debug).Infof("[BZZ] syncer[%v]: deliver Total: %v, Success: %v, High: %v/%v, Medium: %v/%v, Low %v/%v", self.key.Log(), total, success, c[High], n[High], c[Medium], n[Medium], c[Low], n[Low]) + } + } +} + +/* + addRequest handles requests for delivery + it accepts 4 types: + + * storeRequestMsgData: coming from netstore propagate response + * chunk: coming from forwarding (questionable: id?) + * key: from incoming syncRequest + * syncDbEntry: key,id encoded in db + + If sync mode is on for the type of request, then + it sends the request to the keys queue of the correct priority + channel buffered with capacity (SyncBufferSize) + + If sync mode is off then, requests are directly sent to deliveries +*/ +func (self *syncer) addRequest(req interface{}, ty int) { + // retrieve priority for request type + priority := self.SyncPriorities[ty] + // sync mode for this type ON + if self.SyncModes[ty] { + self.addKey(req, priority, self.quit) + } else { + self.addDelivery(req, priority, self.quit) + } +} + +// addKey queues sync request for sync confirmation with given priority +// ie the key will go out in an unsyncedKeys message +func (self *syncer) addKey(req interface{}, priority uint, quit chan bool) bool { + select { + case self.keys[priority] <- req: + // this wakes up the unsynced keys loop if idle + select { + case self.newUnsyncedKeys <- true: + default: + } + return true + case <-quit: + return false + } +} + +// addDelivery queues delivery request for with given priority +// ie the chunk will be delivered ASAP mod priority queueing handled by syncdb +// requests are persisted across sessions for correct sync +func (self *syncer) addDelivery(req interface{}, priority uint, quit chan bool) bool { + select { + case self.queues[priority].buffer <- req: + return true + case <-quit: + return false + } +} + +// doDelivery delivers the chunk for the request with given priority +// without queuing +func (self *syncer) doDelivery(req interface{}, priority uint, quit chan bool) bool { + msgdata, err := self.newStoreRequestMsgData(req) + if err != nil { + glog.V(logger.Warn).Infof("unable to deliver request %v: %v", msgdata, err) + return false + } + select { + case self.deliveries[priority] <- msgdata: + return true + case <-quit: + return false + } +} + +// returns the delivery function for given priority +// passed on to syncDb +func (self *syncer) deliver(priority uint) func(req interface{}, quit chan bool) bool { + return func(req interface{}, quit chan bool) bool { + return self.doDelivery(req, priority, quit) + } +} + +// returns the replay function passed on to syncDb +// depending on sync mode settings for BacklogReq, +// re play of request db backlog sends items via confirmation +// or directly delivers +func (self *syncer) replay() func(req interface{}, quit chan bool) bool { + sync := self.SyncModes[BacklogReq] + priority := self.SyncPriorities[BacklogReq] + // sync mode for this type ON + if sync { + return func(req interface{}, quit chan bool) bool { + return self.addKey(req, priority, quit) + } + } else { + return func(req interface{}, quit chan bool) bool { + return self.doDelivery(req, priority, quit) + } + + } +} + +// given a request, extends it to a full storeRequestMsgData +// polimorphic: see addRequest for the types accepted +func (self *syncer) newStoreRequestMsgData(req interface{}) (*storeRequestMsgData, error) { + + key, id, chunk, sreq, err := parseRequest(req) + if err != nil { + return nil, err + } + + if sreq == nil { + if chunk == nil { + var err error + chunk, err = self.dbAccess.get(key) + if err != nil { + return nil, err + } + } + + sreq = &storeRequestMsgData{ + Id: id, + Key: chunk.Key, + SData: chunk.SData, + } + } + + return sreq, nil +} + +// parse request types and extracts, key, id, chunk, request if available +// does not do chunk lookup ! +func parseRequest(req interface{}) (storage.Key, uint64, *storage.Chunk, *storeRequestMsgData, error) { + var key storage.Key + var entry *syncDbEntry + var chunk *storage.Chunk + var id uint64 + var ok bool + var sreq *storeRequestMsgData + var err error + + if key, ok = req.(storage.Key); ok { + id = generateId() + + } else if entry, ok = req.(*syncDbEntry); ok { + id = binary.BigEndian.Uint64(entry.val[32:]) + key = storage.Key(entry.val[:32]) + + } else if chunk, ok = req.(*storage.Chunk); ok { + key = chunk.Key + id = generateId() + + } else if sreq, ok = req.(*storeRequestMsgData); ok { + key = sreq.Key + } else { + err = fmt.Errorf("type not allowed: %v (%T)", req, req) + } + + return key, id, chunk, sreq, err +} diff --git a/swarm/services/swap/swap.go b/swarm/services/swap/swap.go new file mode 100644 index 0000000000..cc44e263c2 --- /dev/null +++ b/swarm/services/swap/swap.go @@ -0,0 +1,271 @@ +package swap + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "os" + "path/filepath" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/common/swap" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +// SwAP Swarm Accounting Protocol with +// SWAP^2 Strategies of Withholding Automatic Payments +// SWAP^3 Accreditation: payment via credit SWAP +// using chequebook pkg for delayed payments +// default parameters + +var ( + autoCashInterval = 300 * time.Second // default interval for autocash + autoCashThreshold = big.NewInt(50000000000000) // threshold that triggers autocash (wei) + autoDepositInterval = 300 * time.Second // default interval for autocash + autoDepositThreshold = big.NewInt(50000000000000) // threshold that triggers autodeposit (wei) + autoDepositBuffer = big.NewInt(100000000000000) // buffer that is surplus for fork protection etc (wei) + buyAt = big.NewInt(20000000000) // maximum chunk price host is willing to pay (wei) + sellAt = big.NewInt(20000000000) // minimum chunk price host requires (wei) + payAt = 100 // threshold that triggers payment request (units) + dropAt = 10000 // threshold that triggers disconnect (units) + + maxRetries = 5 +) + +var ( + retryInterval = 10 * time.Second +) + +type SwapParams struct { + *swap.Params + *PayProfile +} + +type SwapProfile struct { + *swap.Profile + *PayProfile +} + +type PayProfile struct { + PublicKey string // check againsst signature of promise + Contract common.Address // address of chequebook contract + Beneficiary common.Address // recipient address for swarm sales revenue + privateKey *ecdsa.PrivateKey `json:"-"` + publicKey *ecdsa.PublicKey `json:"-"` + owner common.Address + chbook *chequebook.Chequebook `json:"-"` + backend chequebook.Backend + lock sync.RWMutex +} + +func DefaultSwapParams(contract common.Address, prvkey *ecdsa.PrivateKey) *SwapParams { + pubkey := &prvkey.PublicKey + return &SwapParams{ + PayProfile: &PayProfile{ + PublicKey: common.ToHex(crypto.FromECDSAPub(pubkey)), + Contract: contract, + Beneficiary: crypto.PubkeyToAddress(*pubkey), + privateKey: prvkey, + publicKey: pubkey, + owner: crypto.PubkeyToAddress(*pubkey), + }, + Params: &swap.Params{ + Profile: &swap.Profile{ + BuyAt: buyAt, + SellAt: sellAt, + PayAt: uint(payAt), + DropAt: uint(dropAt), + }, + Strategy: &swap.Strategy{ + AutoCashInterval: autoCashInterval, + AutoCashThreshold: autoCashThreshold, + AutoDepositInterval: autoDepositInterval, + AutoDepositThreshold: autoDepositThreshold, + AutoDepositBuffer: autoDepositBuffer, + }, + }, + } +} + +// swap constructor, parameters +// * global chequebook, assume deployed service and +// * the balance is at buffer. +// swap.Add(n) called in netstore +// n > 0 called when sending chunks = receiving retrieve requests +// OR sending cheques. +// n < 0 called when receiving chunks = receiving delivery responses +// OR receiving cheques. + +func NewSwap(local *SwapParams, remote *SwapProfile, proto swap.Protocol) (self *swap.Swap, err error) { + + // check if remote chequebook is valid + // insolvent chequebooks suicide so will signal as invalid + // TODO: monitoring a chequebooks events + var in *chequebook.Inbox + err = chequebook.Validate(remote.Contract, local.backend) + if err != nil { + glog.V(logger.Info).Infof("[BZZ] SWAP invalid contract %v for peer %v: %v)", remote.Contract.Hex()[:8], proto, err) + } else { + // remote contract valid, create inbox + in, err = chequebook.NewInbox(remote.Contract, local.owner, local.Beneficiary, crypto.ToECDSAPub(common.FromHex(remote.PublicKey)), local.backend) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] SWAP unable to set up inbox for chequebook contract %v for peer %v: %v)", remote.Contract.Hex()[:8], proto, err) + } + } + + // cheque if local chequebook contract is valid + var out *chequebook.Outbox + err = chequebook.Validate(local.Contract, local.backend) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] SWAP unable to set up outbox for peer %v: chequebook contract (owner: %v): %v)", proto, local.owner.Hex(), err) + } else { + out = chequebook.NewOutbox(local.Chequebook(), remote.Beneficiary) + } + + pm := swap.Payment{ + In: in, + Out: out, + Buys: out != nil, + Sells: in != nil, + } + self, err = swap.New(local.Params, pm, proto) + if err != nil { + return + } + // remote profile given (first) in handshake + self.SetRemote(remote.Profile) + var buy, sell string + if self.Buys { + buy = "purchase from peer enabled at " + remote.SellAt.String() + " wei/chunk" + } else { + buy = "purchase from peer disabled" + } + if self.Sells { + sell = "selling to peer enabled at " + local.SellAt.String() + " wei/chunk" + } else { + sell = "selling to peer disabled" + } + glog.V(logger.Warn).Infof("[BZZ] SWAP arrangement with <%v>: %v; %v)", proto, buy, sell) + + return +} + +func (self *SwapParams) Chequebook() *chequebook.Chequebook { + defer self.lock.Unlock() + self.lock.Lock() + return self.chbook +} + +func (self *SwapParams) PrivateKey() *ecdsa.PrivateKey { + return self.privateKey +} + +func (self *SwapParams) PublicKey() *ecdsa.PublicKey { + return self.publicKey +} +func (self *SwapParams) SetKey(prvkey *ecdsa.PrivateKey) { + self.privateKey = prvkey + self.publicKey = &prvkey.PublicKey +} + +const ( + confirmationInterval = 60000000000 + timeout = 30000000000 // 30 sec +) + +// setChequebook(path, backend) wraps the +// chequebook initialiser and sets up autoDeposit to cover spending. +func (self *SwapParams) SetChequebook(path string, backend chequebook.Backend) (done chan bool, err error) { + defer self.lock.Unlock() + self.lock.Lock() + var valid bool + done = make(chan bool) + self.backend = backend + err = chequebook.Validate(self.Contract, backend) + if err != nil { + owner := crypto.PubkeyToAddress(*(self.publicKey)) + go self.deployChequebook(owner, path, done) + } else { + valid = true + go func() { + done <- false + close(done) + }() + } + if valid { + err = self.newChequebookFromContract(path, backend) + return done, err + } + return done, nil +} + +func (self *SwapParams) deployChequebook(owner common.Address, path string, done chan bool) { + var timer = time.NewTimer(0).C + retries := 0 + var err error + var valid bool +OUT: + for { + select { + case <-timer: + // this is blocking + glog.V(logger.Info).Infof("[BZZ] SWAP Deploying new chequebook (owner: %v)", owner.Hex()) + var contract common.Address + contract, err = chequebook.Deploy(owner, self.backend, self.AutoDepositBuffer, confirmationInterval, timeout) + if err != nil { + glog.V(logger.Info).Infof("[BZZ] SWAP unable to deploy new chequebook: %v...retrying in %v", err, retryInterval) + if retries >= maxRetries { + glog.V(logger.Info).Infof("[BZZ] SWAP unable to deploy new chequebook: giving up after %v retries", retries) + break OUT + } + retries++ + timer = time.NewTicker(retryInterval).C + } else { + // need to save config at this point + self.lock.Lock() + self.Contract = contract + err = self.newChequebookFromContract(path, self.backend) + if err != nil { + glog.V(logger.Info).Infof("[BZZ] SWAP error initialising cheque book (owner: %v)", owner.Hex()) + } + self.lock.Unlock() + valid = true + break OUT + } + } + } + done <- valid + close(done) +} + +// initialise the chequebook from a persisted json file or create a new one +// caller holds the lock +func (self *SwapParams) newChequebookFromContract(path string, backend chequebook.Backend) error { + + hexkey := common.Bytes2Hex(self.Contract.Bytes()) + err := os.MkdirAll(filepath.Join(path, "chequebooks"), os.ModePerm) + if err != nil { + return fmt.Errorf("unable to create directory for chequebooks: %v", err) + } + + chbookpath := filepath.Join(path, "chequebooks", hexkey+".json") + self.chbook, err = chequebook.LoadChequebook(chbookpath, self.privateKey, backend) + + if err != nil { + self.chbook, err = chequebook.NewChequebook(chbookpath, self.Contract, self.privateKey, backend) + if err != nil { + glog.V(logger.Warn).Infof("[BZZ] SWAP unable to initialise chequebook (owner: %v): %v", self.owner.Hex(), err) + return fmt.Errorf("unable to initialise chequebook (owner: %v): %v", self.owner.Hex(), err) + } + } + + self.chbook.AutoDeposit(self.AutoDepositInterval, self.AutoDepositThreshold, self.AutoDepositBuffer) + glog.V(logger.Info).Infof("[BZZ] SWAP auto deposit ON for %v -> %v: interval = %v, threshold = %v, buffer = %v)", crypto.PubkeyToAddress(*(self.publicKey)).Hex()[:8], self.Contract.Hex()[:8], self.AutoDepositInterval, self.AutoDepositThreshold, self.AutoDepositBuffer) + + return nil +} diff --git a/swarm/services/swear/doc.go b/swarm/services/swear/doc.go new file mode 100644 index 0000000000..bde891d22b --- /dev/null +++ b/swarm/services/swear/doc.go @@ -0,0 +1,3 @@ +package swear + +// https://github.com/ethersphere/swarm/blob/master/book/texi/incentivisation.texi diff --git a/swarm/services/swear/swear.sol b/swarm/services/swear/swear.sol new file mode 100644 index 0000000000..0086abe7b1 --- /dev/null +++ b/swarm/services/swear/swear.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/swarm/services/swear/swear_test.go b/swarm/services/swear/swear_test.go new file mode 100644 index 0000000000..937da310de --- /dev/null +++ b/swarm/services/swear/swear_test.go @@ -0,0 +1 @@ +package swear diff --git a/swarm/storage/chunker.go b/swarm/storage/chunker.go new file mode 100644 index 0000000000..951e389030 --- /dev/null +++ b/swarm/storage/chunker.go @@ -0,0 +1,411 @@ +package storage + +import ( + "encoding/binary" + "fmt" + "io" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +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 = hash(int64(size) + data) + +5 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 = hash(int64(size) + key(slice0) + key(slice1) + ...) + + The underlying hash function is configurable +*/ + +const ( + // defaultHash = "SHA3" // http://golang.org/pkg/hash/#Hash + defaultHash = "SHA256" // http://golang.org/pkg/hash/#Hash + defaultBranches int64 = 128 + joinTimeout = 120 // second + splitTimeout = 120 // second + // hashSize int64 = hasherfunc.New().Size() // hasher knows about its own length in bytes + // chunksize int64 = branches * hashSize // chunk is defined as this +) + +/* +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. +The hashing itself does use extra copies and allocation though, since it does need it. +*/ + +type ChunkerParams struct { + Branches int64 + Hash string + JoinTimeout time.Duration + SplitTimeout time.Duration +} + +func NewChunkerParams() *ChunkerParams { + return &ChunkerParams{ + Branches: defaultBranches, + Hash: defaultHash, + JoinTimeout: joinTimeout, + SplitTimeout: splitTimeout, + } +} + +type TreeChunker struct { + branches int64 + hashFunc Hasher + joinTimeout time.Duration + splitTimeout time.Duration + // calculated + hashSize int64 // self.hashFunc.New().Size() + chunkSize int64 // hashSize* branches +} + +func NewTreeChunker(params *ChunkerParams) (self *TreeChunker) { + self = &TreeChunker{} + self.hashFunc = MakeHashFunc(params.Hash) + self.branches = params.Branches + self.joinTimeout = params.JoinTimeout * time.Second + self.splitTimeout = params.SplitTimeout * time.Second + self.hashSize = int64(self.hashFunc().Size()) + self.chunkSize = self.hashSize * self.branches + return +} + +func (self *TreeChunker) KeySize() int64 { + return self.hashSize +} + +// String() for pretty printing +func (self *Chunk) String() string { + return fmt.Sprintf("Key: %v TreeSize: %v Chunksize: %v\n", self.Key.Log(), self.Size, len(self.SData)) +} + +// 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() + 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++ + } + + // glog.V(logger.Detail).Infof("[BZZ] 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 + // glog.V(logger.Detail).Infof("[BZZ] 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) + // glog.V(logger.Detail).Infof("[BZZ] 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) + // glog.V(logger.Detail).Infof("[BZZ] 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 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 chunks + 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 // root key + chunkC chan *Chunk // chunk channel to send retrieve requests on + size int64 // size of the entire subtree + off int64 // offset + quitC chan bool // channel to abort retrieval + errC chan error // error channel to monitor retrieve errors + chunker *TreeChunker // needs TreeChunker params TODO: should just take + // the chunkSize, branches etc as params +} + +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) + glog.V(logger.Detail).Infof("[BZZ] readAt: reading %v into %d bytes at offset %d.", chunk.Key.Log(), 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)) + // glog.V(logger.Detail).Infof("[BZZ] quit") + return + case <-chunk.C: // bells are ringing, data have been delivered + // glog.V(logger.Detail).Infof("[BZZ] chunk data received for %v", chunk.Key.Log()) + } + if len(chunk.SData) == 0 { + // glog.V(logger.Detail).Infof("[BZZ] No payload in %v", chunk.Key.Log()) + return 0, notFound + } + chunk.Size = int64(binary.LittleEndian.Uint64(chunk.SData[0:8])) + self.size = chunk.Size + if b == nil { + // glog.V(logger.Detail).Infof("[BZZ] Size query for %v", chunk.Key.Log()) + 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: + // glog.V(logger.Detail).Infof("[BZZ] ReadAt received %v", err) + read = len(b) + if off+int64(read) == self.size { + err = io.EOF + } + // glog.V(logger.Detail).Infof("[BZZ] ReadAt returning at %d: %v", read, err) + case <-self.quitC: + // glog.V(logger.Detail).Infof("[BZZ] 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() + + // glog.V(logger.Detail).Infof("[BZZ] 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 { + // glog.V(logger.Detail).Infof("[BZZ] 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] + // glog.V(logger.Detail).Infof("[BZZ] subtree index: %v -> %v", j, childKey.Log()) + + ch := &Chunk{ + Key: childKey, + C: make(chan bool), // close channel to signal data delivery + } + // glog.V(logger.Detail).Infof("[BZZ] chunk data sent for %v (key interval in chunk %v-%v)", ch.Key.Log(), 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 + // glog.V(logger.Detail).Infof("[BZZ] 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/swarm/storage/chunker_test.go b/swarm/storage/chunker_test.go new file mode 100644 index 0000000000..7dd301fff5 --- /dev/null +++ b/swarm/storage/chunker_test.go @@ -0,0 +1,214 @@ +package storage + +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, tester := chunkerAndTester() + 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 = NewTreeChunker(&ChunkerParams{ + Branches: 2, + Hash: "SHA256", + SplitTimeout: 10, + JoinTimeout: 10, + }) + 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/swarm/storage/chunkreader.go b/swarm/storage/chunkreader.go new file mode 100644 index 0000000000..b147c85bf8 --- /dev/null +++ b/swarm/storage/chunkreader.go @@ -0,0 +1,194 @@ +package storage + +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/swarm/storage/common_test.go b/swarm/storage/common_test.go new file mode 100644 index 0000000000..40dc35fc69 --- /dev/null +++ b/swarm/storage/common_test.go @@ -0,0 +1,94 @@ +package storage + +import ( + "crypto/rand" + "io" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +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 := NewTreeChunker(&ChunkerParams{ + Branches: branches, + Hash: defaultHash, + SplitTimeout: splitTimeout, + }) + 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 := NewTreeChunker(&ChunkerParams{ + Branches: branches, + Hash: defaultHash, + SplitTimeout: splitTimeout, + }) + 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 { + glog.V(logger.Detail).Infof("[BZZ] chunk '%x' not found", chunk.Key) + } else if err != nil { + glog.V(logger.Detail).Infof("[BZZ] error retrieving chunk %x: %v", chunk.Key, err) + } else { + chunk.SData = storedChunk.SData + } + glog.V(logger.Detail).Infof("[BZZ] 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/swarm/storage/database.go b/swarm/storage/database.go new file mode 100644 index 0000000000..9d07fae88d --- /dev/null +++ b/swarm/storage/database.go @@ -0,0 +1,96 @@ +package storage + +// 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/swarm/storage/dbstore.go b/swarm/storage/dbstore.go new file mode 100644 index 0000000000..80b53eebea --- /dev/null +++ b/swarm/storage/dbstore.go @@ -0,0 +1,457 @@ +// disk storage layer for the package bzz +// DbStore implements the ChunkStore interface and is used by the DPA as +// persistent storage of chunks +// it implements purging based on access count allowing for external control of +// max capacity + +package storage + +import ( + "bytes" + "encoding/binary" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rlp" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" +) + +const ( + defaultDbCapacity = 5000000 + defaultRadius = 0 // not yet used + + gcArraySize = 10000 + gcArrayFreeRatio = 0.1 + + // key prefixes for leveldb storage + kpIndex = 0 + kpData = 1 +) + +var ( + keyAccessCnt = []byte{2} + keyEntryCnt = []byte{3} + keyDataIdx = []byte{4} + 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 + + hashfunc Hasher + + lock sync.Mutex +} + +func NewDbStore(path string, hash Hasher, capacity uint64, radius int) (s *DbStore, err error) { + s = new(DbStore) + + s.hashfunc = hash + + s.db, err = NewLDBDatabase(path) + if err != nil { + return + } + + s.setCapacity(capacity) + + 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 +} + +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 + copy(key[1:], hash[:]) + 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 { + 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) { + 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 + } + } + it.Release() + + 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) Counter() uint64 { + s.lock.Lock() + defer s.lock.Unlock() + return s.dataIdx +} + +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) + + batch.Put(getDataKey(s.dataIdx), data) + + index.Idx = s.dataIdx + s.updateIndexAccess(&index) + + idata := encodeIndex(&index) + batch.Put(ikey, idata) + + batch.Put(keyEntryCnt, U64ToBytes(s.entryCnt)) + s.entryCnt++ + batch.Put(keyDataIdx, U64ToBytes(s.dataIdx)) + s.dataIdx++ + batch.Put(keyAccessCnt, U64ToBytes(s.accessCnt)) + s.accessCnt++ + + s.db.Write(batch) + if chunk.dbStored != nil { + close(chunk.dbStored) + } + glog.V(logger.Detail).Infof("[BZZ] DbStore.Put: %v. db storage counter: %v ", chunk.Key.Log(), s.dataIdx) +} + +// 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) + + batch.Put(keyAccessCnt, U64ToBytes(s.accessCnt)) + 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 := s.hashfunc() + hasher.Write(data) + hash := hasher.Sum(nil) + if bytes.Compare(hash, key) != 0 { + s.db.Delete(getDataKey(index.Idx)) + err = fmt.Errorf("invalid chunk. hash=%x, key=%v", hash, key[:]) + 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 (s *DbStore) close() { + s.db.Close() +} + +// describes a section of the DbStore representing the unsynced +// domain relevant to a peer +// Start - Stop designate a continuous area Keys in an address space +// typically the addresses closer to us than to the peer but not closer +// another closer peer in between +// From - To designates a time interval typically from the last disconnect +// till the latest connection (real time traffic is relayed) +type DbSyncState struct { + Start, Stop Key + First, Last uint64 +} + +// implements the syncer iterator interface +// iterates by storage index (~ time of storage = first entry to db) +type dbSyncIterator struct { + it iterator.Iterator + DbSyncState +} + +// initialises a sync iterator from a syncToken (passed in with the handshake) +func (self *DbStore) NewSyncIterator(state DbSyncState) (si *dbSyncIterator, err error) { + if state.First > state.Last { + return nil, fmt.Errorf("no entries found") + } + si = &dbSyncIterator{ + it: self.db.NewIterator(), + DbSyncState: state, + } + si.it.Seek(getIndexKey(state.Start)) + return si, nil +} + +// walk the area from Start to Stop and returns items within time interval +// First to Last +func (self *dbSyncIterator) Next() (key Key) { + for self.it.Valid() { + dbkey := self.it.Key() + if dbkey[0] != 0 { + break + } + key = Key(make([]byte, len(dbkey)-1)) + copy(key[:], dbkey[1:]) + if bytes.Compare(key[:], self.Start) <= 0 { + self.it.Next() + continue + } + if bytes.Compare(key[:], self.Stop) > 0 { + break + } + var index dpaDBIndex + decodeIndex(self.it.Value(), &index) + self.it.Next() + if (index.Idx >= self.First) && (index.Idx < self.Last) { + return + } + } + self.it.Release() + return nil +} diff --git a/swarm/storage/dbstore_test.go b/swarm/storage/dbstore_test.go new file mode 100644 index 0000000000..6bebe10133 --- /dev/null +++ b/swarm/storage/dbstore_test.go @@ -0,0 +1,172 @@ +package storage + +import ( + "bytes" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func initDbStore() (m *DbStore) { + os.RemoveAll("/tmp/bzz") + m, err := NewDbStore("/tmp/bzz", MakeHashFunc(defaultHash), defaultDbCapacity, defaultRadius) + 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() + _, err := m.Get(ZeroKey) + if err != notFound { + t.Errorf("Expected notFound, got %v", err) + } +} + +func TestDbStoreSyncIterator(t *testing.T) { + m := initDbStore() + defer m.close() + keys := []Key{ + Key(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000")), + Key(common.Hex2Bytes("4000000000000000000000000000000000000000000000000000000000000000")), + Key(common.Hex2Bytes("5000000000000000000000000000000000000000000000000000000000000000")), + Key(common.Hex2Bytes("3000000000000000000000000000000000000000000000000000000000000000")), + Key(common.Hex2Bytes("2000000000000000000000000000000000000000000000000000000000000000")), + Key(common.Hex2Bytes("1000000000000000000000000000000000000000000000000000000000000000")), + } + for _, key := range keys { + m.Put(NewChunk(key, nil)) + } + it, err := m.NewSyncIterator(DbSyncState{ + Start: Key(common.Hex2Bytes("1000000000000000000000000000000000000000000000000000000000000000")), + Stop: Key(common.Hex2Bytes("4000000000000000000000000000000000000000000000000000000000000000")), + First: 2, + Last: 4, + }) + if err != nil { + t.Fatalf("unexpected error creating NewSyncIterator") + } + + var chunk Key + var res []Key + for { + chunk = it.Next() + if chunk == nil { + break + } + res = append(res, chunk) + } + if len(res) != 1 { + t.Fatalf("Expected 1 chunk, got %v: %v", len(res), res) + } + if !bytes.Equal(res[0][:], keys[3]) { + t.Fatalf("Expected %v chunk, got %v", keys[3], res[0]) + } + + if err != nil { + t.Fatalf("unexpected error creating NewSyncIterator") + } + + it, err = m.NewSyncIterator(DbSyncState{ + Start: Key(common.Hex2Bytes("1000000000000000000000000000000000000000000000000000000000000000")), + Stop: Key(common.Hex2Bytes("5000000000000000000000000000000000000000000000000000000000000000")), + First: 2, + Last: 4, + }) + + res = nil + for { + chunk = it.Next() + if chunk == nil { + break + } + res = append(res, chunk) + } + if len(res) != 2 { + t.Fatalf("Expected 2 chunk, got %v: %v", len(res), res) + } + if !bytes.Equal(res[0][:], keys[3]) { + t.Fatalf("Expected %v chunk, got %v", keys[3], res[0]) + } + if !bytes.Equal(res[1][:], keys[2]) { + t.Fatalf("Expected %v chunk, got %v", keys[2], res[1]) + } + + if err != nil { + t.Fatalf("unexpected error creating NewSyncIterator") + } + + it, err = m.NewSyncIterator(DbSyncState{ + Start: Key(common.Hex2Bytes("1000000000000000000000000000000000000000000000000000000000000000")), + Stop: Key(common.Hex2Bytes("4000000000000000000000000000000000000000000000000000000000000000")), + First: 2, + Last: 5, + }) + res = nil + for { + chunk = it.Next() + if chunk == nil { + break + } + res = append(res, chunk) + } + if len(res) != 2 { + t.Fatalf("Expected 2 chunk, got %v", len(res)) + } + if !bytes.Equal(res[0][:], keys[4]) { + t.Fatalf("Expected %v chunk, got %v", keys[4], res[0]) + } + if !bytes.Equal(res[1][:], keys[3]) { + t.Fatalf("Expected %v chunk, got %v", keys[3], res[1]) + } + + it, err = m.NewSyncIterator(DbSyncState{ + Start: Key(common.Hex2Bytes("2000000000000000000000000000000000000000000000000000000000000000")), + Stop: Key(common.Hex2Bytes("4000000000000000000000000000000000000000000000000000000000000000")), + First: 2, + Last: 5, + }) + res = nil + for { + chunk = it.Next() + if chunk == nil { + break + } + res = append(res, chunk) + } + if len(res) != 1 { + t.Fatalf("Expected 1 chunk, got %v", len(res)) + } + if !bytes.Equal(res[0][:], keys[3]) { + t.Fatalf("Expected %v chunk, got %v", keys[3], res[0]) + } +} diff --git a/swarm/storage/dpa.go b/swarm/storage/dpa.go new file mode 100644 index 0000000000..922ff3bec2 --- /dev/null +++ b/swarm/storage/dpa.go @@ -0,0 +1,234 @@ +package storage + +import ( + "errors" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +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 its own chunk store +implementation for storage or retrieval. +*/ + +const ( + storeChanCapacity = 100 + retrieveChanCapacity = 100 + singletonSwarmDbCapacity = 50000 + singletonSwarmCacheCapacity = 500 +) + +var ( + notFound = errors.New("not found") +) + +type DPA struct { + ChunkStore + storeC chan *Chunk + retrieveC chan *Chunk + Chunker Chunker + + lock sync.Mutex + running bool + wg *sync.WaitGroup + quitC chan bool +} + +// for testing locally +func NewLocalDPA(datadir string) (*DPA, error) { + + hash := MakeHashFunc("SHA256") + + dbStore, err := NewDbStore(datadir, hash, singletonSwarmDbCapacity, 0) + if err != nil { + return nil, err + } + + return NewDPA(&LocalStore{ + NewMemStore(dbStore, singletonSwarmCacheCapacity), + dbStore, + }, NewChunkerParams()), nil +} + +func NewDPA(store ChunkStore, params *ChunkerParams) *DPA { + chunker := NewTreeChunker(params) + return &DPA{ + Chunker: chunker, + ChunkStore: store, + } +} + +// Public API. Main entry point for document retrieval directly. Used by the +// FS-aware API and httpaccess +// Chunk retrieval blocks on netStore requests with a timeout so reader will +// report error if retrieval of chunks within requested range time out. +func (self *DPA) Retrieve(key Key) SectionReader { + return self.Chunker.Join(key, self.retrieveC) +} + +// Public API. Main entry point for document storage directly. Used by the +// FS-aware API and httpaccess +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 { + glog.V(logger.Error).Infof("[BZZ] 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() +} + +func (self *DPA) Stop() { + self.lock.Lock() + defer self.lock.Unlock() + if !self.running { + return + } + self.running = false + close(self.quitC) +} + +// retrieveLoop dispatches the parallel chunk retrieval requests received on the +// retrieve channel to its ChunkStore (NetStore or LocalStore) +func (self *DPA) retrieveLoop() { + self.retrieveC = make(chan *Chunk, retrieveChanCapacity) + + go func() { + RETRIEVE: + for ch := range self.retrieveC { + + go func(chunk *Chunk) { + glog.V(logger.Detail).Infof("[BZZ] dpa: retrieve loop : chunk %v", chunk.Key.Log()) + storedChunk, err := self.Get(chunk.Key) + if err == notFound { + glog.V(logger.Detail).Infof("[BZZ] chunk %v not found", chunk.Key.Log()) + } else if err != nil { + glog.V(logger.Detail).Infof("[BZZ] error retrieving chunk %v: %v", chunk.Key.Log(), err) + } else { + chunk.SData = storedChunk.SData + chunk.Size = storedChunk.Size + } + close(chunk.C) + }(ch) + select { + case <-self.quitC: + break RETRIEVE + default: + } + } + }() +} + +// storeLoop dispatches the parallel chunk store requests received on the +// store channel to its ChunkStore (NetStore or LocalStore) +func (self *DPA) storeLoop() { + self.storeC = make(chan *Chunk) + go func() { + STORE: + for ch := range self.storeC { + go func(chunk *Chunk) { + self.Put(chunk) + if chunk.wg != nil { + glog.V(logger.Detail).Infof("[BZZ] DPA.storeLoop %v", chunk.Key.Log()) + chunk.wg.Done() + } + }(ch) + select { + case <-self.quitC: + break STORE + default: + } + } + }() +} + +// DpaChunkStore implements the ChunkStore interface, +// this chunk access layer assumed 2 chunk stores +// local storage eg. LocalStore and network storage eg., NetStore +// access by calling network is blocking with a timeout + +type dpaChunkStore struct { + n int + localStore ChunkStore + netStore ChunkStore +} + +func NewDpaChunkStore(localStore, netStore ChunkStore) *dpaChunkStore { + return &dpaChunkStore{0, localStore, netStore} +} + +// Get is the entrypoint for local retrieve requests +// waits for response or times out +func (self *dpaChunkStore) Get(key Key) (chunk *Chunk, err error) { + chunk, err = self.netStore.Get(key) + // timeout := time.Now().Add(searchTimeout) + if chunk.SData != nil { + glog.V(logger.Detail).Infof("[BZZ] DPA.Get: %v found locally, %d bytes", key.Log(), len(chunk.SData)) + return + } + // TODO: use self.timer time.Timer and reset with defer disableTimer + timer := time.After(searchTimeout) + select { + case <-timer: + glog.V(logger.Detail).Infof("[BZZ] DPA.Get: %v request time out ", key.Log()) + err = notFound + case <-chunk.Req.C: + glog.V(logger.Detail).Infof("[BZZ] DPA.Get: %v retrieved, %d bytes (%p)", key.Log(), len(chunk.SData), chunk) + } + return +} + +// Put is the entrypoint for local store requests coming from storeLoop +func (self *dpaChunkStore) Put(entry *Chunk) { + chunk, err := self.localStore.Get(entry.Key) + if err != nil { + glog.V(logger.Detail).Infof("[BZZ] DPA.Put: %v new chunk. call netStore.Put", entry.Key.Log()) + chunk = entry + } else if chunk.SData == nil { + glog.V(logger.Detail).Infof("[BZZ] DPA.Put: %v request entry found", entry.Key.Log()) + chunk.SData = entry.SData + chunk.Size = entry.Size + } else { + glog.V(logger.Detail).Infof("[BZZ] DPA.Put: %v chunk already known", entry.Key.Log()) + return + } + // from this point on the storage logic is the same with network storage requests + glog.V(logger.Detail).Infof("[BZZ] DPA.Put %v: %v", self.n, chunk.Key.Log()) + self.n++ + self.netStore.Put(chunk) +} diff --git a/swarm/storage/dpa_test.go b/swarm/storage/dpa_test.go new file mode 100644 index 0000000000..a4400783fb --- /dev/null +++ b/swarm/storage/dpa_test.go @@ -0,0 +1,133 @@ +package storage + +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", MakeHashFunc(defaultHash), defaultDbCapacity, defaultRadius) + dbStore.setCapacity(50000) + if err != nil { + t.Errorf("DB error: %v", err) + } + memStore := NewMemStore(dbStore, defaultCacheCapacity) + localStore := &LocalStore{ + memStore, + dbStore, + } + chunker := NewTreeChunker(NewChunkerParams()) + 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, defaultCacheCapacity) + 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", MakeHashFunc(defaultHash), defaultDbCapacity, defaultRadius) + if err != nil { + t.Errorf("DB error: %v", err) + } + memStore := NewMemStore(dbStore, defaultCacheCapacity) + localStore := &LocalStore{ + memStore, + dbStore, + } + memStore.setCapacity(0) + chunker := NewTreeChunker(NewChunkerParams()) + 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 + 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/swarm/storage/localstore.go b/swarm/storage/localstore.go new file mode 100644 index 0000000000..81d4a6f2d0 --- /dev/null +++ b/swarm/storage/localstore.go @@ -0,0 +1,53 @@ +package storage + +// LocalStore is a combination of inmemory db over a disk persisted db +// implements a Get/Put with fallback (caching) logic using any 2 ChunkStores +type LocalStore struct { + memStore ChunkStore + DbStore ChunkStore +} + +// This constructor uses MemStore and DbStore as components +func NewLocalStore(hash Hasher, params *StoreParams) (*LocalStore, error) { + dbStore, err := NewDbStore(params.ChunkDbPath, hash, params.DbCapacity, params.Radius) + if err != nil { + return nil, err + } + return &LocalStore{ + memStore: NewMemStore(dbStore, params.CacheCapacity), + DbStore: dbStore, + }, nil +} + +// LocalStore is itself a chunk store +// unsafe, in that the data is not integrity 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 may be needed to wrap this call if +// ChunkStores are remote and can have long latency +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/swarm/storage/memstore.go b/swarm/storage/memstore.go new file mode 100644 index 0000000000..f415bdfa59 --- /dev/null +++ b/swarm/storage/memstore.go @@ -0,0 +1,336 @@ +// memory storage layer for the package blockhash + +package storage + +import ( + "bytes" + "sync" +) + +const ( + memTreeLW = 2 // log2(subtree count) of the subtrees + memTreeFLW = 14 // log2(subtree count) of the root layer + dbForceUpdateAccessCnt = 1000 + defaultCacheCapacity = 5000 +) + +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, capacity uint) (m *MemStore) { + m = &MemStore{} + m.memtree = newMemTree(memTreeFLW, nil, 0) + m.dbStore = d + m.setCapacity(capacity) + 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/swarm/storage/memstore_test.go b/swarm/storage/memstore_test.go new file mode 100644 index 0000000000..ff90d17602 --- /dev/null +++ b/swarm/storage/memstore_test.go @@ -0,0 +1,34 @@ +package storage + +import ( + "testing" +) + +func testMemStore(l int64, branches int64, t *testing.T) { + m := NewMemStore(nil, defaultCacheCapacity) + 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, defaultCacheCapacity) + _, err := m.Get(ZeroKey) + if err != notFound { + t.Errorf("Expected notFound, got %v", err) + } +} diff --git a/swarm/storage/netstore.go b/swarm/storage/netstore.go new file mode 100644 index 0000000000..9d61feb45b --- /dev/null +++ b/swarm/storage/netstore.go @@ -0,0 +1,117 @@ +package storage + +import ( + "path/filepath" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +/* +NetStore is a cloud storage access abstaction layer for swarm +it contains the shared logic of network served chunk store/retrieval requests +both local (coming from DPA api) and remote (coming from peers via bzz protocol) +it implements the ChunkStore interface and embeds LocalStore + +It is called by the bzz protocol instances via Depo (the store/retrieve request handler) +a protocol instance is running on each peer, so this is heavily parallelised. +NetStore falls back to a backend (CloudStorage interface) +implemented by bzz/network/forwarder. forwarder or IPFS or IPΞS +*/ +type NetStore struct { + hashfunc Hasher + localStore *LocalStore + cloud CloudStore + lock sync.Mutex +} + +// backend engine for cloud store +// It can be aggregate dispatching to several parallel implementations: +// bzz/network/forwarder. forwarder or IPFS or IPΞS +type CloudStore interface { + Store(*Chunk) + Deliver(*Chunk) + Retrieve(*Chunk) +} + +type StoreParams struct { + ChunkDbPath string + DbCapacity uint64 + CacheCapacity uint + Radius int +} + +func NewStoreParams(path string) (self *StoreParams) { + return &StoreParams{ + ChunkDbPath: filepath.Join(path, "chunks"), + DbCapacity: defaultDbCapacity, + CacheCapacity: defaultCacheCapacity, + Radius: defaultRadius, + } +} + +// netstore contructor, takes path argument that is used to initialise dbStore, +// the persistent (disk) storage component of LocalStore +// the second argument is the hive, the connection/logistics manager for the node +func NewNetStore(hash Hasher, lstore *LocalStore, cloud CloudStore, params *StoreParams) *NetStore { + return &NetStore{ + hashfunc: hash, + localStore: lstore, + cloud: cloud, + } +} + +const ( + // maximum number of peers that a retrieved message is delivered to + requesterCount = 3 +) + +var ( + // timeout interval before retrieval is timed out + searchTimeout = 3 * time.Second +) + +// store logic common to local and network chunk store requests +// ~ unsafe put in localdb no check if exists no extra copy no hash validation +// the chunk is forced to propagate (Cloud.Store) even if locally found! +// caller needs to make sure if that is wanted +func (self *NetStore) Put(entry *Chunk) { + self.localStore.Put(entry) + + // handle deliveries + if entry.Req != nil { + glog.V(logger.Detail).Infof("[BZZ] NetStore.Put: localStore.Put %v hit existing request...delivering", entry.Key.Log()) + // closing C singals to other routines (local requests) + // that the chunk is has been retrieved + close(entry.Req.C) + // deliver the chunk to requesters upstream + self.cloud.Deliver(entry) + } else { + glog.V(logger.Detail).Infof("[BZZ] NetStore.Put: localStore.Put %v stored locally", entry.Key.Log()) + // handle propagating store requests + go self.cloud.Store(entry) + } +} + +// retrieve logic common for local and network chunk retrieval requests +func (self *NetStore) Get(key Key) (*Chunk, error) { + var err error + chunk, err := self.localStore.Get(key) + if err == nil { + if chunk.Req == nil { + glog.V(logger.Detail).Infof("[BZZ] NetStore.Get: %v found locally", key) + } else { + glog.V(logger.Detail).Infof("[BZZ] NetStore.Get: %v hit on an existing request", key) + // no need to launch again + } + return chunk, err + } + // no data and no request status + glog.V(logger.Detail).Infof("[BZZ] NetStore.Get: %v not found locally. open new request", key) + chunk = NewChunk(key, newRequestStatus(key)) + self.localStore.memStore.Put(chunk) + go self.cloud.Retrieve(chunk) + return chunk, nil +} diff --git a/swarm/storage/types.go b/swarm/storage/types.go new file mode 100644 index 0000000000..124a56a085 --- /dev/null +++ b/swarm/storage/types.go @@ -0,0 +1,155 @@ +package storage + +import ( + "bytes" + "crypto" + "fmt" + "hash" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto/sha3" +) + +type Hasher func() hash.Hash + +type Peer interface{} + +type Key []byte + +func IsZeroKey(key Key) bool { + return len(key) == 0 || bytes.Equal(key, ZeroKey) +} + +var ZeroKey = Key(common.Hash{}.Bytes()) + +func MakeHashFunc(hash string) Hasher { + switch hash { + case "SHA256": + return crypto.SHA256.New + case "SHA3": + return sha3.NewKeccak256 + } + return nil +} + +func (key Key) Hex() string { + return fmt.Sprintf("%064x", []byte(key[:])) +} + +func (key Key) Log() string { + if len(key[:]) < 4 { + return fmt.Sprintf("%x", []byte(key[:])) + } + return fmt.Sprintf("%08x", []byte(key[:4])) +} + +func (key Key) String() string { + return fmt.Sprintf("%064x", []byte(key)[:]) +} + +func (key Key) MarshalJSON() (out []byte, err error) { + return []byte(`"` + key.String() + `"`), nil +} + +func (key *Key) UnmarshalJSON(value []byte) error { + s := string(value) + *key = make([]byte, 32) + h := common.Hex2Bytes(s[1 : len(s)-1]) + copy(*key, h) + return nil +} + +// each chunk when first requested opens a record associated with the request +// next time a request for the same chunk arrives, this record is updated +// this request status keeps track of the request ID-s as well as the requesting +// peers and has a channel that is closed when the chunk is retrieved. Multiple +// local callers can wait on this channel (or combined with a timeout, block with a +// select). +type RequestStatus struct { + Key Key + Source Peer + C chan bool + Requesters map[uint64][]interface{} +} + +func newRequestStatus(key Key) *RequestStatus { + return &RequestStatus{ + Key: key, + Requesters: make(map[uint64][]interface{}), + C: make(chan bool), + } +} + +// Chunk 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() +// but the size of the subtree encoded in the chunk +// 0 if request, to be supplied by the dpa +type Chunk struct { + Key Key // always + SData []byte // nil if request, to be supplied by dpa + Size int64 // size of the data covered by the subtree encoded in this chunk + Source Peer // peer + C chan bool // to signal data delivery by the dpa + Req *RequestStatus // request Status needed by netStore + wg *sync.WaitGroup // wg to synchronize + dbStored chan bool // never remove a chunk from memStore before it is written to dbStore +} + +func NewChunk(key Key, rs *RequestStatus) *Chunk { + return &Chunk{Key: key, Req: rs} +} + +/* +The ChunkStore interface is implemented by : + +- MemStore: a memory cache +- DbStore: local disk/db store +- LocalStore: a combination (sequence of) memStore and dbStore +- NetStore: cloud storage abstraction layer +- DPA: local requests for swarm storage and retrieval +*/ +type ChunkStore interface { + Put(*Chunk) // effectively there is no error even if there is an error + Get(Key) (*Chunk, error) +} + +/* +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). + +Split returns an error channel, which the caller can monitor. +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 seekable lazy reader. The caller again provides a channel 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). As the seekable reader is used, the chunker then puts these together the relevant parts on demand. +*/ +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, which is + seekable and implements on-demand fetching of chunks as and where it is read. + 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. + As a result, partial reads from a document are possible even if other parts + are corrupt or lost. + The chunks are not meant to be validated by the chunker when joining. This + is because it is left to the DPA to decide which sources are trusted. + */ + Join(key Key, chunkC chan *Chunk) SectionReader + + // returns the key length + KeySize() int64 +} diff --git a/swarm/swarm.go b/swarm/swarm.go new file mode 100644 index 0000000000..d7edbfbcd8 --- /dev/null +++ b/swarm/swarm.go @@ -0,0 +1,231 @@ +package swarm + +import ( + "bytes" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/chequebook" + "github.com/ethereum/go-ethereum/common/httpclient" + "github.com/ethereum/go-ethereum/common/registrar/ethreg" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/swarm/api" + "github.com/ethereum/go-ethereum/swarm/network" + "github.com/ethereum/go-ethereum/swarm/storage" +) + +// the swarm stack +type Swarm struct { + config *api.Config // swarm configuration + api *api.Api // high level api layer (fs/manifest) + dbAccess *network.DbAccess // access to local chunk db iterator and storage counter + storage storage.ChunkStore // internal access to storage, common interface to cloud storage backends + dpa *storage.DPA // distributed preimage archive, the local API to the storage with document level storage/retrieval support + depo network.StorageHandler // remote request handler, interface between bzz protocol and the storage + cloud storage.CloudStore // procurement, cloud storage backend (can multi-cloud) + hive *network.Hive // the logistic manager + client *httpclient.HTTPClient // bzz capable light http client +} + +// creates a new swarm service instance +// implements node.Service +func NewSwarm(stack *node.ServiceContext, config *api.Config, swapEnabled bool) (self *Swarm, err error) { + + if bytes.Equal(common.FromHex(config.PublicKey), storage.ZeroKey) { + return nil, fmt.Errorf("empty public key") + } + if bytes.Equal(common.FromHex(config.BzzKey), storage.ZeroKey) { + return nil, fmt.Errorf("empty bzz key") + } + + var ethereum *eth.Ethereum + if err := stack.Service(ðereum); err != nil { + return nil, fmt.Errorf("unable to find Ethereum service: %v", err) + } + self = &Swarm{ + config: config, + client: ethereum.HTTPClient(), + } + glog.V(logger.Debug).Infof("[BZZ] Setting up Swarm service components") + + // setup local store + hash := storage.MakeHashFunc(config.ChunkerParams.Hash) + lstore, err := storage.NewLocalStore(hash, config.StoreParams) + if err != nil { + return + } + glog.V(logger.Debug).Infof("[BZZ] Set up local storage") + + self.dbAccess = network.NewDbAccess(lstore) + glog.V(logger.Debug).Infof("[BZZ] Set up local db access (iterator/counter)") + + // set up the kademlia hive + self.hive = network.NewHive( + common.HexToHash(self.config.BzzKey), // key to hive (kademlia base address) + config.HiveParams, // configuration parameters + ) + glog.V(logger.Debug).Infof("[BZZ] Set up swarm network with Kademlia hive") + + // setup cloud storage backend + cloud := network.NewForwarder(self.hive) + glog.V(logger.Debug).Infof("[BZZ] -> set swarm forwarder as cloud storage backend") + // setup cloud storage internal access layer + + self.storage = storage.NewNetStore(hash, lstore, cloud, config.StoreParams) + glog.V(logger.Debug).Infof("[BZZ] -> Level 0: swarm net store shared access layer to Swarm Chunk Store") + + // set up Depo (storage handler = remote cloud storage access layer) + self.depo = network.NewDepo(hash, lstore, self.storage) + glog.V(logger.Debug).Infof("[BZZ] -> REmote Access to CHunks") + + // set up DPA, the cloud storage local access layer + dpaChunkStore := storage.NewDpaChunkStore(lstore, self.storage) + glog.V(logger.Debug).Infof("[BZZ] -> Local Access to Swarm") + // Swarm Hash Merklised Chunking for Arbitrary-length Document/File storage + self.dpa = storage.NewDPA(dpaChunkStore, self.config.ChunkerParams) + glog.V(logger.Debug).Infof("[BZZ] -> Level 1: Document/File API") + + // set up high level api + backend := api.NewEthApi(ethereum) + backend.UpdateState() + self.api = api.NewApi(self.dpa, ethreg.New(backend), self.config) + // Manifests for Smart Hosting + glog.V(logger.Debug).Infof("[BZZ] -> Level 2: Collection/Directory API") + + // set chequebook + if swapEnabled { + err = self.SetChequebook(backend) + if err != nil { + return nil, fmt.Errorf("Unable to set chequebook for SWAP: %v", err) + } + glog.V(logger.Debug).Infof("[BZZ] -> cheque book for SWAP") + } + return self, nil +} + +/* +Start is called when the stack is started +* starts the network kademlia hive peer management +* (starts netStore level 0 api) +* starts DPA level 1 api (chunking -> store/retrieve requests) +* (starts level 2 api) +* starts http proxy server +* registers url scheme handlers for bzz, etc +* TODO: start subservices like sword, swear, swarmdns +*/ +// implements the node.Service interface +func (self *Swarm) Start(net *p2p.Server) error { + connectPeer := func(url string) error { + node, err := discover.ParseNode(url) + if err != nil { + return fmt.Errorf("invalid node URL: %v", err) + } + net.AddPeer(node) + return nil + } + + glog.V(logger.Warn).Infof("[BZZ] Starting Swarm service") + self.hive.Start( + discover.PubkeyID(&net.PrivateKey.PublicKey), + func() string { return net.ListenAddr }, + connectPeer, + ) + glog.V(logger.Info).Infof("[BZZ] Swarm network started on bzz address: %v", self.hive.Addr()) + + self.dpa.Start() + glog.V(logger.Debug).Infof("[BZZ] Swarm DPA started") + + // start swarm http proxy server + if self.config.Port != "" { + go api.StartHttpServer(self.api, self.config.Port) + } + glog.V(logger.Debug).Infof("[BZZ] Swarm http proxy started on port: %v", self.config.Port) + + // register roundtripper (using proxy) as bzz scheme handler + // for the ethereum http client + // this is a place holder until schemes and ports are properly mapped in config + schemes := map[string]string{ + "bzz": self.config.Port, + } + for scheme, port := range schemes { + self.client.RegisterScheme(scheme, &api.RoundTripper{Port: port}) + } + glog.V(logger.Debug).Infof("[BZZ] Swarm protocol handlers registered for url schemes: %v", schemes) + + return nil +} + +// implements the node.Service interface +// stops all component services. +func (self *Swarm) Stop() error { + self.dpa.Stop() + self.hive.Stop() + if ch := self.config.Swap.Chequebook(); ch != nil { + ch.Stop() + ch.Save() + } + return self.config.Save() +} + +// implements the node.Service interface +func (self *Swarm) Protocols() []p2p.Protocol { + proto, err := network.Bzz(self.depo, self.hive, self.dbAccess, self.config.Swap, self.config.SyncParams) + if err != nil { + return nil + } + return []p2p.Protocol{proto} +} + +func (self *Swarm) Api() *api.Api { + return self.api +} + +// Backend interface implemented by eth or JSON-IPC client +func (self *Swarm) SetChequebook(backend chequebook.Backend) (err error) { + done, err := self.config.Swap.SetChequebook(self.config.Path, backend) + if err != nil { + return err + } + go func() { + ok := <-done + if ok { + glog.V(logger.Info).Infof("[BZZ] Swarm: new chequebook set (%v): saving config file, resetting all connections in the hive", self.config.Swap.Contract) + self.config.Save() + self.hive.DropAll() + } + }() + return nil +} + +// Local swarm without netStore +func NewLocalSwarm(datadir, port string) (self *Swarm, err error) { + + prvKey, err := crypto.GenerateKey() + if err != nil { + return + } + + config, err := api.NewConfig(datadir, common.Address{}, prvKey) + if err != nil { + return + } + config.Port = port + + dpa, err := storage.NewLocalDPA(datadir) + if err != nil { + return + } + + self = &Swarm{ + api: api.NewApi(dpa, nil, config), + config: config, + } + + return +} diff --git a/swarm/test/syncing/00.sh b/swarm/test/syncing/00.sh new file mode 100644 index 0000000000..281087f701 --- /dev/null +++ b/swarm/test/syncing/00.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +function up { #file port + echo "Upload file '$1' to node $2 on port 85$2" 1>&2 + key=`bash swarm/cmd/bzzup.sh $1 85$2` + echo -n $key +} + +function down { #key port + echo "Download hash '$1' from node $2 on port 85$2" + wget -O- http://localhost:85$2/$1 > /dev/null && echo "got it" || echo "not found" +} + +function clean { #index + echo "Clean up for $1" + rm -rf ~/tmp/sync/$1/{bzz/*/chunks,bzz/*/requests/,bzz/*/bzz-peers.json,chaindata,nodes} +} + +function gethup { #index account + cp geth geth$1 + echo "start node $1" + echo "./geth$1 --datadir ~/tmp/sync/$1 --bzzaccount $2 --unlock 0 --password <(echo bzz) --networkid 323 --port 303$1 --vmodule netstore=6,depo=6,forwarding=6,hive=5,dpa=6,dpa=6,http=6,syncb=6,syncer=6,protocol=6,swap=6,chequebook=6 --shh=false --nodiscover --maxpeers 20 --dev --vmdebug=false --verbosity 4 2> sync$1.log &" + ./geth$1 --datadir ~/tmp/sync/$1 --bzzaccount "$2" --unlock 0 --password <(echo bzz) --networkid 323 --port 303$1 --vmodule netstore=6,depo=6,forwarding=6,hive=5,dpa=6,dpa=6,http=6,syncb=6,syncer=6,protocol=6,swap=6,chequebook=6 --shh=false --nodiscover --maxpeers 20 --dev --vmdebug=false --verbosity 4 2> sync$1.log +} + +function gethdown { #index + killall -INT geth$1 +} + +wait=5 +gethdown 00 +gethdown 01 + +# ./geth --datadir ~/tmp/sync/00 --password <(echo bzz) account new +BZZKEY00=5c6332e46a095feb9da1ed9803af2fa425f96aa6 +BZZKEY01=7dafa7436cba4b5b94a0452557b20b01d23bdc05 +echo "Fresh start, wipe datadir" +clean 00 +clean 01 + +gethup 00 $BZZKEY00 & +sleep 2 +echo "beyond\n" +key=$(up COPYING.LESSER 00) +echo "beyond\n" +down $key 00 + +echo "beyond\n" +gethup 01 $BZZKEY01 & +sleep $wait +down $key 01 +gethdown 01 + + +key=$(up AUTHORS 00) +down $key 00 +gethup 01 $BZZKEY01 & +sleep $wait +down $key 01 + +key=$(up COPYING 00) +down $key 00 +down $key 01 + +key=$(up README.md 01) +down $key 01 +sleep $wait +down $key 00 + +exit 0 + +gethdown 00 +key=$(up README.md 01) +down $key 01 +gethup 00 $BZZKEY00 & +sleep $wait +down $key 00 + +gethdown 00 +gethdown 01 + + + diff --git a/swarm/test/test0/img/logo.png b/swarm/test/test0/img/logo.png new file mode 100644 index 0000000000..e0fb15ab33 Binary files /dev/null and b/swarm/test/test0/img/logo.png differ diff --git a/swarm/test/test0/index.css b/swarm/test/test0/index.css new file mode 100644 index 0000000000..67cb8d0ffc --- /dev/null +++ b/swarm/test/test0/index.css @@ -0,0 +1,9 @@ +h1 { + color: black; + font-size: 12px; + background-color: orange; + border: 4px solid black; +} +body { + background-color: orange +} \ No newline at end of file diff --git a/swarm/test/test0/index.html b/swarm/test/test0/index.html new file mode 100644 index 0000000000..321e910d7a --- /dev/null +++ b/swarm/test/test0/index.html @@ -0,0 +1,10 @@ + + + + + + +

Swarm Test

+ Ethereum logo + + \ No newline at end of file