solidity compiler and contract metadocs integration

* common/compiler: solidity compiler + tests
* rpc: eth_compilers, eth_compileSolidity + tests
* fix natspec test using keystore API, notice exp dynamically changes addr, cleanup
* resolver implements registrars and needs to create reg contract (temp)
* xeth: solidity compiler. expose getter Solc() and paths setter SetSolc(solcPath)
* ethereumApi: implement compiler related RPC calls using XEth - json struct tests
* admin: make use of XEth.SetSolc to allow runtime setting of compiler paths
* cli: command line flags solc to set custom solc bin path
* js admin api with new features debug and contractInfo modules
* wiki is the doc https://github.com/ethereum/go-ethereum/wiki/Contracts-and-Transactions
This commit is contained in:
zelig
2015-04-22 23:11:11 +01:00
parent 97c37356fd
commit 009b221692
21 changed files with 1463 additions and 517 deletions

187
common/compiler/solidity.go Normal file
View File

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

View File

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

View File

@ -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 New(xeth *xeth.XEth, tx string, http *docserver.DocServer) (self *NatSpec, err 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"`
}
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"`
}
func New(xeth *xeth.XEth, jsontx string, http *docserver.DocServer) (self *NatSpec, err error) {
// extract contract address from tx
var obj map[string]json.RawMessage
err = json.Unmarshal([]byte(tx), &obj)
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")
codehex := xeth.CodeAt(contractAddress)
codeb := xeth.CodeAtBytes(contractAddress)
if codehex == "0x" {
err = fmt.Errorf("contract (%v) not found", contractAddress)
return
}
codehex := xeth.CodeAt(contractAddress)
codeHash := common.BytesToHash(crypto.Sha3(common.Hex2Bytes(codehex[2:])))
// parse out host/domain
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)
content, err = http.GetAuthContent(uri, hash)
if err != nil {
return
}
// get abi, userdoc
var obj2 map[string]json.RawMessage
err = json.Unmarshal(content, &obj2)
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
}

View File

@ -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"
testFileName = "long_file_name_for_testing_registration_of_URLs_longer_than_32_bytes.content"
testNotice = "Register key `utils.toHex(_key)` <- content `utils.toHex(_content)`"
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 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"}]}`
const testUserDoc = `
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 + `
}
`
)
func (f *testFrontend) UnlockAccount(acc []byte) bool {
f.t.Logf("Unlocking account %v\n", common.Bytes2Hex(acc))
f.ethereum.AccountManager().Unlock(acc, "password")
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 (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)
if err != nil {
t.Errorf("%v", err)
return
}
err = os.MkdirAll("/tmp/eth-natspec/data", os.ModePerm)
if err != nil {
t.Errorf("%v", err)
return
}
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)
port++
os.RemoveAll("/tmp/eth-natspec/")
err = os.MkdirAll("/tmp/eth-natspec/keys", os.ModePerm)
if err != nil {
panic(err)
}
// create a testAddress
ks := crypto.NewKeyStorePassphrase("/tmp/eth-natspec/keys")
am := accounts.NewManager(ks)
testAccount, err := am.NewAccount("password")
if err != nil {
panic(err)
}
testAddress := common.Bytes2Hex(testAccount.Address)
// set up mock genesis with balance on the testAddress
core.GenesisData = []byte(`{
"` + testAddress + `": {"balance": "` + testBalance + `"}
}`)
// only use minimalistic stack with no networking
ethereum, err = eth.New(&eth.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()
// 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)
return
}
func (self *testFrontend) insertTx(addr, contract, fnsig string, args []string) {
//cb := common.HexToAddress(self.coinbase)
//coinbase := self.ethereum.ChainManager().State().GetStateObject(cb)
hash := common.Bytes2Hex(crypto.Sha3([]byte(fnsig)))
data := "0x" + hash[0:8]
for _, arg := range args {
data = data + common.Bytes2Hex(common.Hex2BytesFixed(arg, 32))
}
self.t.Logf("Tx data: %v", data)
jsontx := `
[{
"from": "` + addr + `",
"to": "` + 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)
}
// 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()
// 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)))
ioutil.WriteFile("/tmp/"+testFileName, []byte(testDocs), os.ModePerm)
dochash := common.BytesToHash(crypto.Sha3([]byte(testDocs)))
// 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))
codehex := tf.xeth.CodeAt(resolver.HashRegContractAddress)
codehash := common.BytesToHash(crypto.Sha3(common.Hex2Bytes(codehex[2:])))
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.Errorf("error registering: %v", err)
}
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.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)
}
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)
}
// 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)
}
if tf.lastConfirm != exp {
t.Errorf("Wrong confirm message, expected '%v', got '%v'", exp, tf.lastConfirm)
}
}

View File

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

View File

@ -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)
if err != nil {
panic(err)
}
HashRegContractAddress, err = xeth.Transact(addr, "", "", "100000000000", "1000000", "100000", ContractCodeHashReg)
if err != nil {
panic(err)
}
const (
txValue = "0"
txGas = "100000"
txGasPrice = "1000000000000"
)
func abi(s string) string {
return common.ToHex(crypto.Sha3([]byte(s))[:4])
}
type Resolver struct {
backend Backend
urlHintContractAddress string
hashRegContractAddress string
}
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)
}
func New(eth Backend, uhca, nrca string) *Resolver {
return &Resolver{eth, uhca, nrca}
type Resolver struct {
backend Backend
}
func (self *Resolver) KeyToContentHash(khash common.Hash) (chash common.Hash, err error) {
// look up in hashReg
key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:]))
hash := self.backend.StorageAt(self.hashRegContractAddress, key)
func New(eth Backend) *Resolver {
return &Resolver{eth}
}
if hash == "0x0" || len(hash) < 3 {
err = fmt.Errorf("GetHashReg: content hash not found")
// for testing and play temporarily
// ideally the HashReg and UrlHint contracts should be in the genesis block
// if we got build-in support for natspec/contract info
// there should be only one of these officially endorsed
// addresses as constants
// TODO: could get around this with namereg, check
func (self *Resolver) CreateContracts(addr common.Address) (err error) {
HashRegContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeHashReg)
if err != nil {
return
}
UrlHintContractAddress, err = self.backend.Transact(addr.Hex(), "", "", txValue, txGas, txGasPrice, ContractCodeURLhint)
glog.V(logger.Detail).Infof("HashReg @ %v\nUrlHint @ %v\n", HashRegContractAddress, UrlHintContractAddress)
return
}
// called as first step in the registration process on HashReg
func (self *Resolver) SetOwner(address common.Address) (txh string, err error) {
return self.backend.Transact(
address.Hex(),
HashRegContractAddress,
"", txValue, txGas, txGasPrice,
setOwnerAbi,
)
}
// registers some content hash to a key/code hash
// e.g., the contract Info combined Json Doc's ContentHash
// to CodeHash of a contract or hash of a domain
// kept
func (self *Resolver) RegisterContentHash(address common.Address, codehash, dochash common.Hash) (txh string, err error) {
_, err = self.SetOwner(address)
if err != nil {
return
}
codehex := common.Bytes2Hex(codehash[:])
dochex := common.Bytes2Hex(dochash[:])
data := registerContentHashAbi + codehex + dochex
return self.backend.Transact(
address.Hex(),
HashRegContractAddress,
"", txValue, txGas, txGasPrice,
data,
)
}
// registers a url to a content hash so that the content can be fetched
// address is used as sender for the transaction and will be the owner of a new
// registry entry on first time use
// FIXME: silently doing nothing if sender is not the owner
// note that with content addressed storage, this step is no longer necessary
// it could be purely
func (self *Resolver) RegisterUrl(address common.Address, hash common.Hash, url string) (txh string, err error) {
hashHex := common.Bytes2Hex(hash[:])
var urlHex string
urlb := []byte(url)
var cnt byte
n := len(urlb)
for n > 0 {
if n > 32 {
n = 32
}
urlHex = common.Bytes2Hex(urlb[:n])
urlb = urlb[n:]
n = len(urlb)
bcnt := make([]byte, 32)
bcnt[31] = cnt
data := registerUrlAbi +
hashHex +
common.Bytes2Hex(bcnt) +
common.Bytes2Hex(common.Hex2BytesFixed(urlHex, 32))
txh, err = self.backend.Transact(
address.Hex(),
UrlHintContractAddress,
"", txValue, txGas, txGasPrice,
data,
)
if err != nil {
return
}
cnt++
}
return
}
func (self *Resolver) Register(address common.Address, codehash, dochash common.Hash, url string) (txh string, err error) {
_, err = self.RegisterContentHash(address, codehash, dochash)
if err != nil {
return
}
return self.RegisterUrl(address, dochash, url)
}
// resolution is costless non-transactional
// implemented as direct retrieval from db
func (self *Resolver) KeyToContentHash(khash common.Hash) (chash common.Hash, err error) {
// look up in hashReg
at := common.Bytes2Hex(common.FromHex(HashRegContractAddress))
key := storageAddress(storageMapping(storageIdx2Addr(1), khash[:]))
hash := self.backend.StorageAt(at, key)
if hash == "0x0" || len(hash) < 3 {
err = fmt.Errorf("content hash not found for '%v'", khash.Hex())
return
}
copy(chash[:], common.Hex2BytesFixed(hash[2:], 32))
return
}
// retrieves the url-hint for the content hash -
// if we use content addressed storage, this step is no longer necessary
func (self *Resolver) ContentHashToUrl(chash common.Hash) (uri string, err error) {
// look up in URL reg
var str string = " "
@ -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 {

View File

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