diff options
Diffstat (limited to 'common')
-rw-r--r-- | common/compiler/solidity.go | 187 | ||||
-rw-r--r-- | common/compiler/solidity_test.go | 89 | ||||
-rw-r--r-- | common/natspec/natspec.go | 134 | ||||
-rw-r--r-- | common/natspec/natspec_e2e_test.go | 328 | ||||
-rw-r--r-- | common/natspec/natspec_test.go | 89 | ||||
-rw-r--r-- | common/resolver/resolver.go | 154 | ||||
-rw-r--r-- | common/resolver/resolver_test.go | 29 |
7 files changed, 641 insertions, 369 deletions
diff --git a/common/compiler/solidity.go b/common/compiler/solidity.go new file mode 100644 index 000000000..36d0e96cc --- /dev/null +++ b/common/compiler/solidity.go @@ -0,0 +1,187 @@ +package compiler + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +const ( + flair = "Christian <c@ethdev.com> and Lefteris <lefteris@ethdev.com> (c) 2014-2015" + languageVersion = "0" +) + +var ( + versionRegExp = regexp.MustCompile("[0-9]+.[0-9]+.[0-9]+") + params = []string{ + "--binary", // Request to output the contract in binary (hexadecimal). + "file", // + "--json-abi", // Request to output the contract's JSON ABI interface. + "file", // + "--natspec-user", // Request to output the contract's Natspec user documentation. + "file", // + "--natspec-dev", // Request to output the contract's Natspec developer documentation. + "file", + } +) + +type Contract struct { + Code string `json:"code"` + Info ContractInfo `json:"info"` +} + +type ContractInfo struct { + Source string `json:"source"` + Language string `json:"language"` + LanguageVersion string `json:"languageVersion"` + CompilerVersion string `json:"compilerVersion"` + AbiDefinition interface{} `json:"abiDefinition"` + UserDoc interface{} `json:"userDoc"` + DeveloperDoc interface{} `json:"developerDoc"` +} + +type Solidity struct { + solcPath string + version string +} + +func New(solcPath string) (sol *Solidity, err error) { + // set default solc + if len(solcPath) == 0 { + solcPath = "solc" + } + solcPath, err = exec.LookPath(solcPath) + if err != nil { + return + } + + cmd := exec.Command(solcPath, "--version") + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + return + } + + version := versionRegExp.FindString(out.String()) + sol = &Solidity{ + solcPath: solcPath, + version: version, + } + glog.V(logger.Info).Infoln(sol.Info()) + return +} + +func (sol *Solidity) Info() string { + return fmt.Sprintf("solc v%s\nSolidity Compiler: %s\n%s", sol.version, sol.solcPath, flair) +} + +func (sol *Solidity) Compile(source string) (contract *Contract, err error) { + + if len(source) == 0 { + err = fmt.Errorf("empty source") + return + } + + wd, err := ioutil.TempDir("", "solc") + if err != nil { + return + } + defer os.RemoveAll(wd) + + in := strings.NewReader(source) + var out bytes.Buffer + // cwd set to temp dir + cmd := exec.Command(sol.solcPath, params...) + cmd.Dir = wd + cmd.Stdin = in + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + err = fmt.Errorf("solc error: %v", err) + return + } + + matches, _ := filepath.Glob(wd + "/*.binary") + if len(matches) < 1 { + err = fmt.Errorf("solc error: missing code output") + return + } + if len(matches) > 1 { + err = fmt.Errorf("multi-contract sources are not supported") + return + } + _, file := filepath.Split(matches[0]) + base := strings.Split(file, ".")[0] + + codeFile := path.Join(wd, base+".binary") + abiDefinitionFile := path.Join(wd, base+".abi") + userDocFile := path.Join(wd, base+".docuser") + developerDocFile := path.Join(wd, base+".docdev") + + code, err := ioutil.ReadFile(codeFile) + if err != nil { + err = fmt.Errorf("error reading compiler output for code: %v", err) + return + } + abiDefinitionJson, err := ioutil.ReadFile(abiDefinitionFile) + if err != nil { + err = fmt.Errorf("error reading compiler output for abiDefinition: %v", err) + return + } + var abiDefinition interface{} + err = json.Unmarshal(abiDefinitionJson, &abiDefinition) + + userDocJson, err := ioutil.ReadFile(userDocFile) + if err != nil { + err = fmt.Errorf("error reading compiler output for userDoc: %v", err) + return + } + var userDoc interface{} + err = json.Unmarshal(userDocJson, &userDoc) + + developerDocJson, err := ioutil.ReadFile(developerDocFile) + if err != nil { + err = fmt.Errorf("error reading compiler output for developerDoc: %v", err) + return + } + var developerDoc interface{} + err = json.Unmarshal(developerDocJson, &developerDoc) + + contract = &Contract{ + Code: string(code), + Info: ContractInfo{ + Source: source, + Language: "Solidity", + LanguageVersion: languageVersion, + CompilerVersion: sol.version, + AbiDefinition: abiDefinition, + UserDoc: userDoc, + DeveloperDoc: developerDoc, + }, + } + + return +} + +func ExtractInfo(contract *Contract, filename string) (contenthash common.Hash, err error) { + contractInfo, err := json.Marshal(contract.Info) + if err != nil { + return + } + contenthash = common.BytesToHash(crypto.Sha3(contractInfo)) + err = ioutil.WriteFile(filename, contractInfo, 0600) + return +} diff --git a/common/compiler/solidity_test.go b/common/compiler/solidity_test.go new file mode 100644 index 000000000..8fdcb6a99 --- /dev/null +++ b/common/compiler/solidity_test.go @@ -0,0 +1,89 @@ +package compiler + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +var ( + source = ` +contract test { + /// @notice Will multiply ` + "`a`" + ` by 7. + function multiply(uint a) returns(uint d) { + return a * 7; + } +} +` + code = "605280600c6000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063c6888fa114602e57005b60376004356041565b8060005260206000f35b6000600782029050604d565b91905056" + info = `{"source":"\ncontract test {\n /// @notice Will multiply ` + "`a`" + ` by 7.\n function multiply(uint a) returns(uint d) {\n return a * 7;\n }\n}\n","language":"Solidity","languageVersion":"0","compilerVersion":"0.9.13","abiDefinition":[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"type":"function"}],"userDoc":{"methods":{"multiply(uint256)":{"notice":"Will multiply ` + "`a`" + ` by 7."}}},"developerDoc":{"methods":{}}}` + + infohash = common.HexToHash("0xfdb031637e8a1c1891143f8d129ebc7f7c4e4b41ecad8c85abe1756190f74204") +) + +func TestCompiler(t *testing.T) { + sol, err := New("") + if err != nil { + t.Skip("no solc installed") + } + contract, err := sol.Compile(source) + if err != nil { + t.Errorf("error compiling source. result %v: %v", contract, err) + return + } + if contract.Code != code { + t.Errorf("wrong code, expected\n%s, got\n%s", code, contract.Code) + } +} + +func TestCompileError(t *testing.T) { + sol, err := New("") + if err != nil { + t.Skip("no solc installed") + } + contract, err := sol.Compile(source[2:]) + if err == nil { + t.Errorf("error expected compiling source. got none. result %v", contract) + return + } +} + +func TestNoCompiler(t *testing.T) { + _, err := New("/path/to/solc") + if err != nil { + t.Log("solidity quits with error: %v", err) + } else { + t.Errorf("no solc installed, but got no error") + } +} + +func TestExtractInfo(t *testing.T) { + var cinfo ContractInfo + err := json.Unmarshal([]byte(info), &cinfo) + if err != nil { + t.Errorf("%v", err) + } + contract := &Contract{ + Code: "", + Info: cinfo, + } + filename := "/tmp/solctest.info.json" + os.Remove(filename) + cinfohash, err := ExtractInfo(contract, filename) + if err != nil { + t.Errorf("%v", err) + } + got, err := ioutil.ReadFile(filename) + if err != nil { + t.Errorf("%v", err) + } + if string(got) != info { + t.Errorf("incorrect info.json extracted, expected:\n%s\ngot\n%s", info, string(got)) + } + if cinfohash != infohash { + t.Errorf("content hash for info is incorrect. expected %v, got %v", infohash.Hex(), cinfohash.Hex()) + } +} diff --git a/common/natspec/natspec.go b/common/natspec/natspec.go index 38e7c1a9d..7e5f053c7 100644 --- a/common/natspec/natspec.go +++ b/common/natspec/natspec.go @@ -17,118 +17,119 @@ import ( type abi2method map[[8]byte]*method type NatSpec struct { - jsvm *otto.Otto - userDocJson, abiDocJson []byte - userDoc userDoc - tx, data string - // abiDoc abiDoc -} - -func getFallbackNotice(comment, tx string) string { - - return "About to submit transaction (" + comment + "): " + tx - + jsvm *otto.Otto + abiDocJson []byte + userDoc userDoc + tx, data string } +// 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 *docserver.DocServer) (notice string) { - ns, err := New(xeth, tx, http) if err != nil { if ns == nil { - return getFallbackNotice("no NatSpec info found for contract", tx) + return getFallbackNotice(fmt.Sprintf("no NatSpec info found for contract: %v", err), tx) } else { - return getFallbackNotice("invalid NatSpec info", tx) + return getFallbackNotice(fmt.Sprintf("invalid NatSpec info: %v", err), tx) } } - notice, err2 := ns.Notice() - - if err2 != nil { - return getFallbackNotice("NatSpec notice error \""+err2.Error()+"\"", tx) + notice, err = ns.Notice() + if err != nil { + return getFallbackNotice(fmt.Sprintf("NatSpec notice error: %v", err), tx) } return +} +func getFallbackNotice(comment, tx string) string { + return fmt.Sprintf("About to submit transaction (%s): %s", comment, tx) } -func New(xeth *xeth.XEth, tx string, http *docserver.DocServer) (self *NatSpec, err error) { +type transaction struct { + To string `json:"to"` + Data string `json:"data"` +} - // extract contract address from tx +type jsonTx struct { + Params []transaction `json:"params"` +} + +type contractInfo struct { + Source string `json:"source"` + Language string `json:"language"` + Version string `json:"compilerVersion"` + AbiDefinition json.RawMessage `json:"abiDefinition"` + UserDoc userDoc `json:"userDoc"` + DeveloperDoc json.RawMessage `json:"developerDoc"` +} - var obj map[string]json.RawMessage - err = json.Unmarshal([]byte(tx), &obj) +func New(xeth *xeth.XEth, jsontx string, http *docserver.DocServer) (self *NatSpec, err error) { + + // extract contract address from tx + var tx jsonTx + err = json.Unmarshal([]byte(jsontx), &tx) if err != nil { return } - var tmp []map[string]string - err = json.Unmarshal(obj["params"], &tmp) + t := tx.Params[0] + contractAddress := t.To + + content, err := FetchDocsForContract(contractAddress, xeth, http) if err != nil { return } - contractAddress := tmp[0]["to"] + self, err = NewWithDocs(content, jsontx, t.Data) + return +} + +// also called by admin.contractInfo.get +func FetchDocsForContract(contractAddress string, xeth *xeth.XEth, http *docserver.DocServer) (content []byte, err error) { // retrieve contract hash from state - if !xeth.IsContract(contractAddress) { - err = fmt.Errorf("NatSpec error: contract not found") - return - } codehex := xeth.CodeAt(contractAddress) - codeHash := common.BytesToHash(crypto.Sha3(common.Hex2Bytes(codehex[2:]))) - // parse out host/domain + codeb := xeth.CodeAtBytes(contractAddress) + if codehex == "0x" { + err = fmt.Errorf("contract (%v) not found", contractAddress) + return + } + codehash := common.BytesToHash(crypto.Sha3(codeb)) // set up nameresolver with natspecreg + urlhint contract addresses - res := resolver.New( - xeth, - resolver.URLHintContractAddress, - resolver.HashRegContractAddress, - ) + res := resolver.New(xeth) // resolve host via HashReg/UrlHint Resolver - uri, hash, err := res.KeyToUrl(codeHash) + uri, hash, err := res.KeyToUrl(codehash) if err != nil { return } // get content via http client and authenticate content using hash - content, err := http.GetAuthContent(uri, hash) - if err != nil { - return - } - - // get abi, userdoc - var obj2 map[string]json.RawMessage - err = json.Unmarshal(content, &obj2) + content, err = http.GetAuthContent(uri, hash) if err != nil { return } - abi := []byte(obj2["abi"]) - userdoc := []byte(obj2["userdoc"]) - - self, err = NewWithDocs(abi, userdoc, tx) return } -func NewWithDocs(abiDocJson, userDocJson []byte, tx string) (self *NatSpec, err error) { +func NewWithDocs(infoDoc []byte, tx string, data string) (self *NatSpec, err error) { - var obj map[string]json.RawMessage - err = json.Unmarshal([]byte(tx), &obj) + var contract contractInfo + err = json.Unmarshal(infoDoc, &contract) if err != nil { return } - var tmp []map[string]string - err = json.Unmarshal(obj["params"], &tmp) - if err != nil { - return - } - data := tmp[0]["data"] self = &NatSpec{ - jsvm: otto.New(), - abiDocJson: abiDocJson, - userDocJson: userDocJson, - tx: tx, - data: data, + jsvm: otto.New(), + abiDocJson: []byte(contract.AbiDefinition), + userDoc: contract.UserDoc, + tx: tx, + data: data, } // load and require natspec js (but it is meant to be protected environment) @@ -137,13 +138,6 @@ func NewWithDocs(abiDocJson, userDocJson []byte, tx string) (self *NatSpec, err return } _, err = self.jsvm.Run("var natspec = require('natspec');") - if err != nil { - return - } - - err = json.Unmarshal(userDocJson, &self.userDoc) - // err = parseAbiJson(abiDocJson, &self.abiDoc) - return } diff --git a/common/natspec/natspec_e2e_test.go b/common/natspec/natspec_e2e_test.go index e54b9ee96..f9b0c1dcc 100644 --- a/common/natspec/natspec_e2e_test.go +++ b/common/natspec/natspec_e2e_test.go @@ -1,8 +1,8 @@ package natspec import ( + "fmt" "io/ioutil" - "math/big" "os" "testing" @@ -14,39 +14,26 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/rpc" xe "github.com/ethereum/go-ethereum/xeth" ) -type testFrontend struct { - t *testing.T - ethereum *eth.Ethereum - xeth *xe.XEth - api *rpc.EthereumApi - coinbase string - stateDb *state.StateDB - txc uint64 - lastConfirm string - makeNatSpec bool -} - const ( - testAccount = "e273f01c99144c438695e10f24926dc1f9fbf62d" - testBalance = "1000000000000" -) + testBalance = "10000000000000000000" -const testFileName = "long_file_name_for_testing_registration_of_URLs_longer_than_32_bytes.content" + testFileName = "long_file_name_for_testing_registration_of_URLs_longer_than_32_bytes.content" -const testNotice = "Register key `utils.toHex(_key)` <- content `utils.toHex(_content)`" -const testExpNotice = "Register key 0xadd1a7d961cff0242089674ec2ef6fca671ab15e1fe80e38859fc815b98d88ab <- content 0xc00d5bcc872e17813df6ec5c646bb281a6e2d3b454c2c400c78192adf3344af9" -const testExpNotice2 = `About to submit transaction (NatSpec notice error "abi key does not match any method"): {"id":6,"jsonrpc":"2.0","method":"eth_transact","params":[{"from":"0xe273f01c99144c438695e10f24926dc1f9fbf62d","to":"0xb737b91f8e95cf756766fc7c62c9a8ff58470381","value":"100000000000","gas":"100000","gasPrice":"100000","data":"0x31e12c20"}]}` -const testExpNotice3 = `About to submit transaction (no NatSpec info found for contract): {"id":6,"jsonrpc":"2.0","method":"eth_transact","params":[{"from":"0xe273f01c99144c438695e10f24926dc1f9fbf62d","to":"0x8b839ad85686967a4f418eccc81962eaee314ac3","value":"100000000000","gas":"100000","gasPrice":"100000","data":"0x300a3bbfc00d5bcc872e17813df6ec5c646bb281a6e2d3b454c2c400c78192adf3344af900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"}]}` + testNotice = "Register key `utils.toHex(_key)` <- content `utils.toHex(_content)`" -const testUserDoc = ` + testExpNotice = "Register key 0xadd1a7d961cff0242089674ec2ef6fca671ab15e1fe80e38859fc815b98d88ab <- content 0xb3a2dea218de5d8bbe6c4645aadbf67b5ab00ecb1a9ec95dbdad6a0eed3e41a7" + + testExpNotice2 = `About to submit transaction (NatSpec notice error: abi key does not match any method): {"params":[{"to":"%s","data": "0x31e12c20"}]}` + + testExpNotice3 = `About to submit transaction (no NatSpec info found for contract: content hash not found for '0x1392c62d05b2d149e22a339c531157ae06b44d39a674cce500064b12b9aeb019'): {"params":[{"to":"%s","data": "0x300a3bbfb3a2dea218de5d8bbe6c4645aadbf67b5ab00ecb1a9ec95dbdad6a0eed3e41a7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066696c653a2f2f2f746573742e636f6e74656e74"}]}` +) + +const ( + testUserDoc = ` { - "source": "...", - "language": "Solidity", - "languageVersion": 1, "methods": { "register(uint256,uint256)": { "notice": "` + testNotice + `" @@ -60,8 +47,7 @@ const testUserDoc = ` ] } ` - -const testABI = ` + testAbiDefinition = ` [{ "name": "register", "constant": false, @@ -77,70 +63,81 @@ const testABI = ` }] ` -const testDocs = ` + testContractInfo = ` { - "userdoc": ` + testUserDoc + `, - "abi": ` + testABI + ` + "userDoc": ` + testUserDoc + `, + "abiDefinition": ` + testAbiDefinition + ` } ` +) + +type testFrontend struct { + t *testing.T + // resolver *resolver.Resolver + ethereum *eth.Ethereum + xeth *xe.XEth + coinbase common.Address + stateDb *state.StateDB + txc uint64 + lastConfirm string + wantNatSpec bool +} -func (f *testFrontend) UnlockAccount(acc []byte) bool { - f.t.Logf("Unlocking account %v\n", common.Bytes2Hex(acc)) - f.ethereum.AccountManager().Unlock(acc, "password") +func (self *testFrontend) UnlockAccount(acc []byte) bool { + self.ethereum.AccountManager().Unlock(acc, "password") return true } -func (f *testFrontend) ConfirmTransaction(tx string) bool { - //f.t.Logf("ConfirmTransaction called tx = %v", tx) - if f.makeNatSpec { +func (self *testFrontend) ConfirmTransaction(tx string) bool { + if self.wantNatSpec { ds, err := docserver.New("/tmp/") if err != nil { - f.t.Errorf("Error creating DocServer: %v", err) + self.t.Errorf("Error creating DocServer: %v", err) } - f.lastConfirm = GetNotice(f.xeth, tx, ds) + self.lastConfirm = GetNotice(self.xeth, tx, ds) } return true } -var port = 30300 - func testEth(t *testing.T) (ethereum *eth.Ethereum, err error) { + os.RemoveAll("/tmp/eth-natspec/") - err = os.MkdirAll("/tmp/eth-natspec/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/", os.ModePerm) + + err = os.MkdirAll("/tmp/eth-natspec/keys", os.ModePerm) if err != nil { - t.Errorf("%v", err) - return + panic(err) } - err = os.MkdirAll("/tmp/eth-natspec/data", os.ModePerm) + + // create a testAddress + ks := crypto.NewKeyStorePassphrase("/tmp/eth-natspec/keys") + am := accounts.NewManager(ks) + testAccount, err := am.NewAccount("password") if err != nil { - t.Errorf("%v", err) - return + panic(err) } - ks := crypto.NewKeyStorePlain("/tmp/eth-natspec/keys") - ioutil.WriteFile("/tmp/eth-natspec/keys/e273f01c99144c438695e10f24926dc1f9fbf62d/e273f01c99144c438695e10f24926dc1f9fbf62d", - []byte(`{"Id":"RhRXD+fNRKS4jx+7ZfEsNA==","Address":"4nPwHJkUTEOGleEPJJJtwfn79i0=","PrivateKey":"h4ACVpe74uIvi5Cg/2tX/Yrm2xdr3J7QoMbMtNX2CNc="}`), os.ModePerm) + testAddress := common.Bytes2Hex(testAccount.Address) + + // set up mock genesis with balance on the testAddress + core.GenesisData = []byte(`{ + "` + testAddress + `": {"balance": "` + testBalance + `"} + }`) - port++ + // only use minimalistic stack with no networking ethereum, err = eth.New(ð.Config{ DataDir: "/tmp/eth-natspec", - AccountManager: accounts.NewManager(ks), - Name: "test", + AccountManager: am, + MaxPeers: 0, }) if err != nil { - t.Errorf("%v", err) - return + panic(err) } return } func testInit(t *testing.T) (self *testFrontend) { - - core.GenesisData = []byte(`{ - "` + testAccount + `": {"balance": "` + testBalance + `"} - }`) - + // initialise and start minimal ethereum stack ethereum, err := testEth(t) if err != nil { t.Errorf("error creating ethereum: %v", err) @@ -152,190 +149,95 @@ func testInit(t *testing.T) (self *testFrontend) { return } + // mock frontend self = &testFrontend{t: t, ethereum: ethereum} self.xeth = xe.New(ethereum, self) - self.api = rpc.NewEthereumApi(self.xeth) - addr := self.xeth.Coinbase() + addr, _ := ethereum.Etherbase() self.coinbase = addr - if addr != "0x"+testAccount { - t.Errorf("CoinBase %v does not match TestAccount 0x%v", addr, testAccount) - } - t.Logf("CoinBase is %v", addr) - - balance := self.xeth.BalanceAt(testAccount) - /*if balance != core.TestBalance { - t.Errorf("Balance %v does not match TestBalance %v", balance, core.TestBalance) - }*/ - t.Logf("Balance is %v", balance) - self.stateDb = self.ethereum.ChainManager().State().Copy() - return - -} - -func (self *testFrontend) insertTx(addr, contract, fnsig string, args []string) { - - //cb := common.HexToAddress(self.coinbase) - //coinbase := self.ethereum.ChainManager().State().GetStateObject(cb) + // initialise the registry contracts + // self.resolver.CreateContracts(addr) + resolver.New(self.xeth).CreateContracts(addr) + self.applyTxs() + // t.Logf("HashReg contract registered at %v", resolver.HashRegContractAddress) + // t.Logf("URLHint contract registered at %v", resolver.UrlHintContractAddress) - hash := common.Bytes2Hex(crypto.Sha3([]byte(fnsig))) - data := "0x" + hash[0:8] - for _, arg := range args { - data = data + common.Bytes2Hex(common.Hex2BytesFixed(arg, 32)) - } - self.t.Logf("Tx data: %v", data) - - jsontx := ` -[{ - "from": "` + addr + `", - "to": "` + contract + `", - "value": "100000000000", - "gas": "100000", - "gasPrice": "100000", - "data": "` + data + `" -}] -` - req := &rpc.RpcRequest{ - Jsonrpc: "2.0", - Method: "eth_transact", - Params: []byte(jsontx), - Id: 6, - } - - var reply interface{} - err0 := self.api.GetRequestReply(req, &reply) - if err0 != nil { - self.t.Errorf("GetRequestReply error: %v", err0) - } - - //self.xeth.Transact(addr, contract, "100000000000", "100000", "100000", data) + return } +// this is needed for transaction to be applied to the state in testing +// the heavy lifing is done in XEth.ApplyTestTxs +// this is fragile, +// and does process leaking since xeth loops cannot quit safely +// should be replaced by proper mining with testDAG for easy full integration tests func (self *testFrontend) applyTxs() { - - cb := common.HexToAddress(self.coinbase) - block := self.ethereum.ChainManager().NewBlock(cb) - coinbase := self.stateDb.GetStateObject(cb) - coinbase.SetGasPool(big.NewInt(10000000)) - txs := self.ethereum.TxPool().GetQueuedTransactions() - - for i := 0; i < len(txs); i++ { - for _, tx := range txs { - //self.t.Logf("%v %v %v", i, tx.Nonce(), self.txc) - if tx.Nonce() == self.txc { - _, gas, err := core.ApplyMessage(core.NewEnv(self.stateDb, self.ethereum.ChainManager(), tx, block), tx, coinbase) - //self.ethereum.TxPool().RemoveSet([]*types.Transaction{tx}) - self.t.Logf("ApplyMessage: gas %v err %v", gas, err) - self.txc++ - } - } - } - - //self.ethereum.TxPool().RemoveSet(txs) - self.xeth = self.xeth.WithState(self.stateDb) - -} - -func (self *testFrontend) registerURL(hash common.Hash, url string) { - hashHex := common.Bytes2Hex(hash[:]) - urlBytes := []byte(url) - var bb bool = true - var cnt byte - for bb { - bb = len(urlBytes) > 0 - urlb := urlBytes - if len(urlb) > 32 { - urlb = urlb[:32] - } - urlHex := common.Bytes2Hex(urlb) - self.insertTx(self.coinbase, resolver.URLHintContractAddress, "register(uint256,uint8,uint256)", []string{hashHex, common.Bytes2Hex([]byte{cnt}), urlHex}) - if len(urlBytes) > 32 { - urlBytes = urlBytes[32:] - } else { - urlBytes = nil - } - cnt++ - } -} - -func (self *testFrontend) setOwner() { - - self.insertTx(self.coinbase, resolver.HashRegContractAddress, "setowner()", []string{}) - - /*owner := self.xeth.StorageAt("0x"+resolver.HashRegContractAddress, "0x0000000000000000000000000000000000000000000000000000000000000000") - self.t.Logf("owner = %v", owner) - if owner != self.coinbase { - self.t.Errorf("setowner() unsuccessful, owner != coinbase") - }*/ -} - -func (self *testFrontend) registerNatSpec(codehash, dochash common.Hash) { - - codeHex := common.Bytes2Hex(codehash[:]) - docHex := common.Bytes2Hex(dochash[:]) - self.insertTx(self.coinbase, resolver.HashRegContractAddress, "register(uint256,uint256)", []string{codeHex, docHex}) -} - -func (self *testFrontend) testResolver() *resolver.Resolver { - return resolver.New(self.xeth, resolver.URLHintContractAddress, resolver.HashRegContractAddress) + self.txc, self.xeth = self.xeth.ApplyTestTxs(self.stateDb, self.coinbase, self.txc) + return } +// end to end test func TestNatspecE2E(t *testing.T) { - t.Skip() + // t.Skip() tf := testInit(t) defer tf.ethereum.Stop() - resolver.CreateContracts(tf.xeth, testAccount) - t.Logf("URLHint contract registered at %v", resolver.URLHintContractAddress) - t.Logf("HashReg contract registered at %v", resolver.HashRegContractAddress) - tf.applyTxs() - - ioutil.WriteFile("/tmp/"+testFileName, []byte(testDocs), os.ModePerm) - dochash := common.BytesToHash(crypto.Sha3([]byte(testDocs))) + // create a contractInfo file (mock cloud-deployed contract metadocs) + // incidentally this is the info for the registry contract itself + ioutil.WriteFile("/tmp/"+testFileName, []byte(testContractInfo), os.ModePerm) + dochash := common.BytesToHash(crypto.Sha3([]byte(testContractInfo))) - codehex := tf.xeth.CodeAt(resolver.HashRegContractAddress) - codehash := common.BytesToHash(crypto.Sha3(common.Hex2Bytes(codehex[2:]))) + // take the codehash for the contract we wanna test + // codehex := tf.xeth.CodeAt(resolver.HashRegContractAddress) + codeb := tf.xeth.CodeAtBytes(resolver.HashRegContractAddress) + codehash := common.BytesToHash(crypto.Sha3(codeb)) - tf.setOwner() - tf.registerNatSpec(codehash, dochash) - tf.registerURL(dochash, "file:///"+testFileName) - tf.applyTxs() - - chash, err := tf.testResolver().KeyToContentHash(codehash) + // use resolver to register codehash->dochash->url + registry := resolver.New(tf.xeth) + _, err := registry.Register(tf.coinbase, codehash, dochash, "file:///"+testFileName) if err != nil { - t.Errorf("Can't find content hash") - } - t.Logf("chash = %x err = %v", chash, err) - url, err2 := tf.testResolver().ContentHashToUrl(dochash) - if err2 != nil { - t.Errorf("Can't find URL hint") + t.Errorf("error registering: %v", err) } - t.Logf("url = %v err = %v", url, err2) + // apply txs to the state + tf.applyTxs() // NatSpec info for register method of HashReg contract installed // now using the same transactions to check confirm messages - tf.makeNatSpec = true - tf.registerNatSpec(codehash, dochash) - t.Logf("Confirm message: %v\n", tf.lastConfirm) + tf.wantNatSpec = true // this is set so now the backend uses natspec confirmation + _, err = registry.RegisterContentHash(tf.coinbase, codehash, dochash) + if err != nil { + t.Errorf("error calling contract registry: %v", err) + } + if tf.lastConfirm != testExpNotice { - t.Errorf("Wrong confirm message, expected '%v', got '%v'", testExpNotice, tf.lastConfirm) + t.Errorf("Wrong confirm message. expected '%v', got '%v'", testExpNotice, tf.lastConfirm) } - tf.setOwner() - t.Logf("Confirm message for unknown method: %v\n", tf.lastConfirm) - if tf.lastConfirm != testExpNotice2 { - t.Errorf("Wrong confirm message, expected '%v', got '%v'", testExpNotice2, tf.lastConfirm) + // test unknown method + exp := fmt.Sprintf(testExpNotice2, resolver.HashRegContractAddress) + _, err = registry.SetOwner(tf.coinbase) + if err != nil { + t.Errorf("error setting owner: %v", err) + } + + if tf.lastConfirm != exp { + t.Errorf("Wrong confirm message, expected '%v', got '%v'", exp, tf.lastConfirm) + } + + // test unknown contract + exp = fmt.Sprintf(testExpNotice3, resolver.UrlHintContractAddress) + + _, err = registry.RegisterUrl(tf.coinbase, dochash, "file:///test.content") + if err != nil { + t.Errorf("error registering: %v", err) } - tf.registerURL(dochash, "file:///test.content") - t.Logf("Confirm message for unknown contract: %v\n", tf.lastConfirm) - if tf.lastConfirm != testExpNotice3 { - t.Errorf("Wrong confirm message, expected '%v', got '%v'", testExpNotice3, tf.lastConfirm) + if tf.lastConfirm != exp { + t.Errorf("Wrong confirm message, expected '%v', got '%v'", exp, tf.lastConfirm) } } diff --git a/common/natspec/natspec_test.go b/common/natspec/natspec_test.go index 35a59469a..05df9e750 100644 --- a/common/natspec/natspec_test.go +++ b/common/natspec/natspec_test.go @@ -4,70 +4,65 @@ import ( "testing" ) -func makeUserdoc(desc string) []byte { +func makeInfoDoc(desc string) []byte { return []byte(` { - "source": "...", + "source": "contract test { }", "language": "Solidity", - "languageVersion": 1, - "methods": { - "multiply(uint256)": { - "notice": "` + desc + `" + "compilerVersion": "1", + "userDoc": { + "methods": { + "multiply(uint256)": { + "notice": "` + desc + `" + }, + "balance(address)": { + "notice": "` + "`(balanceInmGAV / 1000).fixed(0,3)`" + ` GAV is the total funds available to ` + "`who.address()`." + `" + } }, - "balance(address)": { - "notice": "` + "`(balanceInmGAV / 1000).fixed(0,3)`" + ` GAV is the total funds available to ` + "`who.address()`." + `" - } + "invariants": [ + { "notice": "The sum total amount of GAV in the system is 1 million." } + ], + "construction": [ + { "notice": "Endows ` + "`message.caller.address()`" + ` with 1m GAV." } + ] }, - "invariants": [ - { "notice": "The sum total amount of GAV in the system is 1 million." } - ], - "construction": [ - { "notice": "Endows ` + "`message.caller.address()`" + ` with 1m GAV." } - ] -} -`) + "abiDefinition": [{ + "name": "multiply", + "constant": false, + "type": "function", + "inputs": [{ + "name": "a", + "type": "uint256" + }], + "outputs": [{ + "name": "d", + "type": "uint256" + }] + }] +}`) } var data = "0xc6888fa1000000000000000000000000000000000000000000000000000000000000007a" var tx = ` { - "jsonrpc": "2.0", - "method": "eth_call", "params": [{ "to": "0x8521742d3f456bd237e312d6e30724960f72517a", "data": "0xc6888fa1000000000000000000000000000000000000000000000000000000000000007a" }], - "id": 6 } ` -var abi = []byte(` -[{ - "name": "multiply", - "constant": false, - "type": "function", - "inputs": [{ - "name": "a", - "type": "uint256" - }], - "outputs": [{ - "name": "d", - "type": "uint256" - }] -}] -`) - func TestNotice(t *testing.T) { desc := "Will multiply `a` by 7 and return `a * 7`." expected := "Will multiply 122 by 7 and return 854." - userdoc := makeUserdoc(desc) - - ns, err := NewWithDocs(abi, userdoc, tx) + infodoc := makeInfoDoc(desc) + ns, err := NewWithDocs(infodoc, tx, data) if err != nil { t.Errorf("New: error: %v", err) + return } notice, err := ns.Notice() @@ -78,8 +73,6 @@ func TestNotice(t *testing.T) { if notice != expected { t.Errorf("incorrect notice. expected %v, got %v", expected, notice) - } else { - t.Logf("returned notice \"%v\"", notice) } } @@ -87,10 +80,10 @@ func TestNotice(t *testing.T) { func TestMissingMethod(t *testing.T) { desc := "Will multiply `a` by 7 and return `a * 7`." - userdoc := makeUserdoc(desc) expected := "natspec.js error evaluating expression: Natspec evaluation failed, method does not exist" - ns, err := NewWithDocs(abi, userdoc, tx) + infodoc := makeInfoDoc(desc) + ns, err := NewWithDocs(infodoc, tx, data) if err != nil { t.Errorf("New: error: %v", err) } @@ -113,9 +106,8 @@ func TestInvalidDesc(t *testing.T) { desc := "Will multiply 122 by \"7\" and return 854." expected := "invalid character '7' after object key:value pair" - userdoc := makeUserdoc(desc) - - _, err := NewWithDocs(abi, userdoc, tx) + infodoc := makeInfoDoc(desc) + _, err := NewWithDocs(infodoc, tx, data) if err == nil { t.Errorf("expected error, got nothing", err) } else { @@ -131,9 +123,8 @@ func TestWrongInputParams(t *testing.T) { desc := "Will multiply `e` by 7 and return `a * 7`." expected := "natspec.js error evaluating expression: Natspec evaluation failed, wrong input params" - userdoc := makeUserdoc(desc) - - ns, err := NewWithDocs(abi, userdoc, tx) + infodoc := makeInfoDoc(desc) + ns, err := NewWithDocs(infodoc, tx, data) if err != nil { t.Errorf("New: error: %v", err) } diff --git a/common/resolver/resolver.go b/common/resolver/resolver.go index 42348a89c..9016547e1 100644 --- a/common/resolver/resolver.go +++ b/common/resolver/resolver.go @@ -6,7 +6,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - xe "github.com/ethereum/go-ethereum/xeth" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" ) /* @@ -18,50 +19,152 @@ The resolver is meant to be called by the roundtripper transport implementation of a url scheme */ -// contract addresses will be hardcoded after they're created -var URLHintContractAddress string = "0000000000000000000000000000000000000000000000000000000000001234" -var HashRegContractAddress string = "0000000000000000000000000000000000000000000000000000000000005678" +// // contract addresses will be hardcoded after they're created +var UrlHintContractAddress, HashRegContractAddress string -func CreateContracts(xeth *xe.XEth, addr string) { - var err error - URLHintContractAddress, err = xeth.Transact(addr, "", "", "100000000000", "1000000", "100000", ContractCodeURLhint) +const ( + txValue = "0" + txGas = "100000" + txGasPrice = "1000000000000" +) + +func abi(s string) string { + return common.ToHex(crypto.Sha3([]byte(s))[:4]) +} + +var ( + registerContentHashAbi = abi("register(uint256,uint256)") + registerUrlAbi = abi("register(uint256,uint8,uint256)") + setOwnerAbi = abi("setowner()") +) + +type Backend interface { + StorageAt(string, string) string + Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) +} + +type Resolver struct { + backend Backend +} + +func New(eth Backend) *Resolver { + return &Resolver{eth} +} + +// for testing and play temporarily +// ideally the HashReg and UrlHint contracts should be in the genesis block +// if we got build-in support for natspec/contract info +// there should be only one of these officially endorsed +// addresses as constants +// TODO: could get around this with namereg, check +func (self *Resolver) CreateContracts(addr common.Address) (err error) { + HashRegContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeHashReg) if err != nil { - panic(err) + return } - HashRegContractAddress, err = xeth.Transact(addr, "", "", "100000000000", "1000000", "100000", ContractCodeHashReg) + UrlHintContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeURLhint) + glog.V(logger.Detail).Infof("HashReg @ %v\nUrlHint @ %v\n", HashRegContractAddress, UrlHintContractAddress) + return +} + +// called as first step in the registration process on HashReg +func (self *Resolver) SetOwner(address common.Address) (txh string, err error) { + return self.backend.Transact( + address.Hex(), + HashRegContractAddress, + "", txValue, txGas, txGasPrice, + setOwnerAbi, + ) +} + +// registers some content hash to a key/code hash +// e.g., the contract Info combined Json Doc's ContentHash +// to CodeHash of a contract or hash of a domain +// kept +func (self *Resolver) RegisterContentHash(address common.Address, codehash, dochash common.Hash) (txh string, err error) { + _, err = self.SetOwner(address) if err != nil { - panic(err) + return } + codehex := common.Bytes2Hex(codehash[:]) + dochex := common.Bytes2Hex(dochash[:]) + + data := registerContentHashAbi + codehex + dochex + return self.backend.Transact( + address.Hex(), + HashRegContractAddress, + "", txValue, txGas, txGasPrice, + data, + ) } -type Resolver struct { - backend Backend - urlHintContractAddress string - hashRegContractAddress string +// registers a url to a content hash so that the content can be fetched +// address is used as sender for the transaction and will be the owner of a new +// registry entry on first time use +// FIXME: silently doing nothing if sender is not the owner +// note that with content addressed storage, this step is no longer necessary +// it could be purely +func (self *Resolver) RegisterUrl(address common.Address, hash common.Hash, url string) (txh string, err error) { + hashHex := common.Bytes2Hex(hash[:]) + var urlHex string + urlb := []byte(url) + var cnt byte + n := len(urlb) + + for n > 0 { + if n > 32 { + n = 32 + } + urlHex = common.Bytes2Hex(urlb[:n]) + urlb = urlb[n:] + n = len(urlb) + bcnt := make([]byte, 32) + bcnt[31] = cnt + data := registerUrlAbi + + hashHex + + common.Bytes2Hex(bcnt) + + common.Bytes2Hex(common.Hex2BytesFixed(urlHex, 32)) + txh, err = self.backend.Transact( + address.Hex(), + UrlHintContractAddress, + "", txValue, txGas, txGasPrice, + data, + ) + if err != nil { + return + } + cnt++ + } + return } -type Backend interface { - StorageAt(string, string) string -} +func (self *Resolver) Register(address common.Address, codehash, dochash common.Hash, url string) (txh string, err error) { -func New(eth Backend, uhca, nrca string) *Resolver { - return &Resolver{eth, uhca, nrca} + _, err = self.RegisterContentHash(address, codehash, dochash) + if err != nil { + return + } + return self.RegisterUrl(address, dochash, url) } +// resolution is costless non-transactional +// implemented as direct retrieval from db func (self *Resolver) KeyToContentHash(khash common.Hash) (chash common.Hash, err error) { // look up in hashReg + at := common.Bytes2Hex(common.FromHex(HashRegContractAddress)) key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:])) - hash := self.backend.StorageAt(self.hashRegContractAddress, key) + hash := self.backend.StorageAt(at, key) if hash == "0x0" || len(hash) < 3 { - err = fmt.Errorf("GetHashReg: content hash not found") + err = fmt.Errorf("content hash not found for '%v'", khash.Hex()) return } - copy(chash[:], common.Hex2BytesFixed(hash[2:], 32)) return } +// retrieves the url-hint for the content hash - +// if we use content addressed storage, this step is no longer necessary func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error) { // look up in URL reg var str string = " " @@ -69,7 +172,7 @@ func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error for len(str) > 0 { mapaddr := storageMapping(storageIdx2Addr(1), chash[:]) key := storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(idx))) - hex := self.backend.StorageAt(self.urlHintContractAddress, key) + hex := self.backend.StorageAt(UrlHintContractAddress, key) str = string(common.Hex2Bytes(hex[2:])) l := len(str) for (l > 0) && (str[l-1] == 0) { @@ -81,7 +184,7 @@ func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error } if len(uri) == 0 { - err = fmt.Errorf("GetURLhint: URL hint not found") + err = fmt.Errorf("GetURLhint: URL hint not found for '%v'", chash.Hex()) } return } @@ -106,7 +209,8 @@ func storageMapping(addr, key []byte) []byte { data := make([]byte, 64) copy(data[0:32], key[0:32]) copy(data[32:64], addr[0:32]) - return crypto.Sha3(data) + sha := crypto.Sha3(data) + return sha } func storageFixedArray(addr, idx []byte) []byte { diff --git a/common/resolver/resolver_test.go b/common/resolver/resolver_test.go index f5eb51437..02d12592e 100644 --- a/common/resolver/resolver_test.go +++ b/common/resolver/resolver_test.go @@ -20,6 +20,8 @@ var ( ) func NewTestBackend() *testBackend { + HashRegContractAddress = common.BigToAddress(common.Big0).Hex()[2:] + UrlHintContractAddress = common.BigToAddress(common.Big1).Hex()[2:] self := &testBackend{} self.contracts = make(map[string](map[string]string)) @@ -27,14 +29,13 @@ func NewTestBackend() *testBackend { key := storageAddress(storageMapping(storageIdx2Addr(1), codehash[:])) self.contracts[HashRegContractAddress][key] = hash.Hex() - self.contracts[URLHintContractAddress] = make(map[string]string) + self.contracts[UrlHintContractAddress] = make(map[string]string) mapaddr := storageMapping(storageIdx2Addr(1), hash[:]) key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(0))) - self.contracts[URLHintContractAddress][key] = common.ToHex([]byte(url)) + self.contracts[UrlHintContractAddress][key] = common.ToHex([]byte(url)) key = storageAddress(storageFixedArray(mapaddr, storageIdx2Addr(1))) - self.contracts[URLHintContractAddress][key] = "0x00" - + self.contracts[UrlHintContractAddress][key] = "0x00" return self } @@ -47,42 +48,46 @@ func (self *testBackend) StorageAt(ca, sa string) (res string) { return } +func (self *testBackend) Transact(fromStr, toStr, nonceStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) { + return "", nil +} + func TestKeyToContentHash(t *testing.T) { b := NewTestBackend() - res := New(b, URLHintContractAddress, HashRegContractAddress) + res := New(b) got, err := res.KeyToContentHash(codehash) if err != nil { t.Errorf("expected no error, got %v", err) } else { if got != hash { - t.Errorf("incorrect result, expected %x, got %x: ", hash.Hex(), got.Hex()) + t.Errorf("incorrect result, expected '%v', got '%v'", hash.Hex(), got.Hex()) } } } func TestContentHashToUrl(t *testing.T) { b := NewTestBackend() - res := New(b, URLHintContractAddress, HashRegContractAddress) + res := New(b) got, err := res.ContentHashToUrl(hash) if err != nil { t.Errorf("expected no error, got %v", err) } else { - if string(got) != url { - t.Errorf("incorrect result, expected %v, got %s: ", url, string(got)) + if got != url { + t.Errorf("incorrect result, expected '%v', got '%s'", url, got) } } } func TestKeyToUrl(t *testing.T) { b := NewTestBackend() - res := New(b, URLHintContractAddress, HashRegContractAddress) + res := New(b) got, _, err := res.KeyToUrl(codehash) if err != nil { t.Errorf("expected no error, got %v", err) } else { - if string(got) != url { - t.Errorf("incorrect result, expected %v, got %s: ", url, string(got)) + if got != url { + t.Errorf("incorrect result, expected \n'%s', got \n'%s'", url, got) } } } |