diff --git a/cmd/geth/js.go b/cmd/geth/js.go index cdafab7fa1..60b7acd128 100644 --- a/cmd/geth/js.go +++ b/cmd/geth/js.go @@ -18,6 +18,7 @@ package main import ( "bufio" + "encoding/json" "fmt" "math/big" "os" @@ -30,9 +31,9 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/natspec" - "github.com/ethereum/go-ethereum/common/registrar" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/natspec" + "github.com/ethereum/go-ethereum/eth/registrar" re "github.com/ethereum/go-ethereum/jsre" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" @@ -368,15 +369,23 @@ func (self *jsre) AskPassword() (string, bool) { return pass, true } -func (self *jsre) ConfirmTransaction(tx string) bool { +func (self *jsre) ConfirmTransaction(txs string) bool { // Retrieve the Ethereum instance from the node var ethereum *eth.Ethereum if err := self.stack.Service(ðereum); err != nil { return false } // If natspec is enabled, ask for permission - if ethereum.NatSpec { - notice := natspec.GetNotice(self.xeth, tx, ethereum.HTTPClient()) + if ethereum.NatSpecEnabled { + var tx natspec.SendTxArgs + err := json.Unmarshal([]byte(txs), &tx) + if err != nil { + return false + } + notice, err := ethereum.NatSpec().GetNatSpec(&tx) + if err != nil { + return false + } fmt.Println(notice) answer, _ := self.Prompt("Confirm Transaction [y/n]") return strings.HasPrefix(strings.Trim(answer, " "), "y") diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 63efa08ee3..970de33ae7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -604,6 +604,7 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node. BootstrapNodes: MakeBootstrapNodes(ctx), ListenAddr: MakeListenAddress(ctx), NAT: MakeNAT(ctx), + DocRoot: ctx.GlobalString(DocRootFlag.Name), MaxPeers: ctx.GlobalInt(MaxPeersFlag.Name), MaxPendingPeers: ctx.GlobalInt(MaxPendingPeersFlag.Name), } @@ -620,8 +621,7 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node. Etherbase: MakeEtherbase(accman, ctx), MinerThreads: ctx.GlobalInt(MinerThreadsFlag.Name), ExtraData: MakeMinerExtra(extra, ctx), - NatSpec: ctx.GlobalBool(NatspecEnabledFlag.Name), - DocRoot: ctx.GlobalString(DocRootFlag.Name), + NatSpecEnabled: ctx.GlobalBool(NatspecEnabledFlag.Name), GasPrice: common.String2Big(ctx.GlobalString(GasPriceFlag.Name)), GpoMinGasPrice: common.String2Big(ctx.GlobalString(GpoMinGasPriceFlag.Name)), GpoMaxGasPrice: common.String2Big(ctx.GlobalString(GpoMaxGasPriceFlag.Name)), diff --git a/common/bytes.go b/common/bytes.go index ba6987a9ec..c6e2cd3127 100644 --- a/common/bytes.go +++ b/common/bytes.go @@ -23,9 +23,14 @@ 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" @@ -139,8 +144,14 @@ func HasHexPrefix(str string) bool { } func IsHex(str string) bool { - l := len(str) - return l >= 4 && l%2 == 0 && str[0:2] == "0x" + return hexRe.MatchString(str) +} + +func NormaliseHex(str string) (string, bool) { + if HasHexPrefix(str) { + str = str[2:] + } + return str, IsHex(str) } func Bytes2Hex(d []byte) string { @@ -202,8 +213,8 @@ func ParseData(data ...interface{}) (ret []byte) { switch t := item.(type) { case string: var str []byte - if IsHex(t) { - str = Hex2Bytes(t[2:]) + if n, ok := NormaliseHex(t); ok { + str = Hex2Bytes(n) } else { str = []byte(t) } diff --git a/common/httpclient/httpclient.go b/common/httpclient/httpclient.go deleted file mode 100644 index 23373ecaf5..0000000000 --- a/common/httpclient/httpclient.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library 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. -// -// The go-ethereum library 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 the go-ethereum library. If not, see . - -package httpclient - -import ( - "fmt" - "io/ioutil" - "net/http" - "path/filepath" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" -) - -type HTTPClient struct { - *http.Transport - DocRoot string - schemes []string -} - -func New(docRoot string) (self *HTTPClient) { - self = &HTTPClient{ - Transport: &http.Transport{}, - DocRoot: docRoot, - schemes: []string{"file"}, - } - self.RegisterProtocol("file", http.NewFileTransport(http.Dir(self.DocRoot))) - return -} - -// Clients should be reused instead of created as needed. Clients are safe for concurrent use by multiple goroutines. - -// A Client is higher-level than a RoundTripper (such as Transport) and additionally handles HTTP details such as cookies and redirects. - -func (self *HTTPClient) Client() *http.Client { - return &http.Client{ - Transport: self, - } -} - -func (self *HTTPClient) RegisterScheme(scheme string, rt http.RoundTripper) { - self.schemes = append(self.schemes, scheme) - self.RegisterProtocol(scheme, rt) -} - -func (self *HTTPClient) HasScheme(scheme string) bool { - for _, s := range self.schemes { - if s == scheme { - return true - } - } - return false -} - -func (self *HTTPClient) GetAuthContent(uri string, hash common.Hash) ([]byte, error) { - // retrieve content - content, err := self.Get(uri, "") - if err != nil { - return nil, err - } - - // check hash to authenticate content - chash := crypto.Sha3Hash(content) - if chash != hash { - return nil, fmt.Errorf("content hash mismatch %x != %x (exp)", hash[:], chash[:]) - } - - return content, nil - -} - -// Get(uri, path) downloads the document at uri, if path is non-empty it -// is interpreted as a filepath to which the contents are saved -func (self *HTTPClient) Get(uri, path string) ([]byte, error) { - // retrieve content - resp, err := self.Client().Get(uri) - if err != nil { - return nil, err - } - defer func() { - if resp != nil { - resp.Body.Close() - } - }() - - var content []byte - content, err = ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode/100 != 2 { - return content, fmt.Errorf("HTTP error: %s", resp.Status) - } - - if path != "" { - var abspath string - abspath, err = filepath.Abs(path) - if err != nil { - return nil, err - } - err = ioutil.WriteFile(abspath, content, 0600) - if err != nil { - return nil, err - } - } - - return content, nil - -} diff --git a/eth/api.go b/eth/api.go index a1630e2d10..b9788787c8 100644 --- a/eth/api.go +++ b/eth/api.go @@ -32,12 +32,13 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/compiler" "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/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/compiler" + "github.com/ethereum/go-ethereum/eth/natspec" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/logger" @@ -882,19 +883,9 @@ func (s *PublicTransactionPoolAPI) sign(address common.Address, tx *types.Transa return tx.WithSignature(signature) } -type SendTxArgs struct { - From common.Address `json:"from"` - To common.Address `json:"to"` - Gas *rpc.HexNumber `json:"gas"` - GasPrice *rpc.HexNumber `json:"gasPrice"` - Value *rpc.HexNumber `json:"value"` - Data string `json:"data"` - Nonce *rpc.HexNumber `json:"nonce"` -} - // SendTransaction will create a transaction for the given transaction argument, sign it and submit it to the // transaction pool. -func (s *PublicTransactionPoolAPI) SendTransaction(args SendTxArgs) (common.Hash, error) { +func (s *PublicTransactionPoolAPI) SendTransaction(args *natspec.SendTxArgs) (common.Hash, error) { if args.Gas == nil { args.Gas = rpc.NewHexNumber(defaultGas) } diff --git a/eth/backend.go b/eth/backend.go index abd1214caa..238cc59dd4 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -30,12 +30,12 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/compiler" - "github.com/ethereum/go-ethereum/common/httpclient" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/compiler" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/eth/natspec" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/logger" @@ -70,7 +70,6 @@ type Config struct { DatabaseCache int NatSpec bool - DocRoot string AutoDAG bool PowTest bool ExtraData []byte @@ -80,6 +79,7 @@ type Config struct { GasPrice *big.Int MinerThreads int SolcPath string + NatSpecEnabled bool GpoMinGasPrice *big.Int GpoMaxGasPrice *big.Int @@ -108,6 +108,8 @@ type Ethereum struct { protocolManager *ProtocolManager SolcPath string solc *compiler.Solidity + natSpec *natspec.NatSpec + NatSpecEnabled bool GpoMinGasPrice *big.Int GpoMaxGasPrice *big.Int @@ -116,14 +118,12 @@ type Ethereum struct { GpobaseStepUp int GpobaseCorrectionFactor int - httpclient *httpclient.HTTPClient - eventMux *event.TypeMux + http *node.HTTPClient miner *miner.Miner Mining bool MinerThreads int - NatSpec bool AutoDAG bool PowTest bool autodagquit chan bool @@ -196,9 +196,9 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { accountManager: config.AccountManager, etherbase: config.Etherbase, netVersionId: config.NetworkId, - NatSpec: config.NatSpec, MinerThreads: config.MinerThreads, SolcPath: config.SolcPath, + NatSpecEnabled: config.NatSpecEnabled, AutoDAG: config.AutoDAG, PowTest: config.PowTest, GpoMinGasPrice: config.GpoMinGasPrice, @@ -207,9 +207,12 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { GpobaseStepDown: config.GpobaseStepDown, GpobaseStepUp: config.GpobaseStepUp, GpobaseCorrectionFactor: config.GpobaseCorrectionFactor, - httpclient: httpclient.New(config.DocRoot), } + backend := NewPublicBlockChainAPI(eth.BlockChain(), eth.ChainDb(), eth.EventMux(), eth.AccountManager()) + // eth.natSpec = natspec.New(backend, ctx.HTTP, registrar.New(backend)) + eth.natSpec = natspec.New(backend, ctx.HTTP, nil) + if config.PowTest { glog.V(logger.Info).Infof("ethash used in test mode") eth.pow, err = ethash.NewForTesting() @@ -307,6 +310,10 @@ func (s *Ethereum) APIs() []rpc.API { Namespace: "debug", Version: "1.0", Service: NewPrivateDebugAPI(s), + }, { + Namespace: "eth", + Version: "1.0", + Service: s.natSpec, }, } } @@ -327,6 +334,8 @@ func (s *Ethereum) Etherbase() (eb common.Address, err error) { return } +func (self *Ethereum) URLSchemes() []node.URLScheme { return nil } + // set in js console via admin interface or wrapper from cli flags func (self *Ethereum) SetEtherbase(etherbase common.Address) { self.etherbase = etherbase @@ -341,6 +350,7 @@ func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } func (s *Ethereum) TxPool() *core.TxPool { return s.txPool } func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux } +func (s *Ethereum) NatSpec() *natspec.NatSpec { return s.natSpec } func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb } func (s *Ethereum) DappDb() ethdb.Database { return s.dappDb } func (s *Ethereum) IsListening() bool { return true } // Always listening @@ -449,10 +459,10 @@ func (self *Ethereum) StopAutoDAG() { glog.V(logger.Info).Infof("Automatic pregeneration of ethash DAG OFF (ethash dir: %s)", ethash.DefaultDir) } -// HTTPClient returns the light http client used for fetching offchain docs +// HTTP returns the light http client used for fetching offchain docs // (natspec, source for verification) -func (self *Ethereum) HTTPClient() *httpclient.HTTPClient { - return self.httpclient +func (self *Ethereum) HTTP() *node.HTTPClient { + return self.http } func (self *Ethereum) Solc() (*compiler.Solidity, error) { diff --git a/common/compiler/solidity.go b/eth/compiler/solidity.go similarity index 100% rename from common/compiler/solidity.go rename to eth/compiler/solidity.go diff --git a/common/compiler/solidity_test.go b/eth/compiler/solidity_test.go similarity index 100% rename from common/compiler/solidity_test.go rename to eth/compiler/solidity_test.go diff --git a/common/natspec/natspec.go b/eth/natspec/natspec.go similarity index 55% rename from common/natspec/natspec.go rename to eth/natspec/natspec.go index d9627b4e1c..eabcd7b199 100644 --- a/common/natspec/natspec.go +++ b/eth/natspec/natspec.go @@ -23,58 +23,70 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/httpclient" - "github.com/ethereum/go-ethereum/common/registrar" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/xeth" + "github.com/ethereum/go-ethereum/eth/registrar" + "github.com/ethereum/go-ethereum/node" + rpc "github.com/ethereum/go-ethereum/rpc/v2" "github.com/robertkrimen/otto" ) type abi2method map[[8]byte]*method type NatSpec struct { - jsvm *otto.Otto - abiDocJson []byte - userDoc userDoc - tx, data string + backend Backend + http *node.HTTPClient + reg *registrar.Registrar } -// main entry point for to get natspec notice for a transaction -// the implementation is frontend friendly in that it always gives back -// a notice that is safe to display -// :FIXME: the second return value is an error, which can be used to fine-tune bahaviour -func GetNotice(xeth *xeth.XEth, tx string, http *httpclient.HTTPClient) (notice string) { - ns, err := New(xeth, tx, http) - if err != nil { - if ns == nil { - return getFallbackNotice(fmt.Sprintf("no NatSpec info found for contract: %v", err), tx) - } else { - return getFallbackNotice(fmt.Sprintf("invalid NatSpec info: %v", err), tx) - } - } - - notice, err = ns.Notice() - if err != nil { - return getFallbackNotice(fmt.Sprintf("NatSpec notice error: %v", err), tx) - } - - return +type Backend interface { + GetCode(address common.Address, blockNr rpc.BlockNumber) (string, error) } -func getFallbackNotice(comment, tx string) string { - return fmt.Sprintf("About to submit transaction (%s): %s", comment, tx) -} - -type transaction struct { - To string `json:"to"` - Data string `json:"data"` -} - -type jsonTx struct { - Params []transaction `json:"params"` +func New(backend Backend, http *node.HTTPClient, reg *registrar.Registrar) *NatSpec { + return &NatSpec{backend, http, reg} } type contractInfo struct { + jsvm *otto.Otto + abiDocJson []byte + userDoc userDoc + tx *SendTxArgs +} + +type SendTxArgs struct { + From common.Address `json:"from"` + To common.Address `json:"to"` + Gas *rpc.HexNumber `json:"gas"` + GasPrice *rpc.HexNumber `json:"gasPrice"` + Value *rpc.HexNumber `json:"value"` + Data string `json:"data"` + Nonce *rpc.HexNumber `json:"nonce"` +} + +func (self *NatSpec) GetNatSpec(tx *SendTxArgs) (string, error) { + to := tx.To + // in principle this could be cached, but practically swarm takes care of that + info, err := self.GetContractInfo(to) + if err != nil { + if info == nil { + return "", fmt.Errorf("no contract info found for %v: %v", to, err) + } else { + return "", fmt.Errorf("invalid contract info for %v: %v", to, err) + } + } + ns, err := newContractInfo(info, tx) + if err != nil { + return "", fmt.Errorf("invalid contract info for %v: %v", to, err) + } + notice, err := ns.notice() + if err != nil { + return "", fmt.Errorf("NatSpec notice error for contract %v: %v", to, err) + } + + return notice, nil +} + +type ContractInfo struct { Source string `json:"source"` Language string `json:"language"` Version string `json:"compilerVersion"` @@ -83,81 +95,56 @@ type contractInfo struct { DeveloperDoc json.RawMessage `json:"developerDoc"` } -func New(xeth *xeth.XEth, jsontx string, http *httpclient.HTTPClient) (self *NatSpec, err error) { - - // extract contract address from tx - var tx jsonTx - err = json.Unmarshal([]byte(jsontx), &tx) - if err != nil { - return - } - t := tx.Params[0] - contractAddress := t.To - - content, err := FetchDocsForContract(contractAddress, xeth, http) - if err != nil { - return - } - - self, err = NewWithDocs(content, jsontx, t.Data) - return -} - -// also called by admin.contractInfo.get -func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, client *httpclient.HTTPClient) (content []byte, err error) { +func (self *NatSpec) GetContractInfo(to common.Address) (*ContractInfo, error) { // retrieve contract hash from state - codehex := xeth.CodeAt(contractAddress) - codeb := xeth.CodeAtBytes(contractAddress) - - if codehex == "0x" { - err = fmt.Errorf("contract (%v) not found", contractAddress) - return + codehex, err := self.backend.GetCode(to, -1) + if err != nil || len(codehex) <= 3 { + return nil, fmt.Errorf("contract (%v) not found", to) } - codehash := common.BytesToHash(crypto.Sha3(codeb)) - // set up nameresolver with natspecreg + urlhint contract addresses - reg := registrar.New(xeth) + codehash := common.BytesToHash(crypto.Sha3(common.FromHex(codehex))) // resolve host via HashReg/UrlHint Resolver - hash, err := reg.HashToHash(codehash) + hash, err := self.reg.HashToHash(codehash) if err != nil { - return + return nil, fmt.Errorf("no content hash registered for contract %v: %v", to, err) } - if client.HasScheme("bzz") { - content, err = client.Get("bzz://"+hash.Hex()[2:], "") - if err == nil { // non-fatal - return + var data []byte + if self.http.HasScheme("bzz") { + data, err = self.http.GetBody("bzz://" + hash.Hex()[2:]) + if err != nil { // non-fatal + data = nil } - err = nil - //falling back to urlhint } - uri, err := reg.HashToUrl(hash) - if err != nil { - return + //falling back to urlhint + if data == nil { + uri, err := self.reg.HashToUrl(hash) + if err != nil { + return nil, err + } + + // get content via http client and authenticate content using hash + data, err = self.http.GetAuthBody(uri, hash) + if err != nil { + return nil, err + } } - // get content via http client and authenticate content using hash - content, err = client.GetAuthContent(uri, hash) + var info ContractInfo + err = json.Unmarshal(data, &info) if err != nil { - return + return nil, err } - return + return &info, nil } -func NewWithDocs(infoDoc []byte, tx string, data string) (self *NatSpec, err error) { +func newContractInfo(info *ContractInfo, tx *SendTxArgs) (self *contractInfo, err error) { - var contract contractInfo - err = json.Unmarshal(infoDoc, &contract) - if err != nil { - return - } - - self = &NatSpec{ + self = &contractInfo{ jsvm: otto.New(), - abiDocJson: []byte(contract.AbiDefinition), - userDoc: contract.UserDoc, + abiDocJson: []byte(info.AbiDefinition), + userDoc: info.UserDoc, tx: tx, - data: data, } // load and require natspec js (but it is meant to be protected environment) @@ -192,7 +179,7 @@ type userDoc struct { Methods map[string]*method `json:methods` } -func (self *NatSpec) makeAbi2method(abiKey [8]byte) (meth *method) { +func (self *contractInfo) makeAbi2method(abiKey [8]byte) (meth *method) { for signature, m := range self.userDoc.Methods { name := strings.Split(signature, "(")[0] hash := []byte(common.Bytes2Hex(crypto.Sha3([]byte(signature)))) @@ -207,26 +194,30 @@ func (self *NatSpec) makeAbi2method(abiKey [8]byte) (meth *method) { return } -func (self *NatSpec) Notice() (notice string, err error) { +func (self *contractInfo) notice() (notice string, err error) { var abiKey [8]byte - if len(self.data) < 10 { + if len(self.tx.Data) < 10 { err = fmt.Errorf("Invalid transaction data") return } - copy(abiKey[:], self.data[2:10]) + copy(abiKey[:], self.tx.Data[2:10]) meth := self.makeAbi2method(abiKey) if meth == nil { err = fmt.Errorf("abi key does not match any method") return } - notice, err = self.noticeForMethod(self.tx, meth.name, meth.Notice) + notice, err = self.noticeForMethod(meth.name, meth.Notice) return } -func (self *NatSpec) noticeForMethod(tx string, name, expression string) (notice string, err error) { +func (self *contractInfo) noticeForMethod(name, expression string) (notice string, err error) { + tx, err := json.Marshal(self.tx) + if err != nil { + return "", nil + } - if _, err = self.jsvm.Run("var transaction = " + tx + ";"); err != nil { + if _, err = self.jsvm.Run("var transaction = " + string(tx) + ";"); err != nil { return "", fmt.Errorf("natspec.js error setting transaction: %v", err) } @@ -247,12 +238,13 @@ func (self *NatSpec) noticeForMethod(tx string, name, expression string) (notice if err != nil { return "", fmt.Errorf("natspec.js error evaluating expression: %v", err) } + evalError := "Natspec evaluation failed, wrong input params" if value.String() == evalError { return "", fmt.Errorf("natspec.js error evaluating expression: wrong input params in expression '%s'", expression) } if len(value.String()) == 0 { - return "", fmt.Errorf("natspec.js error evaluating expression") + return "", fmt.Errorf("natspec.js error evaluating expression: empty notice") } return value.String(), nil diff --git a/common/natspec/natspec_e2e_test.go b/eth/natspec/natspec_e2e_test.go similarity index 100% rename from common/natspec/natspec_e2e_test.go rename to eth/natspec/natspec_e2e_test.go diff --git a/common/natspec/natspec_e2e_test.go.orig b/eth/natspec/natspec_e2e_test.go.orig similarity index 100% rename from common/natspec/natspec_e2e_test.go.orig rename to eth/natspec/natspec_e2e_test.go.orig diff --git a/common/natspec/natspec_js.go b/eth/natspec/natspec_js.go similarity index 100% rename from common/natspec/natspec_js.go rename to eth/natspec/natspec_js.go diff --git a/common/natspec/natspec_test.go b/eth/natspec/natspec_test.go similarity index 100% rename from common/natspec/natspec_test.go rename to eth/natspec/natspec_test.go diff --git a/common/registrar/contracts.go b/eth/registrar/contracts.go similarity index 100% rename from common/registrar/contracts.go rename to eth/registrar/contracts.go diff --git a/common/registrar/ethreg/ethreg.go b/eth/registrar/ethreg/ethreg.go similarity index 100% rename from common/registrar/ethreg/ethreg.go rename to eth/registrar/ethreg/ethreg.go diff --git a/common/registrar/registrar.go b/eth/registrar/registrar.go similarity index 100% rename from common/registrar/registrar.go rename to eth/registrar/registrar.go diff --git a/common/registrar/registrar_test.go b/eth/registrar/registrar_test.go similarity index 100% rename from common/registrar/registrar_test.go rename to eth/registrar/registrar_test.go diff --git a/node/config.go b/node/config.go index 93f0ba79d6..d7c3db3099 100644 --- a/node/config.go +++ b/node/config.go @@ -49,6 +49,10 @@ type Config struct { // in memory. DataDir string + // Document Root, durectory path for serving files from local file system + // used as root for Node#HTTP.Get("file://") requestss + DocRoot string + // This field should be a valid secp256k1 private key that will be used for both // remote peer identification as well as network traffic encryption. If no key // is configured, the preset one is loaded from the data dir, generating it if diff --git a/node/http.go b/node/http.go new file mode 100644 index 0000000000..08f8584571 --- /dev/null +++ b/node/http.go @@ -0,0 +1,139 @@ +package node + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func newHTTP(docRoot string) *HTTPClient { + t := &http.Transport{} + if len(docRoot) > 0 { + t.RegisterProtocol("file", http.NewFileTransport(http.Dir(docRoot))) + } + return &HTTPClient{ + Client: &http.Client{Transport: t}, + transport: t, + schemes: []string{"file"}, + } +} + +func (self *HTTPClient) registerSchemes(schemes []URLScheme) { + for _, scheme := range schemes { + self.schemes = append(self.schemes, scheme.Name) + self.transport.RegisterProtocol(scheme.Name, scheme.RoundTripper) + } +} + +type HTTPClient struct { + *http.Client + transport *http.Transport + schemes []string +} + +// Clients should be reused instead of created as needed. Clients are safe for concurrent use by multiple goroutines. + +// A Client is higher-level than a RoundTripper (such as Transport) and additionally handles HTTP details such as cookies and redirects. + +type URLScheme struct { + Name string + RoundTripper http.RoundTripper +} + +func (self *HTTPClient) HasScheme(scheme string) bool { + for _, s := range self.schemes { + if s == scheme { + return true + } + } + return false +} + +// GetBody(uri) downloads the document at uri, if path is non-empty it +// GetBody(uri) fetches the uri and returns the body +func (self *HTTPClient) GetBody(uri string) ([]byte, error) { + // retrieve body + resp, err := self.Get(uri) + if err != nil { + return nil, err + } + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + var body []byte + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode/100 != 2 { + return body, fmt.Errorf("HTTP error: %s", resp.Status) + } + return body, nil +} + +// Download(uri, path) fetches the uri and writes the body into the file +func (self *HTTPClient) Download(uri, path string) error { + + resp, err := self.Get(uri) + if err != nil { + return err + } + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + var abspath string + abspath, err = filepath.Abs(path) + if err != nil { + return err + } + w, err := os.Create(abspath) + if err != nil { + return err + } + defer w.Close() + _, err = io.Copy(w, resp.Body) + if err != nil { + return err + } + + return nil + +} + +func (self *HTTPClient) GetAuthBody(uri string, hash common.Hash) ([]byte, error) { + // retrieve body + body, err := self.GetBody(uri) + if err != nil { + return nil, err + } + + // check hash to authenticate body + chash := crypto.Sha3Hash(body) + if chash != hash { + return nil, fmt.Errorf("body hash mismatch %x != %x (exp)", hash[:], chash[:]) + } + + return body, nil + +} + +// type HTTPClientAPI interface { +// Get(uri string) (*http.Response, err) +// Download(uri, path string) error +// GetBody(uri) ([]byte, error) +// HasScheme(scheme string) bool +// GetAuthBody(uri string, hash common.Hash) ([]byte, error) +// } diff --git a/common/httpclient/httpclient_test.go b/node/http_test.go similarity index 65% rename from common/httpclient/httpclient_test.go rename to node/http_test.go index 6c3782e15a..ad66a6a396 100644 --- a/common/httpclient/httpclient_test.go +++ b/node/http_test.go @@ -14,9 +14,10 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package httpclient +package node import ( + "errors" "io/ioutil" "net/http" "os" @@ -27,20 +28,20 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -func TestGetAuthContent(t *testing.T) { +func TestGetAuthBody(t *testing.T) { dir, err := ioutil.TempDir("", "httpclient-test") if err != nil { t.Fatal("cannot create temporary directory:", err) } defer os.RemoveAll(dir) - client := New(dir) + client := newHTTP(dir) text := "test" hash := crypto.Sha3Hash([]byte(text)) if err := ioutil.WriteFile(path.Join(dir, "test.content"), []byte(text), os.ModePerm); err != nil { t.Fatal("could not write test file", err) } - content, err := client.GetAuthContent("file:///test.content", hash) + content, err := client.GetAuthBody("file:///test.content", hash) if err != nil { t.Errorf("no error expected, got %v", err) } @@ -49,8 +50,8 @@ func TestGetAuthContent(t *testing.T) { } hash = common.Hash{} - content, err = client.GetAuthContent("file:///test.content", hash) - expected := "content hash mismatch 0000000000000000000000000000000000000000000000000000000000000000 != 9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658 (exp)" + content, err = client.GetAuthBody("file:///test.content", hash) + expected := "body hash mismatch 0000000000000000000000000000000000000000000000000000000000000000 != 9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658 (exp)" if err == nil { t.Errorf("expected error, got nothing") } else { @@ -61,17 +62,26 @@ func TestGetAuthContent(t *testing.T) { } -type rt struct{} +type rterr error +type rt struct{ error } -func (rt) RoundTrip(req *http.Request) (resp *http.Response, err error) { return } +// roundtripper with an error. this can prove the roundtrip for testing +// without having the need to write a Response +func (e rt) RoundTrip(req *http.Request) (resp *http.Response, err error) { + return &http.Response{}, e.error +} -func TestRegisterScheme(t *testing.T) { - client := New("/tmp/") +func TestHasScheme(t *testing.T) { + client := newHTTP("") if client.HasScheme("scheme") { t.Errorf("expected scheme not to be registered") } - client.RegisterScheme("scheme", rt{}) + client.registerSchemes([]URLScheme{{"scheme", &rt{rterr(errors.New("rt"))}}}) if !client.HasScheme("scheme") { t.Errorf("expected scheme to be registered") } + body, err := client.GetBody("scheme://url.com") + if _, ok := err.(rterr); !ok { + t.Fatalf("failed to use registered scheme. Got error %v and body %v", body, err) + } } diff --git a/node/node.go b/node/node.go index 5d7b5869c5..b9c2c0eb5e 100644 --- a/node/node.go +++ b/node/node.go @@ -44,6 +44,7 @@ var ( type Node struct { datadir string // Path to the currently used data directory eventmux *event.TypeMux // Event multiplexer used between the services of a stack + http *HTTPClient // HTTP Client that services can register their URL schemes serverConfig *p2p.Server // Configuration of the underlying P2P networking layer server *p2p.Server // Currently running P2P networking layer @@ -68,6 +69,7 @@ func New(conf *Config) (*Node, error) { if conf.DataDir != "" { nodeDbPath = filepath.Join(conf.DataDir, datadirNodeDatabase) } + httpClient := newHTTP(conf.DocRoot) return &Node{ datadir: conf.DataDir, serverConfig: &p2p.Server{ @@ -87,6 +89,7 @@ func New(conf *Config) (*Node, error) { }, serviceFuncs: []ServiceConstructor{}, eventmux: new(event.TypeMux), + http: httpClient, }, nil } @@ -123,6 +126,7 @@ func (n *Node) Start() error { datadir: n.datadir, services: make(map[reflect.Type]Service), EventMux: n.eventmux, + HTTP: n.http, } for kind, s := range services { // copy needed for threaded access ctx.services[kind] = s @@ -132,6 +136,7 @@ func (n *Node) Start() error { if err != nil { return err } + n.http.registerSchemes(service.URLSchemes()) kind := reflect.TypeOf(service) if _, exists := services[kind]; exists { return &DuplicateServiceError{Kind: kind} @@ -163,6 +168,7 @@ func (n *Node) Start() error { // Mark the service started for potential cleanup started = append(started, kind) } + // Finish initializing the startup n.services = services n.server = running @@ -290,6 +296,10 @@ func (n *Node) APIs() []rpc.API { Version: "1.0", Service: NewPublicDebugAPI(n), Public: true, + }, { + Namespace: "http", + Version: "1.0", + Service: n.http, }, } // Inject all the APIs owned by various services diff --git a/node/node_example_test.go b/node/node_example_test.go index ef41dddaf4..03eb027a20 100644 --- a/node/node_example_test.go +++ b/node/node_example_test.go @@ -31,14 +31,17 @@ import ( // // The following methods are needed to implement a node.Service: // - Protocols() []p2p.Protocol - devp2p protocols the service can communicate on +// - APIs() []rpc.API - APIs exposed to the rpc +// - URLSchemes() []URLScheme - URL schemes handled by the service registered on Node#HTTP // - Start() error - method invoked when the node is ready to start the service // - Stop() error - method invoked when the node terminates the service type SampleService struct{} -func (s *SampleService) Protocols() []p2p.Protocol { return nil } -func (s *SampleService) APIs() []rpc.API { return nil } -func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil } -func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil } +func (s *SampleService) Protocols() []p2p.Protocol { return nil } +func (s *SampleService) APIs() []rpc.API { return nil } +func (s *SampleService) URLSchemes() []node.URLScheme { return nil } +func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil } +func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil } func ExampleUsage() { // Create a network node to run protocols with the default values. The below list diff --git a/node/node_test.go b/node/node_test.go index ef096fc917..b3e4af76cb 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -18,6 +18,7 @@ package node import ( "errors" + "fmt" "io/ioutil" "os" "reflect" @@ -131,6 +132,36 @@ func TestServiceRegistry(t *testing.T) { } } +// Tests whether services can be registered and duplicates caught. +func TestServiceURLSchemes(t *testing.T) { + stack, err := New(testNodeConfig) + if err != nil { + t.Fatalf("failed to create protocol stack: %v", err) + } + var h *HTTPClient + newSchemeUserService := func(ctx *ServiceContext) (Service, error) { + h = ctx.HTTP + return new(SchemeUserService), nil + } + // Register a batch of unique services and ensure they start successfully + services := []ServiceConstructor{NewSchemeService, newSchemeUserService} + for i, constructor := range services { + if err := stack.Register(constructor); err != nil { + t.Fatalf("service #%d: registration failed: %v", i, err) + } + } + if err := stack.Start(); err != nil { + t.Fatalf("failed to start original service stack: %v", err) + } + defer stack.Stop() + + body, err := h.GetBody("rt://url.com") + if _, ok := err.(rterr); !ok { + t.Fatalf("failed to use registered scheme. Got error %v and body %v", body, err) + } + +} + // Tests that registered services get started and stopped correctly. func TestServiceLifeCycle(t *testing.T) { stack, err := New(testNodeConfig) diff --git a/node/service.go b/node/service.go index b83d4c80d9..6dc3c43e31 100644 --- a/node/service.go +++ b/node/service.go @@ -33,6 +33,7 @@ type ServiceContext struct { datadir string // Data directory for protocol persistence services map[reflect.Type]Service // Index of the already constructed services EventMux *event.TypeMux // Event multiplexer used for decoupled notifications + HTTP *HTTPClient // HTTP Client services register their URL schemes with } // OpenDatabase opens an existing database with the given name (or creates one @@ -74,6 +75,9 @@ type Service interface { // APIs retrieves the list of RPC descriptors the service provides APIs() []rpc.API + // Url Schemes to register on the node http client + URLSchemes() []URLScheme + // Start is called after all services have been constructed and the networking // layer was also initialized to spawn any goroutines required by the service. Start(server *p2p.Server) error diff --git a/node/utils_test.go b/node/utils_test.go index 2b7bfadbea..4601b85e23 100644 --- a/node/utils_test.go +++ b/node/utils_test.go @@ -20,6 +20,7 @@ package node import ( + "errors" "reflect" "github.com/ethereum/go-ethereum/p2p" @@ -31,6 +32,7 @@ type NoopService struct{} func (s *NoopService) Protocols() []p2p.Protocol { return nil } func (s *NoopService) APIs() []rpc.API { return nil } +func (s *NoopService) URLSchemes() []URLScheme { return nil } func (s *NoopService) Start(*p2p.Server) error { return nil } func (s *NoopService) Stop() error { return nil } @@ -48,6 +50,26 @@ func NewNoopServiceB(*ServiceContext) (Service, error) { return new(NoopServiceB func NewNoopServiceC(*ServiceContext) (Service, error) { return new(NoopServiceC), nil } func NewNoopServiceD(*ServiceContext) (Service, error) { return new(NoopServiceD), nil } +// Scheme Service is a service that registers a url scheme on the node +type SchemeService struct{ NoopService } + +func NewSchemeService(*ServiceContext) (Service, error) { return new(SchemeService), nil } + +// func (s *NoopService) Protocols() []p2p.Protocol { return nil } +// func (s *NoopService) APIs() []rpc.API { return nil } +// func (s *NoopService) URLSchemes() []URLScheme { return nil } +// func (s *NoopService) Start(*p2p.Server) error { return nil } +// func (s *NoopService) Stop() error { return nil } + +func (*SchemeService) URLSchemes() []URLScheme { + return []URLScheme{{"rt", &rt{rterr(errors.New("rt"))}}} +} + +// Scheme User Service is a service that expects a http client on the service context +type SchemeUserService struct { + NoopService +} + // InstrumentedService is an implementation of Service for which all interface // methods can be instrumented both return value as well as event hook wise. type InstrumentedService struct { @@ -73,6 +95,10 @@ func (s *InstrumentedService) APIs() []rpc.API { return nil } +func (s *InstrumentedService) URLSchemes() []URLScheme { + return nil +} + func (s *InstrumentedService) Start(server *p2p.Server) error { if s.startHook != nil { s.startHook(server) diff --git a/rpc/api/admin.go b/rpc/api/admin.go index 1133c9bca3..541bab8110 100644 --- a/rpc/api/admin.go +++ b/rpc/api/admin.go @@ -24,13 +24,13 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/compiler" - "github.com/ethereum/go-ethereum/common/natspec" - "github.com/ethereum/go-ethereum/common/registrar" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/compiler" + // "github.com/ethereum/go-ethereum/eth/natspec" + "github.com/ethereum/go-ethereum/eth/registrar" "github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p/discover" @@ -426,12 +426,12 @@ func (self *adminApi) RegisterUrl(req *shared.Request) (interface{}, error) { } func (self *adminApi) StartNatSpec(req *shared.Request) (interface{}, error) { - self.ethereum.NatSpec = true + self.ethereum.NatSpecEnabled = true return true, nil } func (self *adminApi) StopNatSpec(req *shared.Request) (interface{}, error) { - self.ethereum.NatSpec = false + self.ethereum.NatSpecEnabled = false return true, nil } @@ -441,13 +441,7 @@ func (self *adminApi) GetContractInfo(req *shared.Request) (interface{}, error) return nil, shared.NewDecodeParamError(err.Error()) } - infoDoc, err := natspec.FetchDocsForContract(args.Contract, self.xeth, self.ethereum.HTTPClient()) - if err != nil { - return nil, err - } - - var info interface{} - err = self.coder.Decode(infoDoc, &info) + info, err := self.ethereum.NatSpec().GetContractInfo(common.HexToAddress(args.Contract)) if err != nil { return nil, err } @@ -461,12 +455,12 @@ func (self *adminApi) HttpGet(req *shared.Request) (interface{}, error) { return nil, shared.NewDecodeParamError(err.Error()) } - resp, err := self.ethereum.HTTPClient().Get(args.Uri, args.Path) + body, err := self.ethereum.HTTP().GetBody(args.Uri) if err != nil { return nil, err } - return string(resp), nil + return string(body), nil } func (self *adminApi) EnableUserAgent(req *shared.Request) (interface{}, error) { diff --git a/rpc/api/admin_args.go b/rpc/api/admin_args.go index e3a2f72bf9..59dfa5df30 100644 --- a/rpc/api/admin_args.go +++ b/rpc/api/admin_args.go @@ -19,7 +19,7 @@ package api import ( "encoding/json" - "github.com/ethereum/go-ethereum/common/compiler" + "github.com/ethereum/go-ethereum/eth/compiler" "github.com/ethereum/go-ethereum/rpc/shared" ) diff --git a/rpc/api/eth.go b/rpc/api/eth.go index db7a643d82..96bc6d5edd 100644 --- a/rpc/api/eth.go +++ b/rpc/api/eth.go @@ -24,8 +24,8 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/natspec" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/natspec" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc/codec" "github.com/ethereum/go-ethereum/rpc/shared" @@ -372,10 +372,11 @@ func (self *ethApi) GetNatSpec(req *shared.Request) (interface{}, error) { return nil, shared.NewDecodeParamError(err.Error()) } - var jsontx = fmt.Sprintf(`{"params":[{"to":"%s","data": "%s"}]}`, args.To, args.Data) - notice := natspec.GetNotice(self.xeth, jsontx, self.ethereum.HTTPClient()) - - return notice, nil + tx := &natspec.SendTxArgs{ + To: common.HexToAddress(args.To), + Data: args.Data, + } + return self.ethereum.NatSpec().GetNatSpec(tx) } func (self *ethApi) EstimateGas(req *shared.Request) (interface{}, error) { diff --git a/whisper/whisper.go b/whisper/whisper.go index e99950b760..bf23bb2404 100644 --- a/whisper/whisper.go +++ b/whisper/whisper.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/event/filter" "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" rpc "github.com/ethereum/go-ethereum/rpc/v2" @@ -112,6 +113,8 @@ func (s *Whisper) APIs() []rpc.API { } } +func (self *Whisper) URLSchemes() []node.URLScheme { return nil } + // Protocols returns the whisper sub-protocols ran by this particular client. func (self *Whisper) Protocols() []p2p.Protocol { return []p2p.Protocol{self.protocol} diff --git a/xeth/xeth.go b/xeth/xeth.go index 5a5399a3e7..e54a06242d 100644 --- a/xeth/xeth.go +++ b/xeth/xeth.go @@ -29,13 +29,13 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/compiler" "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/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/compiler" "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" @@ -865,7 +865,8 @@ func (self *XEth) Call(fromStr, toStr, valueStr, gasStr, gasPriceStr, dataStr st return common.ToHex(res), gas.String(), err } -func (self *XEth) ConfirmTransaction(tx string) bool { +func (self *XEth) ConfirmTransaction(to, data string) bool { + tx := fmt.Sprintf(`{"params":[{"to":"%s","data": "%s"}]}`, to, data) return self.frontend.ConfirmTransaction(tx) } @@ -962,9 +963,7 @@ func (self *XEth) SignTransaction(fromStr, toStr, nonceStr, valueStr, gasStr, ga func (self *XEth) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) { - // this minimalistic recoding is enough (works for natspec.js) - var jsontx = fmt.Sprintf(`{"params":[{"to":"%s","data": "%s"}]}`, toStr, codeStr) - if !self.ConfirmTransaction(jsontx) { + if !self.ConfirmTransaction(toStr, codeStr) { err := fmt.Errorf("Transaction not confirmed") return "", err }