diff --git a/config/tendermint_test/config.go b/config/tendermint_test/config.go index 418f026dc..cd9a17b18 100644 --- a/config/tendermint_test/config.go +++ b/config/tendermint_test/config.go @@ -104,25 +104,38 @@ func defaultConfig(moniker string) (defaultConfig string) { return } +// priv keys generated deterministically eg rpc/tests/helpers.go var defaultGenesis = `{ "chain_id" : "tendermint_test", "accounts": [ { - "address": "1D7A91CB32F758A02EBB9BE1FB6F8DEE56F90D42", - "amount": 200000000 + "address": "E9B5D87313356465FAE33C406CE2C2979DE60BCB", + "amount": 200000000 }, { - "address": "AC89A6DDF4C309A89A2C4078CE409A5A7B282270", - "amount": 200000000 + "address": "DFE4AFFA4CEE17CD01CB9E061D77C3ECED29BD88", + "amount": 200000000 + }, + { + "address": "F60D30722E7B497FA532FB3207C3FB29C31B1992", + "amount": 200000000 + }, + { + "address": "336CB40A5EB92E496E19B74FDFF2BA017C877FD6", + "amount": 200000000 + }, + { + "address": "D218F0F439BF0384F6F5EF8D0F8B398D941BD1DC", + "amount": 200000000 } ], "validators": [ { - "pub_key": [1, "06FBAC4E285285D1D91FCBC7E91C780ADA11516F67462340B3980CE2B94940E8"], + "pub_key": [1, "583779C3BFA3F6C7E23C7D830A9C3D023A216B55079AD38BFED1207B94A19548"], "amount": 1000000, "unbond_to": [ { - "address": "1D7A91CB32F758A02EBB9BE1FB6F8DEE56F90D42", + "address": "E9B5D87313356465FAE33C406CE2C2979DE60BCB", "amount": 100000 } ] diff --git a/consensus/types/proposal.go b/consensus/types/proposal.go index a306f032a..d2fb4b15f 100644 --- a/consensus/types/proposal.go +++ b/consensus/types/proposal.go @@ -39,8 +39,7 @@ func (p *Proposal) String() string { } func (p *Proposal) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id name so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":"%s"`, chainID)), w, n, err) binary.WriteTo([]byte(`,"proposal":{"block_parts":`), w, n, err) p.BlockParts.WriteSignBytes(w, n, err) binary.WriteTo([]byte(Fmt(`,"height":%v,"pol_parts":`, p.Height)), w, n, err) diff --git a/consensus/types/proposal_test.go b/consensus/types/proposal_test.go index fa8f37be3..5cc4a5f38 100644 --- a/consensus/types/proposal_test.go +++ b/consensus/types/proposal_test.go @@ -19,7 +19,7 @@ func TestProposalSignable(t *testing.T) { } signBytes := account.SignBytes(config.GetString("chain_id"), proposal) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","proposal":{"block_parts":{"hash":"626C6F636B7061727473","total":111},"height":12345,"pol_parts":{"hash":"706F6C7061727473","total":222},"round":23456}}`, + expected := Fmt(`{"chain_id":"%s","proposal":{"block_parts":{"hash":"626C6F636B7061727473","total":111},"height":12345,"pol_parts":{"hash":"706F6C7061727473","total":222},"round":23456}}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for SendTx. Expected:\n%v\nGot:\n%v", expected, signStr) diff --git a/rpc/core/accounts.go b/rpc/core/accounts.go index ba43673a0..e102e1b72 100644 --- a/rpc/core/accounts.go +++ b/rpc/core/accounts.go @@ -7,11 +7,11 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" ) -func GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) { - return &ctypes.ResponseGenPrivAccount{acm.GenPrivAccount()}, nil +func GenPrivAccount() (*acm.PrivAccount, error) { + return acm.GenPrivAccount(), nil } -func GetAccount(address []byte) (*ctypes.ResponseGetAccount, error) { +func GetAccount(address []byte) (*acm.Account, error) { cache := mempoolReactor.Mempool.GetCache() account := cache.GetAccount(address) if account == nil { @@ -24,7 +24,7 @@ func GetAccount(address []byte) (*ctypes.ResponseGetAccount, error) { StorageRoot: nil, } } - return &ctypes.ResponseGetAccount{account}, nil + return account, nil } func GetStorage(address, key []byte) (*ctypes.ResponseGetStorage, error) { diff --git a/rpc/core/mempool.go b/rpc/core/mempool.go index e4ae77096..c9e83b3df 100644 --- a/rpc/core/mempool.go +++ b/rpc/core/mempool.go @@ -9,9 +9,8 @@ import ( //----------------------------------------------------------------------------- -// pass pointer? // Note: tx must be signed -func BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, error) { +func BroadcastTx(tx types.Tx) (*ctypes.Receipt, error) { err := mempoolReactor.BroadcastTx(tx) if err != nil { return nil, fmt.Errorf("Error broadcasting transaction: %v", err) @@ -27,10 +26,9 @@ func BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, error) { contractAddr = state.NewContractAddress(callTx.Input.Address, uint64(callTx.Input.Sequence)) } } - return &ctypes.ResponseBroadcastTx{ctypes.Receipt{txHash, createsContract, contractAddr}}, nil + return &ctypes.Receipt{txHash, createsContract, contractAddr}, nil } -func ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, error) { - txs := mempoolReactor.Mempool.GetProposalTxs() - return &ctypes.ResponseListUnconfirmedTxs{txs}, nil +func ListUnconfirmedTxs() ([]types.Tx, error) { + return mempoolReactor.Mempool.GetProposalTxs(), nil } diff --git a/rpc/core/names.go b/rpc/core/names.go new file mode 100644 index 000000000..7f0d872ec --- /dev/null +++ b/rpc/core/names.go @@ -0,0 +1,29 @@ +package core + +import ( + "fmt" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/types" +) + +func GetName(name string) (*types.NameRegEntry, error) { + st := consensusState.GetState() // performs a copy + entry := st.GetNameRegEntry(name) + if entry == nil { + return nil, fmt.Errorf("Name %s not found", name) + } + return entry, nil +} + +func ListNames() (*ctypes.ResponseListNames, error) { + var blockHeight uint + var names []*types.NameRegEntry + state := consensusState.GetState() + blockHeight = state.LastBlockHeight + state.GetNames().Iterate(func(key interface{}, value interface{}) bool { + names = append(names, value.(*types.NameRegEntry)) + return false + }) + return &ctypes.ResponseListNames{blockHeight, names}, nil +} diff --git a/rpc/core/routes.go b/rpc/core/routes.go index f65d58e23..6e8fe2499 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -20,6 +20,8 @@ var Routes = map[string]*rpc.RPCFunc{ "broadcast_tx": rpc.NewRPCFunc(BroadcastTx, []string{"tx"}), "list_unconfirmed_txs": rpc.NewRPCFunc(ListUnconfirmedTxs, []string{}), "list_accounts": rpc.NewRPCFunc(ListAccounts, []string{}), + "get_name": rpc.NewRPCFunc(GetName, []string{"name"}), + "list_names": rpc.NewRPCFunc(ListNames, []string{}), "unsafe/gen_priv_account": rpc.NewRPCFunc(GenPrivAccount, []string{}), "unsafe/sign_tx": rpc.NewRPCFunc(SignTx, []string{"tx", "privAccounts"}), } diff --git a/rpc/core/txs.go b/rpc/core/txs.go index 07e14d94d..be269f470 100644 --- a/rpc/core/txs.go +++ b/rpc/core/txs.go @@ -78,7 +78,7 @@ func CallCode(code, data []byte) (*ctypes.ResponseCall, error) { //----------------------------------------------------------------------------- -func SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (*ctypes.ResponseSignTx, error) { +func SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (types.Tx, error) { // more checks? for i, privAccount := range privAccounts { @@ -113,5 +113,5 @@ func SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (*ctypes.ResponseS rebondTx := tx.(*types.RebondTx) rebondTx.Signature = privAccounts[0].Sign(config.GetString("chain_id"), rebondTx).(account.SignatureEd25519) } - return &ctypes.ResponseSignTx{tx}, nil + return tx, nil } diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 9239695f2..221b95628 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -6,14 +6,6 @@ import ( "github.com/tendermint/tendermint/types" ) -type ResponseGenPrivAccount struct { - PrivAccount *account.PrivAccount `json:"priv_account"` -} - -type ResponseGetAccount struct { - Account *account.Account `json:"account"` -} - type ResponseGetStorage struct { Key []byte `json:"key"` Value []byte `json:"value"` @@ -50,14 +42,6 @@ type ResponseGetBlock struct { Block *types.Block `json:"block"` } -type ResponseBroadcastTx struct { - Receipt Receipt `json:"receipt"` -} - -type ResponseListUnconfirmedTxs struct { - Txs []types.Tx `json:"txs"` -} - type Receipt struct { TxHash []byte `json:"tx_hash"` CreatesContract uint8 `json:"creates_contract"` @@ -86,10 +70,6 @@ type Peer struct { IsOutbound bool `json:"is_outbound"` } -type ResponseSignTx struct { - Tx types.Tx `json:"tx"` -} - type ResponseListValidators struct { BlockHeight uint `json:"block_height"` BondedValidators []*sm.Validator `json:"bonded_validators"` @@ -100,3 +80,8 @@ type ResponseDumpConsensusState struct { RoundState string `json:"round_state"` PeerRoundStates []string `json:"peer_round_states"` } + +type ResponseListNames struct { + BlockHeight uint `json:"block_height"` + Names []*types.NameRegEntry `json:"names"` +} diff --git a/rpc/core_client/client.go b/rpc/core_client/client.go index 3254aad27..2af0ef0b5 100644 --- a/rpc/core_client/client.go +++ b/rpc/core_client/client.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/tendermint/tendermint/binary" rpctypes "github.com/tendermint/tendermint/rpc/types" - // NOTE: do not import rpc/core. - // What kind of client imports all of core logic? :P "io/ioutil" "net/http" "net/url" @@ -18,20 +16,25 @@ import ( // maps camel-case function names to lower case rpc version var reverseFuncMap = map[string]string{ - "Status": "status", - "NetInfo": "net_info", - "BlockchainInfo": "blockchain", - "GetBlock": "get_block", - "GetAccount": "get_account", - "GetStorage": "get_storage", - "Call": "call", - "CallCode": "call_code", - "ListValidators": "list_validators", - "DumpStorage": "dump_storage", - "BroadcastTx": "broadcast_tx", - "ListAccounts": "list_accounts", - "GenPrivAccount": "unsafe/gen_priv_account", - "SignTx": "unsafe/sign_tx", + "Status": "status", + "NetInfo": "net_info", + "BlockchainInfo": "blockchain", + "Genesis": "genesis", + "GetBlock": "get_block", + "GetAccount": "get_account", + "GetStorage": "get_storage", + "Call": "call", + "CallCode": "call_code", + "ListValidators": "list_validators", + "DumpConsensusState": "dump_consensus_state", + "DumpStorage": "dump_storage", + "BroadcastTx": "broadcast_tx", + "ListUnconfirmedTxs": "list_unconfirmed_txs", + "ListAccounts": "list_accounts", + "GetName": "get_name", + "ListNames": "list_names", + "GenPrivAccount": "unsafe/gen_priv_account", + "SignTx": "unsafe/sign_tx", } /* diff --git a/rpc/core_client/client_methods.go b/rpc/core_client/client_methods.go index aa48dcffd..9ac669842 100644 --- a/rpc/core_client/client_methods.go +++ b/rpc/core_client/client_methods.go @@ -5,6 +5,7 @@ package core_client import ( "fmt" "github.com/tendermint/tendermint/account" + acm "github.com/tendermint/tendermint/account" "github.com/tendermint/tendermint/binary" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctypes "github.com/tendermint/tendermint/rpc/types" @@ -15,21 +16,23 @@ import ( type Client interface { BlockchainInfo(minHeight uint, maxHeight uint) (*ctypes.ResponseBlockchainInfo, error) - BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, error) + BroadcastTx(tx types.Tx) (*ctypes.Receipt, error) Call(address []byte, data []byte) (*ctypes.ResponseCall, error) CallCode(code []byte, data []byte) (*ctypes.ResponseCall, error) DumpConsensusState() (*ctypes.ResponseDumpConsensusState, error) DumpStorage(address []byte) (*ctypes.ResponseDumpStorage, error) - GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) + GenPrivAccount() (*acm.PrivAccount, error) Genesis() (*string, error) - GetAccount(address []byte) (*ctypes.ResponseGetAccount, error) + GetAccount(address []byte) (*acm.Account, error) GetBlock(height uint) (*ctypes.ResponseGetBlock, error) + GetName(name string) (*types.NameRegEntry, error) GetStorage(address []byte, key []byte) (*ctypes.ResponseGetStorage, error) ListAccounts() (*ctypes.ResponseListAccounts, error) - ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, error) + ListNames() (*ctypes.ResponseListNames, error) + ListUnconfirmedTxs() ([]types.Tx, error) ListValidators() (*ctypes.ResponseListValidators, error) NetInfo() (*ctypes.ResponseNetInfo, error) - SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (*ctypes.ResponseSignTx, error) + SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (types.Tx, error) Status() (*ctypes.ResponseStatus, error) } @@ -63,7 +66,7 @@ func (c *ClientHTTP) BlockchainInfo(minHeight uint, maxHeight uint) (*ctypes.Res return response.Result, nil } -func (c *ClientHTTP) BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, error) { +func (c *ClientHTTP) BroadcastTx(tx types.Tx) (*ctypes.Receipt, error) { values, err := argsToURLValues([]string{"tx"}, tx) if err != nil { return nil, err @@ -78,10 +81,10 @@ func (c *ClientHTTP) BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, erro return nil, err } var response struct { - Result *ctypes.ResponseBroadcastTx `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *ctypes.Receipt `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -213,7 +216,7 @@ func (c *ClientHTTP) DumpStorage(address []byte) (*ctypes.ResponseDumpStorage, e return response.Result, nil } -func (c *ClientHTTP) GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) { +func (c *ClientHTTP) GenPrivAccount() (*acm.PrivAccount, error) { values, err := argsToURLValues(nil) if err != nil { return nil, err @@ -228,10 +231,10 @@ func (c *ClientHTTP) GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) { return nil, err } var response struct { - Result *ctypes.ResponseGenPrivAccount `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *acm.PrivAccount `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -273,7 +276,7 @@ func (c *ClientHTTP) Genesis() (*string, error) { return response.Result, nil } -func (c *ClientHTTP) GetAccount(address []byte) (*ctypes.ResponseGetAccount, error) { +func (c *ClientHTTP) GetAccount(address []byte) (*acm.Account, error) { values, err := argsToURLValues([]string{"address"}, address) if err != nil { return nil, err @@ -288,10 +291,10 @@ func (c *ClientHTTP) GetAccount(address []byte) (*ctypes.ResponseGetAccount, err return nil, err } var response struct { - Result *ctypes.ResponseGetAccount `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *acm.Account `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -333,6 +336,36 @@ func (c *ClientHTTP) GetBlock(height uint) (*ctypes.ResponseGetBlock, error) { return response.Result, nil } +func (c *ClientHTTP) GetName(name string) (*types.NameRegEntry, error) { + values, err := argsToURLValues([]string{"name"}, name) + if err != nil { + return nil, err + } + resp, err := http.PostForm(c.addr+reverseFuncMap["GetName"], values) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var response struct { + Result *types.NameRegEntry `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` + } + binary.ReadJSON(&response, body, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + return response.Result, nil +} + func (c *ClientHTTP) GetStorage(address []byte, key []byte) (*ctypes.ResponseGetStorage, error) { values, err := argsToURLValues([]string{"address", "key"}, address, key) if err != nil { @@ -393,7 +426,37 @@ func (c *ClientHTTP) ListAccounts() (*ctypes.ResponseListAccounts, error) { return response.Result, nil } -func (c *ClientHTTP) ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, error) { +func (c *ClientHTTP) ListNames() (*ctypes.ResponseListNames, error) { + values, err := argsToURLValues(nil) + if err != nil { + return nil, err + } + resp, err := http.PostForm(c.addr+reverseFuncMap["ListNames"], values) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var response struct { + Result *ctypes.ResponseListNames `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` + } + binary.ReadJSON(&response, body, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + return response.Result, nil +} + +func (c *ClientHTTP) ListUnconfirmedTxs() ([]types.Tx, error) { values, err := argsToURLValues(nil) if err != nil { return nil, err @@ -408,10 +471,10 @@ func (c *ClientHTTP) ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, e return nil, err } var response struct { - Result *ctypes.ResponseListUnconfirmedTxs `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result []types.Tx `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -483,7 +546,7 @@ func (c *ClientHTTP) NetInfo() (*ctypes.ResponseNetInfo, error) { return response.Result, nil } -func (c *ClientHTTP) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (*ctypes.ResponseSignTx, error) { +func (c *ClientHTTP) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (types.Tx, error) { values, err := argsToURLValues([]string{"tx", "privAccounts"}, tx, privAccounts) if err != nil { return nil, err @@ -498,10 +561,10 @@ func (c *ClientHTTP) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (* return nil, err } var response struct { - Result *ctypes.ResponseSignTx `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result types.Tx `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -570,7 +633,7 @@ func (c *ClientJSON) BlockchainInfo(minHeight uint, maxHeight uint) (*ctypes.Res return response.Result, nil } -func (c *ClientJSON) BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, error) { +func (c *ClientJSON) BroadcastTx(tx types.Tx) (*ctypes.Receipt, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", Method: reverseFuncMap["BroadcastTx"], @@ -582,10 +645,10 @@ func (c *ClientJSON) BroadcastTx(tx types.Tx) (*ctypes.ResponseBroadcastTx, erro return nil, err } var response struct { - Result *ctypes.ResponseBroadcastTx `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *ctypes.Receipt `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -705,7 +768,7 @@ func (c *ClientJSON) DumpStorage(address []byte) (*ctypes.ResponseDumpStorage, e return response.Result, nil } -func (c *ClientJSON) GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) { +func (c *ClientJSON) GenPrivAccount() (*acm.PrivAccount, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", Method: reverseFuncMap["GenPrivAccount"], @@ -717,10 +780,10 @@ func (c *ClientJSON) GenPrivAccount() (*ctypes.ResponseGenPrivAccount, error) { return nil, err } var response struct { - Result *ctypes.ResponseGenPrivAccount `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *acm.PrivAccount `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -759,7 +822,7 @@ func (c *ClientJSON) Genesis() (*string, error) { return response.Result, nil } -func (c *ClientJSON) GetAccount(address []byte) (*ctypes.ResponseGetAccount, error) { +func (c *ClientJSON) GetAccount(address []byte) (*acm.Account, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", Method: reverseFuncMap["GetAccount"], @@ -771,10 +834,10 @@ func (c *ClientJSON) GetAccount(address []byte) (*ctypes.ResponseGetAccount, err return nil, err } var response struct { - Result *ctypes.ResponseGetAccount `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result *acm.Account `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -813,6 +876,33 @@ func (c *ClientJSON) GetBlock(height uint) (*ctypes.ResponseGetBlock, error) { return response.Result, nil } +func (c *ClientJSON) GetName(name string) (*types.NameRegEntry, error) { + request := rpctypes.RPCRequest{ + JSONRPC: "2.0", + Method: reverseFuncMap["GetName"], + Params: []interface{}{name}, + Id: 0, + } + body, err := c.RequestResponse(request) + if err != nil { + return nil, err + } + var response struct { + Result *types.NameRegEntry `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` + } + binary.ReadJSON(&response, body, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + return response.Result, nil +} + func (c *ClientJSON) GetStorage(address []byte, key []byte) (*ctypes.ResponseGetStorage, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", @@ -867,7 +957,34 @@ func (c *ClientJSON) ListAccounts() (*ctypes.ResponseListAccounts, error) { return response.Result, nil } -func (c *ClientJSON) ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, error) { +func (c *ClientJSON) ListNames() (*ctypes.ResponseListNames, error) { + request := rpctypes.RPCRequest{ + JSONRPC: "2.0", + Method: reverseFuncMap["ListNames"], + Params: []interface{}{}, + Id: 0, + } + body, err := c.RequestResponse(request) + if err != nil { + return nil, err + } + var response struct { + Result *ctypes.ResponseListNames `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` + } + binary.ReadJSON(&response, body, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + return response.Result, nil +} + +func (c *ClientJSON) ListUnconfirmedTxs() ([]types.Tx, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", Method: reverseFuncMap["ListUnconfirmedTxs"], @@ -879,10 +996,10 @@ func (c *ClientJSON) ListUnconfirmedTxs() (*ctypes.ResponseListUnconfirmedTxs, e return nil, err } var response struct { - Result *ctypes.ResponseListUnconfirmedTxs `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result []types.Tx `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { @@ -948,7 +1065,7 @@ func (c *ClientJSON) NetInfo() (*ctypes.ResponseNetInfo, error) { return response.Result, nil } -func (c *ClientJSON) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (*ctypes.ResponseSignTx, error) { +func (c *ClientJSON) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (types.Tx, error) { request := rpctypes.RPCRequest{ JSONRPC: "2.0", Method: reverseFuncMap["SignTx"], @@ -960,10 +1077,10 @@ func (c *ClientJSON) SignTx(tx types.Tx, privAccounts []*account.PrivAccount) (* return nil, err } var response struct { - Result *ctypes.ResponseSignTx `json:"result"` - Error string `json:"error"` - Id string `json:"id"` - JSONRPC string `json:"jsonrpc"` + Result types.Tx `json:"result"` + Error string `json:"error"` + Id string `json:"id"` + JSONRPC string `json:"jsonrpc"` } binary.ReadJSON(&response, body, &err) if err != nil { diff --git a/rpc/test/client_rpc_test.go b/rpc/test/client_rpc_test.go index 0ced2e664..2d9af88ae 100644 --- a/rpc/test/client_rpc_test.go +++ b/rpc/test/client_rpc_test.go @@ -40,6 +40,10 @@ func TestHTTPCallContract(t *testing.T) { testCall(t, "HTTP") } +func TestHTTPNameReg(t *testing.T) { + testNameReg(t, "HTTP") +} + //-------------------------------------------------------------------------------- // Test the JSONRPC client @@ -74,3 +78,7 @@ func TestJSONCallCode(t *testing.T) { func TestJSONCallContract(t *testing.T) { testCall(t, "JSONRPC") } + +func TestJSONNameReg(t *testing.T) { + testNameReg(t, "JSONRPC") +} diff --git a/rpc/test/client_ws_test.go b/rpc/test/client_ws_test.go index 5f94306b6..a310871b4 100644 --- a/rpc/test/client_ws_test.go +++ b/rpc/test/client_ws_test.go @@ -50,7 +50,7 @@ func TestWSBlockchainGrowth(t *testing.T) { // send a transaction and validate the events from listening for both sender and receiver func TestWSSend(t *testing.T) { - toAddr := []byte{20, 143, 25, 63, 16, 177, 83, 29, 91, 91, 54, 23, 233, 46, 190, 121, 122, 34, 86, 54} + toAddr := user[1].Address amt := uint64(100) con := newWSCon(t) @@ -80,7 +80,7 @@ func TestWSDoubleFire(t *testing.T) { con.Close() }() amt := uint64(100) - toAddr := []byte{20, 143, 25, 63, 16, 177, 83, 29, 91, 91, 54, 23, 233, 46, 190, 121, 122, 34, 86, 54} + toAddr := user[1].Address // broadcast the transaction, wait to hear about it waitForEvent(t, con, eid, true, func() { tx := makeDefaultSendTxSigned(t, wsTyp, toAddr, amt) diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index da05ccfdc..56e338820 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -2,7 +2,6 @@ package rpctest import ( "bytes" - "encoding/hex" "strconv" "testing" "time" @@ -29,8 +28,7 @@ var ( mempoolCount = 0 // make keys - userPriv = "C453604BD6480D5538B4C6FD2E3E314B5BCE518D75ADE4DA3DA85AB8ADFD819606FBAC4E285285D1D91FCBC7E91C780ADA11516F67462340B3980CE2B94940E8" - user = makeUsers(2) + user = makeUsers(5) chainID string @@ -40,6 +38,7 @@ var ( } ) +// deterministic account generation, synced with genesis file in config/tendermint_test/config.go func makeUsers(n int) []*account.PrivAccount { accounts := []*account.PrivAccount{} for i := 0; i < n; i++ { @@ -47,13 +46,6 @@ func makeUsers(n int) []*account.PrivAccount { user := account.GenPrivAccountFromSecret(secret) accounts = append(accounts, user) } - - // include our validator - var byteKey [64]byte - userPrivByteSlice, _ := hex.DecodeString(userPriv) - copy(byteKey[:], userPrivByteSlice) - privAcc := account.GenPrivAccountFromKey(byteKey) - accounts[0] = privAcc return accounts } @@ -102,7 +94,7 @@ func init() { func makeDefaultSendTx(t *testing.T, typ string, addr []byte, amt uint64) *types.SendTx { nonce := getNonce(t, typ, user[0].Address) tx := types.NewSendTx() - tx.AddInputWithNonce(user[0].PubKey, amt, nonce) + tx.AddInputWithNonce(user[0].PubKey, amt, nonce+1) tx.AddOutput(addr, amt) return tx } @@ -115,7 +107,14 @@ func makeDefaultSendTxSigned(t *testing.T, typ string, addr []byte, amt uint64) func makeDefaultCallTx(t *testing.T, typ string, addr, code []byte, amt, gasLim, fee uint64) *types.CallTx { nonce := getNonce(t, typ, user[0].Address) - tx := types.NewCallTxWithNonce(user[0].PubKey, addr, code, amt, gasLim, fee, nonce) + tx := types.NewCallTxWithNonce(user[0].PubKey, addr, code, amt, gasLim, fee, nonce+1) + tx.Sign(chainID, user[0]) + return tx +} + +func makeDefaultNameTx(t *testing.T, typ string, name, value string, amt, fee uint64) *types.NameTx { + nonce := getNonce(t, typ, user[0].Address) + tx := types.NewNameTxWithNonce(user[0].PubKey, name, value, amt, fee, nonce+1) tx.Sign(chainID, user[0]) return tx } @@ -124,16 +123,16 @@ func makeDefaultCallTx(t *testing.T, typ string, addr, code []byte, amt, gasLim, // rpc call wrappers (fail on err) // get an account's nonce -func getNonce(t *testing.T, typ string, addr []byte) uint64 { +func getNonce(t *testing.T, typ string, addr []byte) uint { client := clients[typ] ac, err := client.GetAccount(addr) if err != nil { t.Fatal(err) } - if ac.Account == nil { + if ac == nil { return 0 } - return uint64(ac.Account.Sequence) + return ac.Sequence } // get the account @@ -143,28 +142,28 @@ func getAccount(t *testing.T, typ string, addr []byte) *account.Account { if err != nil { t.Fatal(err) } - return ac.Account + return ac } // sign transaction func signTx(t *testing.T, typ string, tx types.Tx, privAcc *account.PrivAccount) types.Tx { client := clients[typ] - resp, err := client.SignTx(tx, []*account.PrivAccount{privAcc}) + signedTx, err := client.SignTx(tx, []*account.PrivAccount{privAcc}) if err != nil { t.Fatal(err) } - return resp.Tx + return signedTx } // broadcast transaction -func broadcastTx(t *testing.T, typ string, tx types.Tx) ctypes.Receipt { +func broadcastTx(t *testing.T, typ string, tx types.Tx) *ctypes.Receipt { client := clients[typ] - resp, err := client.BroadcastTx(tx) + rec, err := client.BroadcastTx(tx) if err != nil { t.Fatal(err) } mempoolCount += 1 - return resp.Receipt + return rec } // dump all storage for an account. currently unused @@ -210,6 +209,16 @@ func callContract(t *testing.T, client cclient.Client, address, data, expected [ } } +// get the namereg entry +func getNameRegEntry(t *testing.T, typ string, name string) *types.NameRegEntry { + client := clients[typ] + entry, err := client.GetName(name) + if err != nil { + t.Fatal(err) + } + return entry +} + //-------------------------------------------------------------------------------- // utility verification function diff --git a/rpc/test/tests.go b/rpc/test/tests.go index ca9221ea3..b98914cb8 100644 --- a/rpc/test/tests.go +++ b/rpc/test/tests.go @@ -9,6 +9,8 @@ import ( "testing" ) +var doNothing = func(eid string, b []byte) error { return nil } + func testStatus(t *testing.T, typ string) { client := clients[typ] resp, err := client.Status() @@ -23,11 +25,11 @@ func testStatus(t *testing.T, typ string) { func testGenPriv(t *testing.T, typ string) { client := clients[typ] - resp, err := client.GenPrivAccount() + privAcc, err := client.GenPrivAccount() if err != nil { t.Fatal(err) } - if len(resp.PrivAccount.Address) == 0 { + if len(privAcc.Address) == 0 { t.Fatal("Failed to generate an address") } } @@ -44,13 +46,13 @@ func testGetAccount(t *testing.T, typ string) { func testSignedTx(t *testing.T, typ string) { amt := uint64(100) - toAddr := []byte{20, 143, 25, 63, 16, 177, 83, 29, 91, 91, 54, 23, 233, 46, 190, 121, 122, 34, 86, 54} + toAddr := user[1].Address testOneSignTx(t, typ, toAddr, amt) - toAddr = []byte{20, 143, 24, 63, 16, 17, 83, 29, 90, 91, 52, 2, 0, 41, 190, 121, 122, 34, 86, 54} + toAddr = user[2].Address testOneSignTx(t, typ, toAddr, amt) - toAddr = []byte{0, 0, 4, 0, 0, 4, 0, 0, 4, 91, 52, 2, 0, 41, 190, 121, 122, 34, 86, 54} + toAddr = user[3].Address testOneSignTx(t, typ, toAddr, amt) } @@ -71,7 +73,7 @@ func testOneSignTx(t *testing.T, typ string, addr []byte, amt uint64) { func testBroadcastTx(t *testing.T, typ string) { amt := uint64(100) - toAddr := []byte{20, 143, 25, 63, 16, 177, 83, 29, 91, 91, 54, 23, 233, 46, 190, 121, 122, 34, 86, 54} + toAddr := user[1].Address tx := makeDefaultSendTxSigned(t, typ, toAddr, amt) receipt := broadcastTx(t, typ, tx) if receipt.CreatesContract > 0 { @@ -120,10 +122,7 @@ func testGetStorage(t *testing.T, typ string) { } // allow it to get mined - waitForEvent(t, con, eid, true, func() { - }, func(eid string, b []byte) error { - return nil - }) + waitForEvent(t, con, eid, true, func() {}, doNothing) mempoolCount = 0 v := getStorage(t, typ, contractAddr, []byte{0x1}) @@ -179,10 +178,7 @@ func testCall(t *testing.T, typ string) { } // allow it to get mined - waitForEvent(t, con, eid, true, func() { - }, func(eid string, b []byte) error { - return nil - }) + waitForEvent(t, con, eid, true, func() {}, doNothing) mempoolCount = 0 // run a call through the contract @@ -190,3 +186,76 @@ func testCall(t *testing.T, typ string) { expected := []byte{0xb} callContract(t, client, contractAddr, data, expected) } + +func testNameReg(t *testing.T, typ string) { + client := clients[typ] + con := newWSCon(t) + eid := types.EventStringNewBlock() + subscribe(t, con, eid) + defer func() { + unsubscribe(t, con, eid) + con.Close() + }() + + types.MinNameRegistrationPeriod = 1 + + // register a new name, check if its there + // since entries ought to be unique and these run against different clients, we append the typ + name := "ye_old_domain_name_" + typ + data := "if not now, when" + fee := uint64(1000) + numDesiredBlocks := uint64(2) + amt := fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + + tx := makeDefaultNameTx(t, typ, name, data, amt, fee) + broadcastTx(t, typ, tx) + // commit block + waitForEvent(t, con, eid, true, func() {}, doNothing) + mempoolCount = 0 + entry := getNameRegEntry(t, typ, name) + if entry.Data != data { + t.Fatal(fmt.Sprintf("Err on entry.Data: Got %s, expected %s", entry.Data, data)) + } + if bytes.Compare(entry.Owner, user[0].Address) != 0 { + t.Fatal(fmt.Sprintf("Err on entry.Owner: Got %s, expected %s", entry.Owner, user[0].Address)) + } + + // update the data as the owner, make sure still there + numDesiredBlocks = uint64(2) + data = "these are amongst the things I wish to bestow upon the youth of generations come: a safe supply of honey, and a better money. For what else shall they need" + amt = fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + tx = makeDefaultNameTx(t, typ, name, data, amt, fee) + broadcastTx(t, typ, tx) + // commit block + waitForEvent(t, con, eid, true, func() {}, doNothing) + mempoolCount = 0 + entry = getNameRegEntry(t, typ, name) + if entry.Data != data { + t.Fatal(fmt.Sprintf("Err on entry.Data: Got %s, expected %s", entry.Data, data)) + } + + // try to update as non owner, should fail + nonce := getNonce(t, typ, user[1].Address) + data2 := "this is not my beautiful house" + tx = types.NewNameTxWithNonce(user[1].PubKey, name, data2, amt, fee, nonce+1) + tx.Sign(chainID, user[1]) + _, err := client.BroadcastTx(tx) + if err == nil { + t.Fatal("Expected error on NameTx") + } + + // commit block + waitForEvent(t, con, eid, true, func() {}, doNothing) + + // now the entry should be expired, so we can update as non owner + _, err = client.BroadcastTx(tx) + waitForEvent(t, con, eid, true, func() {}, doNothing) + mempoolCount = 0 + entry = getNameRegEntry(t, typ, name) + if entry.Data != data2 { + t.Fatal(fmt.Sprintf("Error on entry.Data: Got %s, expected %s", entry.Data, data2)) + } + if bytes.Compare(entry.Owner, user[1].Address) != 0 { + t.Fatal(fmt.Sprintf("Err on entry.Owner: Got %s, expected %s", entry.Owner, user[1].Address)) + } +} diff --git a/state/block_cache.go b/state/block_cache.go index 2eda83fa9..8cc4ccb08 100644 --- a/state/block_cache.go +++ b/state/block_cache.go @@ -9,6 +9,7 @@ import ( . "github.com/tendermint/tendermint/common" dbm "github.com/tendermint/tendermint/db" "github.com/tendermint/tendermint/merkle" + "github.com/tendermint/tendermint/types" ) func makeStorage(db dbm.DB, root []byte) merkle.Tree { @@ -27,6 +28,7 @@ type BlockCache struct { backend *State accounts map[string]accountInfo storages map[Tuple256]storageInfo + names map[string]nameInfo } func NewBlockCache(backend *State) *BlockCache { @@ -35,6 +37,7 @@ func NewBlockCache(backend *State) *BlockCache { backend: backend, accounts: make(map[string]accountInfo), storages: make(map[Tuple256]storageInfo), + names: make(map[string]nameInfo), } } @@ -123,6 +126,44 @@ func (cache *BlockCache) SetStorage(addr Word256, key Word256, value Word256) { // BlockCache.storage //------------------------------------- +// BlockCache.names + +func (cache *BlockCache) GetNameRegEntry(name string) *types.NameRegEntry { + entry, removed, _ := cache.names[name].unpack() + if removed { + return nil + } else if entry != nil { + return entry + } else { + entry = cache.backend.GetNameRegEntry(name) + cache.names[name] = nameInfo{entry, false, false} + return entry + } +} + +func (cache *BlockCache) UpdateNameRegEntry(entry *types.NameRegEntry) { + name := entry.Name + // SANITY CHECK + _, removed, _ := cache.names[name].unpack() + if removed { + panic("UpdateNameRegEntry on a removed name") + } + // SANITY CHECK END + cache.names[name] = nameInfo{entry, false, true} +} + +func (cache *BlockCache) RemoveNameRegEntry(name string) { + // SANITY CHECK + _, removed, _ := cache.names[name].unpack() + if removed { + panic("RemoveNameRegEntry on a removed entry") + } + // SANITY CHECK END + cache.names[name] = nameInfo{nil, true, false} +} + +// BlockCache.names +//------------------------------------- // CONTRACT the updates are in deterministic order. func (cache *BlockCache) Sync() { @@ -202,6 +243,32 @@ func (cache *BlockCache) Sync() { } } + // Determine order for names + // note names may be of any length less than some limit + nameStrs := []string{} + for nameStr := range cache.names { + nameStrs = append(nameStrs, nameStr) + } + sort.Strings(nameStrs) + + // Update or delete names. + for _, nameStr := range nameStrs { + entry, removed, dirty := cache.names[nameStr].unpack() + if removed { + removed := cache.backend.RemoveNameRegEntry(nameStr) + if !removed { + panic(Fmt("Could not remove namereg entry to be removed: %s", nameStr)) + } + } else { + if entry == nil { + continue + } + if dirty { + cache.backend.UpdateNameRegEntry(entry) + } + } + } + } //----------------------------------------------------------------------------- @@ -225,3 +292,13 @@ type storageInfo struct { func (stjInfo storageInfo) unpack() (Word256, bool) { return stjInfo.value, stjInfo.dirty } + +type nameInfo struct { + name *types.NameRegEntry + removed bool + dirty bool +} + +func (nInfo nameInfo) unpack() (*types.NameRegEntry, bool, bool) { + return nInfo.name, nInfo.removed, nInfo.dirty +} diff --git a/state/execution.go b/state/execution.go index e46a2150b..83cbea422 100644 --- a/state/execution.go +++ b/state/execution.go @@ -296,7 +296,7 @@ func ExecTx(blockCache *BlockCache, tx_ types.Tx, runCall bool, evc events.Firea // TODO: do something with fees fees := uint64(0) - _s := blockCache.State() // hack to access validators and event switch. + _s := blockCache.State() // hack to access validators and block height // Exec tx switch tx := tx_.(type) { @@ -478,6 +478,120 @@ func ExecTx(blockCache *BlockCache, tx_ types.Tx, runCall bool, evc events.Firea return nil + case *types.NameTx: + var inAcc *account.Account + + // Validate input + inAcc = blockCache.GetAccount(tx.Input.Address) + if inAcc == nil { + log.Debug(Fmt("Can't find in account %X", tx.Input.Address)) + return types.ErrTxInvalidAddress + } + // pubKey should be present in either "inAcc" or "tx.Input" + if err := checkInputPubKey(inAcc, tx.Input); err != nil { + log.Debug(Fmt("Can't find pubkey for %X", tx.Input.Address)) + return err + } + signBytes := account.SignBytes(_s.ChainID, tx) + err := validateInput(inAcc, signBytes, tx.Input) + if err != nil { + log.Debug(Fmt("validateInput failed on %X:", tx.Input.Address)) + return err + } + // fee is in addition to the amount which is used to determine the TTL + if tx.Input.Amount < tx.Fee { + log.Debug(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) + return types.ErrTxInsufficientFunds + } + + // validate the input strings + if err := tx.ValidateStrings(); err != nil { + log.Debug(err.Error()) + return types.ErrTxInvalidString + } + + value := tx.Input.Amount - tx.Fee + + // let's say cost of a name for one block is len(data) + 32 + costPerBlock := types.NameCostPerBlock * types.NameCostPerByte * tx.BaseEntryCost() + expiresIn := value / uint64(costPerBlock) + lastBlockHeight := uint64(_s.LastBlockHeight) + + log.Debug("New NameTx", "value", value, "costPerBlock", costPerBlock, "expiresIn", expiresIn, "lastBlock", lastBlockHeight) + + // check if the name exists + entry := blockCache.GetNameRegEntry(tx.Name) + + if entry != nil { + var expired bool + // if the entry already exists, and hasn't expired, we must be owner + if entry.Expires > lastBlockHeight { + // ensure we are owner + if bytes.Compare(entry.Owner, tx.Input.Address) != 0 { + log.Debug(Fmt("Sender %X is trying to update a name (%s) for which he is not owner", tx.Input.Address, tx.Name)) + return types.ErrIncorrectOwner + } + } else { + expired = true + } + + // no value and empty data means delete the entry + if value == 0 && len(tx.Data) == 0 { + // maybe we reward you for telling us we can delete this crap + // (owners if not expired, anyone if expired) + log.Debug("Removing namereg entry", "name", entry.Name) + blockCache.RemoveNameRegEntry(entry.Name) + } else { + // update the entry by bumping the expiry + // and changing the data + if expired { + if expiresIn < types.MinNameRegistrationPeriod { + return fmt.Errorf("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod) + } + entry.Expires = lastBlockHeight + expiresIn + entry.Owner = tx.Input.Address + log.Debug("An old namereg entry has expired and been reclaimed", "name", entry.Name, "expiresIn", expiresIn, "owner", entry.Owner) + } else { + // since the size of the data may have changed + // we use the total amount of "credit" + oldCredit := (entry.Expires - lastBlockHeight) * types.BaseEntryCost(entry.Name, entry.Data) + credit := oldCredit + value + expiresIn = credit / costPerBlock + if expiresIn < types.MinNameRegistrationPeriod { + return fmt.Errorf("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod) + } + entry.Expires = lastBlockHeight + expiresIn + log.Debug("Updated namereg entry", "name", entry.Name, "expiresIn", expiresIn, "oldCredit", oldCredit, "value", value, "credit", credit) + } + entry.Data = tx.Data + blockCache.UpdateNameRegEntry(entry) + } + } else { + if expiresIn < types.MinNameRegistrationPeriod { + return fmt.Errorf("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod) + } + // entry does not exist, so create it + entry = &types.NameRegEntry{ + Name: tx.Name, + Owner: tx.Input.Address, + Data: tx.Data, + Expires: lastBlockHeight + expiresIn, + } + log.Debug("Creating namereg entry", "name", entry.Name, "expiresIn", expiresIn) + blockCache.UpdateNameRegEntry(entry) + } + + // TODO: something with the value sent? + + // Good! + inAcc.Sequence += 1 + inAcc.Balance -= value + blockCache.UpdateAccount(inAcc) + + // TODO: maybe we want to take funds on error and allow txs in that don't do anythingi? + + return nil + case *types.BondTx: valInfo := blockCache.State().GetValidatorInfo(tx.PubKey.Address()) if valInfo != nil { diff --git a/state/genesis.go b/state/genesis.go index 691597ce0..2024e2117 100644 --- a/state/genesis.go +++ b/state/genesis.go @@ -100,9 +100,14 @@ func MakeGenesisState(db dbm.DB, genDoc *GenesisDoc) *State { } } + // Make namereg tree + nameReg := merkle.NewIAVLTree(binary.BasicCodec, NameRegCodec, 0, db) + // TODO: add names to genesis.json + // IAVLTrees must be persisted before copy operations. accounts.Save() validatorInfos.Save() + nameReg.Save() return &State{ DB: db, @@ -116,5 +121,6 @@ func MakeGenesisState(db dbm.DB, genDoc *GenesisDoc) *State { UnbondingValidators: NewValidatorSet(nil), accounts: accounts, validatorInfos: validatorInfos, + nameReg: nameReg, } } diff --git a/state/state.go b/state/state.go index 13bd7d74d..a36fbff21 100644 --- a/state/state.go +++ b/state/state.go @@ -3,6 +3,7 @@ package state import ( "bytes" "fmt" + "io" "time" "github.com/tendermint/tendermint/account" @@ -36,6 +37,7 @@ type State struct { UnbondingValidators *ValidatorSet accounts merkle.Tree // Shouldn't be accessed directly. validatorInfos merkle.Tree // Shouldn't be accessed directly. + nameReg merkle.Tree // Shouldn't be accessed directly. evc events.Fireable // typically an events.EventCache } @@ -61,6 +63,9 @@ func LoadState(db dbm.DB) *State { validatorInfosHash := binary.ReadByteSlice(r, n, err) s.validatorInfos = merkle.NewIAVLTree(binary.BasicCodec, ValidatorInfoCodec, 0, db) s.validatorInfos.Load(validatorInfosHash) + nameRegHash := binary.ReadByteSlice(r, n, err) + s.nameReg = merkle.NewIAVLTree(binary.BasicCodec, NameRegCodec, 0, db) + s.nameReg.Load(nameRegHash) if *err != nil { panic(*err) } @@ -72,6 +77,7 @@ func LoadState(db dbm.DB) *State { func (s *State) Save() { s.accounts.Save() s.validatorInfos.Save() + s.nameReg.Save() buf, n, err := new(bytes.Buffer), new(int64), new(error) binary.WriteString(s.ChainID, buf, n, err) binary.WriteUvarint(s.LastBlockHeight, buf, n, err) @@ -83,6 +89,7 @@ func (s *State) Save() { binary.WriteBinary(s.UnbondingValidators, buf, n, err) binary.WriteByteSlice(s.accounts.Hash(), buf, n, err) binary.WriteByteSlice(s.validatorInfos.Hash(), buf, n, err) + binary.WriteByteSlice(s.nameReg.Hash(), buf, n, err) if *err != nil { panic(*err) } @@ -105,6 +112,7 @@ func (s *State) Copy() *State { UnbondingValidators: s.UnbondingValidators.Copy(), // copy the valSet lazily. accounts: s.accounts.Copy(), validatorInfos: s.validatorInfos.Copy(), + nameReg: s.nameReg.Copy(), evc: nil, } } @@ -116,6 +124,7 @@ func (s *State) Hash() []byte { s.UnbondingValidators, s.accounts, s.validatorInfos, + s.nameReg, } return merkle.HashFromHashables(hashables) } @@ -272,6 +281,45 @@ func (s *State) LoadStorage(hash []byte) (storage merkle.Tree) { // State.storage //------------------------------------- +// State.nameReg + +func (s *State) GetNameRegEntry(name string) *types.NameRegEntry { + _, value := s.nameReg.Get(name) + if value == nil { + return nil + } + entry := value.(*types.NameRegEntry) + return entry.Copy() +} + +func (s *State) UpdateNameRegEntry(entry *types.NameRegEntry) bool { + return s.nameReg.Set(entry.Name, entry) +} + +func (s *State) RemoveNameRegEntry(name string) bool { + _, removed := s.nameReg.Remove(name) + return removed +} + +func (s *State) GetNames() merkle.Tree { + return s.nameReg.Copy() +} + +func NameRegEncoder(o interface{}, w io.Writer, n *int64, err *error) { + binary.WriteBinary(o.(*types.NameRegEntry), w, n, err) +} + +func NameRegDecoder(r io.Reader, n *int64, err *error) interface{} { + return binary.ReadBinary(&types.NameRegEntry{}, r, n, err) +} + +var NameRegCodec = binary.Codec{ + Encode: NameRegEncoder, + Decode: NameRegDecoder, +} + +// State.nameReg +//------------------------------------- // Implements events.Eventable. Typically uses events.EventCache func (s *State) SetFireable(evc events.Fireable) { diff --git a/state/state_test.go b/state/state_test.go index a2fb2b78a..acb809fbb 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -21,6 +21,15 @@ func execTxWithState(state *State, tx types.Tx, runCall bool) error { } } +func execTxWithStateNewBlock(state *State, tx types.Tx, runCall bool) error { + if err := execTxWithState(state, tx, runCall); err != nil { + return err + } + + state.LastBlockHeight += 1 + return nil +} + func TestCopyState(t *testing.T) { // Generate a random state s0, privAccounts, _ := RandGenesisState(10, true, 1000, 5, true, 1000) @@ -166,31 +175,13 @@ func TestTxSequence(t *testing.T) { acc0PubKey := privAccounts[0].PubKey acc1 := state.GetAccount(privAccounts[1].PubKey.Address()) - // Try executing a SendTx with various sequence numbers. - makeSendTx := func(sequence uint) *types.SendTx { - return &types.SendTx{ - Inputs: []*types.TxInput{ - &types.TxInput{ - Address: acc0.Address, - Amount: 1, - Sequence: sequence, - PubKey: acc0PubKey, - }, - }, - Outputs: []*types.TxOutput{ - &types.TxOutput{ - Address: acc1.Address, - Amount: 1, - }, - }, - } - } - // Test a variety of sequence numbers for the tx. // The tx should only pass when i == 1. for i := -1; i < 3; i++ { sequence := acc0.Sequence + uint(i) - tx := makeSendTx(sequence) + tx := types.NewSendTx() + tx.AddInputWithNonce(acc0PubKey, 1, sequence) + tx.AddOutput(acc1.Address, 1) tx.Inputs[0].Signature = privAccounts[0].Sign(state.ChainID, tx) stateCopy := state.Copy() err := execTxWithState(stateCopy, tx, true) @@ -220,6 +211,168 @@ func TestTxSequence(t *testing.T) { } } +func TestNameTxs(t *testing.T) { + state, privAccounts, _ := RandGenesisState(3, true, 1000, 1, true, 1000) + + types.MinNameRegistrationPeriod = 5 + startingBlock := uint64(state.LastBlockHeight) + + // try some bad names. these should all fail + names := []string{"", "\n", "123#$%", "\x00", string([]byte{20, 40, 60, 80}), "baffledbythespectacleinallofthisyouseeehesaidwithouteyes", "no spaces please"} + data := "something about all this just doesn't feel right." + fee := uint64(1000) + numDesiredBlocks := uint64(5) + for _, name := range names { + amt := fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + + if err := execTxWithState(state, tx, true); err == nil { + t.Fatalf("Expected invalid name error from %s", name) + } + } + + // try some bad data. these should all fail + name := "hold_it_chum" + datas := []string{"cold&warm", "!@#$%^&*()", "<<<>>>>", "because why would you ever need a ~ or a & or even a % in a json file? make your case and we'll talk"} + for _, data := range datas { + amt := fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + + if err := execTxWithState(state, tx, true); err == nil { + t.Fatalf("Expected invalid data error from %s", data) + } + } + + validateEntry := func(t *testing.T, entry *types.NameRegEntry, name, data string, addr []byte, expires uint64) { + + if entry == nil { + t.Fatalf("Could not find name %s", name) + } + if bytes.Compare(entry.Owner, addr) != 0 { + t.Fatalf("Wrong owner. Got %X expected %X", entry.Owner, addr) + } + if data != entry.Data { + t.Fatalf("Wrong data. Got %s expected %s", entry.Data, data) + } + if name != entry.Name { + t.Fatalf("Wrong name. Got %s expected %s", entry.Name, name) + } + if expires != entry.Expires { + t.Fatalf("Wrong expiry. Got %d, expected %d", entry.Expires, expires) + } + } + + // try a good one, check data, owner, expiry + name = "looking_good/karaoke_bar" + data = "on this side of neptune there are 1234567890 people: first is OMNIVORE. Or is it. Ok this is pretty restrictive. No exclamations :(. Faces tho :')" + amt := fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + if err := execTxWithState(state, tx, true); err != nil { + t.Fatal(err) + } + entry := state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks) + + // fail to update it as non-owner, in same block + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithState(state, tx, true); err == nil { + t.Fatal("Expected error") + } + + // update it as owner, just to increase expiry, in same block + // NOTE: we have to resend the data or it will clear it (is this what we want?) + tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + if err := execTxWithStateNewBlock(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks*2) + + // update it as owner, just to increase expiry, in next block + tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + if err := execTxWithStateNewBlock(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks*3) + + // fail to update it as non-owner + state.LastBlockHeight = uint(entry.Expires - 1) + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithState(state, tx, true); err == nil { + t.Fatal("Expected error") + } + + // once expires, non-owner succeeds + state.LastBlockHeight = uint(entry.Expires) + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithState(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[1].Address, uint64(state.LastBlockHeight)+numDesiredBlocks) + + // update it as new owner, with new data (longer), but keep the expiry! + data = "In the beginning there was no thing, not even the beginning. It hadn't been here, no there, nor for that matter anywhere, not especially because it had not to even exist, let alone to not. Nothing especially odd about that." + oldCredit := amt - fee + numDesiredBlocks = 10 + amt = fee + (numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) - oldCredit) + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithState(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[1].Address, uint64(state.LastBlockHeight)+numDesiredBlocks) + + // test removal + amt = fee + data = "" + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithStateNewBlock(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + if entry != nil { + t.Fatal("Expected removed entry to be nil") + } + + // create entry by key0, + // test removal by key1 after expiry + name = "looking_good/karaoke_bar" + data = "some data" + amt = fee + numDesiredBlocks*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) + tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[0]) + if err := execTxWithState(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + validateEntry(t, entry, name, data, privAccounts[0].Address, uint64(state.LastBlockHeight)+numDesiredBlocks) + state.LastBlockHeight = uint(entry.Expires) + + amt = fee + data = "" + tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) + tx.Sign(state.ChainID, privAccounts[1]) + if err := execTxWithStateNewBlock(state, tx, true); err != nil { + t.Fatal(err) + } + entry = state.GetNameRegEntry(name) + if entry != nil { + t.Fatal("Expected removed entry to be nil") + } +} + // TODO: test overflows. // TODO: test for unbonding validators. func TestTxs(t *testing.T) { @@ -268,6 +421,100 @@ func TestTxs(t *testing.T) { } } + // CallTx. Just runs through it and checks the transfer. See vm, rpc tests for more + { + state := state.Copy() + newAcc1 := state.GetAccount(acc1.Address) + newAcc1.Code = []byte{0x60} + state.UpdateAccount(newAcc1) + tx := &types.CallTx{ + Input: &types.TxInput{ + Address: acc0.Address, + Amount: 1, + Sequence: acc0.Sequence + 1, + PubKey: acc0PubKey, + }, + Address: acc1.Address, + GasLimit: 10, + } + + tx.Input.Signature = privAccounts[0].Sign(state.ChainID, tx) + err := execTxWithState(state, tx, true) + if err != nil { + t.Errorf("Got error in executing call transaction, %v", err) + } + newAcc0 := state.GetAccount(acc0.Address) + if acc0.Balance-1 != newAcc0.Balance { + t.Errorf("Unexpected newAcc0 balance. Expected %v, got %v", + acc0.Balance-1, newAcc0.Balance) + } + newAcc1 = state.GetAccount(acc1.Address) + if acc1.Balance+1 != newAcc1.Balance { + t.Errorf("Unexpected newAcc1 balance. Expected %v, got %v", + acc1.Balance+1, newAcc1.Balance) + } + } + + // NameTx. + { + entryName := "satoshi" + entryData := ` +A purely peer-to-peer version of electronic cash would allow online +payments to be sent directly from one party to another without going through a +financial institution. Digital signatures provide part of the solution, but the main +benefits are lost if a trusted third party is still required to prevent double-spending. +We propose a solution to the double-spending problem using a peer-to-peer network. +The network timestamps transactions by hashing them into an ongoing chain of +hash-based proof-of-work, forming a record that cannot be changed without redoing +the proof-of-work. The longest chain not only serves as proof of the sequence of +events witnessed, but proof that it came from the largest pool of CPU power. As +long as a majority of CPU power is controlled by nodes that are not cooperating to +attack the network, they'll generate the longest chain and outpace attackers. The +network itself requires minimal structure. Messages are broadcast on a best effort +basis, and nodes can leave and rejoin the network at will, accepting the longest +proof-of-work chain as proof of what happened while they were gone ` + entryAmount := uint64(10000) + + state := state.Copy() + tx := &types.NameTx{ + Input: &types.TxInput{ + Address: acc0.Address, + Amount: entryAmount, + Sequence: acc0.Sequence + 1, + PubKey: acc0PubKey, + }, + Name: entryName, + Data: entryData, + } + + tx.Input.Signature = privAccounts[0].Sign(state.ChainID, tx) + err := execTxWithState(state, tx, true) + if err != nil { + t.Errorf("Got error in executing call transaction, %v", err) + } + newAcc0 := state.GetAccount(acc0.Address) + if acc0.Balance-entryAmount != newAcc0.Balance { + t.Errorf("Unexpected newAcc0 balance. Expected %v, got %v", + acc0.Balance-entryAmount, newAcc0.Balance) + } + entry := state.GetNameRegEntry(entryName) + if entry == nil { + t.Errorf("Expected an entry but got nil") + } + if entry.Data != entryData { + t.Errorf("Wrong data stored") + } + + // test a bad string + tx.Data = string([]byte{0, 1, 2, 3, 127, 128, 129, 200, 251}) + tx.Input.Sequence += 1 + tx.Input.Signature = privAccounts[0].Sign(state.ChainID, tx) + err = execTxWithState(state, tx, true) + if err != types.ErrTxInvalidString { + t.Errorf("Expected invalid string error. Got: %s", err.Error()) + } + } + // BondTx. { state := state.Copy() diff --git a/types/names.go b/types/names.go new file mode 100644 index 000000000..3e31991fa --- /dev/null +++ b/types/names.go @@ -0,0 +1,48 @@ +package types + +import ( + "regexp" +) + +var ( + MinNameRegistrationPeriod uint64 = 5 + + // cost for storing a name for a block is + // CostPerBlock*CostPerByte*(len(data) + 32) + NameCostPerByte uint64 = 1 + NameCostPerBlock uint64 = 1 + + MaxNameLength = 32 + MaxDataLength = 1 << 16 + + // Name should be alphanum, underscore, slash + // Data should be anything permitted in JSON + regexpAlphaNum = regexp.MustCompile("^[a-zA-Z0-9_/]*$") + regexpJSON = regexp.MustCompile(`^[a-zA-Z0-9_/ \-"':,\n\t.{}()\[\]]*$`) +) + +// filter strings +func validateNameRegEntryName(name string) bool { + return regexpAlphaNum.Match([]byte(name)) +} + +func validateNameRegEntryData(data string) bool { + return regexpJSON.Match([]byte(data)) +} + +// base cost is "effective" number of bytes +func BaseEntryCost(name, data string) uint64 { + return uint64(len(data) + 32) +} + +type NameRegEntry struct { + Name string `json:"name"` // registered name for the entry + Owner []byte `json:"owner"` // address that created the entry + Data string `json:"data"` // data to store under this name + Expires uint64 `json:"expires"` // block at which this entry expires +} + +func (entry *NameRegEntry) Copy() *NameRegEntry { + entryCopy := *entry + return &entryCopy +} diff --git a/types/tx.go b/types/tx.go index 2d365881f..19274e955 100644 --- a/types/tx.go +++ b/types/tx.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "errors" "io" @@ -18,6 +19,8 @@ var ( ErrTxUnknownPubKey = errors.New("Error unknown pubkey") ErrTxInvalidPubKey = errors.New("Error invalid pubkey") ErrTxInvalidSignature = errors.New("Error invalid signature") + ErrTxInvalidString = errors.New("Error invalid string") + ErrIncorrectOwner = errors.New("Error incorrect owner") ) type ErrTxInvalidSequence struct { @@ -35,6 +38,7 @@ Tx (Transaction) is an atomic operation on the ledger state. Account Txs: - SendTx Send coins to address - CallTx Send a msg to a contract that runs in the vm + - NameTx Store some value under a name in the global namereg Validation Txs: - BondTx New validator posts a bond @@ -50,6 +54,7 @@ const ( // Account transactions TxTypeSend = byte(0x01) TxTypeCall = byte(0x02) + TxTypeName = byte(0x03) // Validation transactions TxTypeBond = byte(0x11) @@ -63,6 +68,7 @@ var _ = binary.RegisterInterface( struct{ Tx }{}, binary.ConcreteType{&SendTx{}, TxTypeSend}, binary.ConcreteType{&CallTx{}, TxTypeCall}, + binary.ConcreteType{&NameTx{}, TxTypeName}, binary.ConcreteType{&BondTx{}, TxTypeBond}, binary.ConcreteType{&UnbondTx{}, TxTypeUnbond}, binary.ConcreteType{&RebondTx{}, TxTypeRebond}, @@ -130,8 +136,7 @@ type SendTx struct { } func (tx *SendTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"inputs":[`, TxTypeSend)), w, n, err) for i, in := range tx.Inputs { in.WriteSignBytes(w, n, err) @@ -164,8 +169,7 @@ type CallTx struct { } func (tx *CallTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","data":"%X"`, TxTypeCall, tx.Address, tx.Data)), w, n, err) binary.WriteTo([]byte(Fmt(`,"fee":%v,"gas_limit":%v,"input":`, tx.Fee, tx.GasLimit)), w, n, err) tx.Input.WriteSignBytes(w, n, err) @@ -178,6 +182,54 @@ func (tx *CallTx) String() string { //----------------------------------------------------------------------------- +type NameTx struct { + Input *TxInput `json:"input"` + Name string `json:"name"` + Data string `json:"data"` + Fee uint64 `json:"fee"` +} + +func (tx *NameTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) + binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"data":%s,"fee":%v`, TxTypeName, jsonEscape(tx.Data), tx.Fee)), w, n, err) + binary.WriteTo([]byte(Fmt(`,"input":`, tx.Input)), w, n, err) + tx.Input.WriteSignBytes(w, n, err) + binary.WriteTo([]byte(Fmt(`,"name":%s`, jsonEscape(tx.Name))), w, n, err) + binary.WriteTo([]byte(`}]}`), w, n, err) +} + +func (tx *NameTx) ValidateStrings() error { + if len(tx.Name) == 0 { + return errors.New("Name must not be empty") + } + if len(tx.Name) > MaxNameLength { + return errors.New(Fmt("Name is too long. Max %d bytes", MaxNameLength)) + } + if len(tx.Data) > MaxDataLength { + return errors.New(Fmt("Data is too long. Max %d bytes", MaxDataLength)) + } + + if !validateNameRegEntryName(tx.Name) { + return errors.New(Fmt("Invalid characters found in NameTx.Name (%s). Only alphanumeric, underscores, and forward slashes allowed", tx.Name)) + } + + if !validateNameRegEntryData(tx.Data) { + return errors.New(Fmt("Invalid characters found in NameTx.Data (%s). Only the kind of things found in a JSON file are allowed", tx.Data)) + } + + return nil +} + +func (tx *NameTx) BaseEntryCost() uint64 { + return BaseEntryCost(tx.Name, tx.Data) +} + +func (tx *NameTx) String() string { + return Fmt("NameTx{%v -> %s: %s}", tx.Input, tx.Name, tx.Data) +} + +//----------------------------------------------------------------------------- + type BondTx struct { PubKey account.PubKeyEd25519 `json:"pub_key"` Signature account.SignatureEd25519 `json:"signature"` @@ -186,8 +238,7 @@ type BondTx struct { } func (tx *BondTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"inputs":[`, TxTypeBond)), w, n, err) for i, in := range tx.Inputs { in.WriteSignBytes(w, n, err) @@ -220,8 +271,7 @@ type UnbondTx struct { } func (tx *UnbondTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","height":%v}]}`, TxTypeUnbond, tx.Address, tx.Height)), w, n, err) } @@ -238,8 +288,7 @@ type RebondTx struct { } func (tx *RebondTx) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":%s`, jsonEscape(chainID))), w, n, err) binary.WriteTo([]byte(Fmt(`,"tx":[%v,{"address":"%X","height":%v}]}`, TxTypeRebond, tx.Address, tx.Height)), w, n, err) } @@ -269,3 +318,14 @@ func TxId(chainID string, tx Tx) []byte { signBytes := account.SignBytes(chainID, tx) return binary.BinaryRipemd160(signBytes) } + +//-------------------------------------------------------------------------------- + +// Contract: This function is deterministic and completely reversible. +func jsonEscape(str string) string { + escapedBytes, err := json.Marshal(str) + if err != nil { + panic(Fmt("Error json-escaping a string", str)) + } + return string(escapedBytes) +} diff --git a/types/tx_test.go b/types/tx_test.go index 5ac1f908d..703f873ed 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -41,7 +41,7 @@ func TestSendTxSignable(t *testing.T) { } signBytes := account.SignBytes(chainID, sendTx) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","tx":[1,{"inputs":[{"address":"696E70757431","amount":12345,"sequence":67890},{"address":"696E70757432","amount":111,"sequence":222}],"outputs":[{"address":"6F757470757431","amount":333},{"address":"6F757470757432","amount":444}]}]}`, + expected := Fmt(`{"chain_id":"%s","tx":[1,{"inputs":[{"address":"696E70757431","amount":12345,"sequence":67890},{"address":"696E70757432","amount":111,"sequence":222}],"outputs":[{"address":"6F757470757431","amount":333},{"address":"6F757470757432","amount":444}]}]}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for SendTx. Expected:\n%v\nGot:\n%v", expected, signStr) @@ -62,7 +62,7 @@ func TestCallTxSignable(t *testing.T) { } signBytes := account.SignBytes(chainID, callTx) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","tx":[2,{"address":"636F6E747261637431","data":"6461746131","fee":222,"gas_limit":111,"input":{"address":"696E70757431","amount":12345,"sequence":67890}}]}`, + expected := Fmt(`{"chain_id":"%s","tx":[2,{"address":"636F6E747261637431","data":"6461746131","fee":222,"gas_limit":111,"input":{"address":"696E70757431","amount":12345,"sequence":67890}}]}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for CallTx. Expected:\n%v\nGot:\n%v", expected, signStr) @@ -98,7 +98,7 @@ func TestBondTxSignable(t *testing.T) { } signBytes := account.SignBytes(chainID, bondTx) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","tx":[17,{"inputs":[{"address":"696E70757431","amount":12345,"sequence":67890},{"address":"696E70757432","amount":111,"sequence":222}],"pub_key":[1,"3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29"],"unbond_to":[{"address":"6F757470757431","amount":333},{"address":"6F757470757432","amount":444}]}]}`, + expected := Fmt(`{"chain_id":"%s","tx":[17,{"inputs":[{"address":"696E70757431","amount":12345,"sequence":67890},{"address":"696E70757432","amount":111,"sequence":222}],"pub_key":[1,"3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29"],"unbond_to":[{"address":"6F757470757431","amount":333},{"address":"6F757470757432","amount":444}]}]}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for BondTx") @@ -112,7 +112,7 @@ func TestUnbondTxSignable(t *testing.T) { } signBytes := account.SignBytes(chainID, unbondTx) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","tx":[18,{"address":"6164647265737331","height":111}]}`, + expected := Fmt(`{"chain_id":"%s","tx":[18,{"address":"6164647265737331","height":111}]}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for UnbondTx") @@ -126,7 +126,7 @@ func TestRebondTxSignable(t *testing.T) { } signBytes := account.SignBytes(chainID, rebondTx) signStr := string(signBytes) - expected := Fmt(`{"chain_id":"%X","tx":[19,{"address":"6164647265737331","height":111}]}`, + expected := Fmt(`{"chain_id":"%s","tx":[19,{"address":"6164647265737331","height":111}]}`, config.GetString("chain_id")) if signStr != expected { t.Errorf("Got unexpected sign string for RebondTx") diff --git a/types/tx_utils.go b/types/tx_utils.go index eb5491ad1..0c878e314 100644 --- a/types/tx_utils.go +++ b/types/tx_utils.go @@ -25,23 +25,15 @@ func (tx *SendTx) AddInput(st AccountGetter, pubkey account.PubKey, amt uint64) if acc == nil { return fmt.Errorf("Invalid address %X from pubkey %X", addr, pubkey) } - - tx.Inputs = append(tx.Inputs, &TxInput{ - Address: addr, - Amount: amt, - Sequence: uint(acc.Sequence) + 1, - Signature: account.SignatureEd25519{}, - PubKey: pubkey, - }) - return nil + return tx.AddInputWithNonce(pubkey, amt, acc.Sequence+1) } -func (tx *SendTx) AddInputWithNonce(pubkey account.PubKey, amt, nonce uint64) error { +func (tx *SendTx) AddInputWithNonce(pubkey account.PubKey, amt uint64, nonce uint) error { addr := pubkey.Address() tx.Inputs = append(tx.Inputs, &TxInput{ Address: addr, Amount: amt, - Sequence: uint(nonce) + 1, + Sequence: nonce, Signature: account.SignatureEd25519{}, PubKey: pubkey, }) @@ -75,16 +67,16 @@ func NewCallTx(st AccountGetter, from account.PubKey, to, data []byte, amt, gasL return nil, fmt.Errorf("Invalid address %X from pubkey %X", addr, from) } - nonce := uint64(acc.Sequence) + nonce := acc.Sequence + 1 return NewCallTxWithNonce(from, to, data, amt, gasLimit, fee, nonce), nil } -func NewCallTxWithNonce(from account.PubKey, to, data []byte, amt, gasLimit, fee, nonce uint64) *CallTx { +func NewCallTxWithNonce(from account.PubKey, to, data []byte, amt, gasLimit, fee uint64, nonce uint) *CallTx { addr := from.Address() input := &TxInput{ Address: addr, Amount: amt, - Sequence: uint(nonce) + 1, + Sequence: nonce, Signature: account.SignatureEd25519{}, PubKey: from, } @@ -103,6 +95,43 @@ func (tx *CallTx) Sign(chainID string, privAccount *account.PrivAccount) { tx.Input.Signature = privAccount.Sign(chainID, tx) } +//---------------------------------------------------------------------------- +// NameTx interface for creating tx + +func NewNameTx(st AccountGetter, from account.PubKey, name, data string, amt, fee uint64) (*NameTx, error) { + addr := from.Address() + acc := st.GetAccount(addr) + if acc == nil { + return nil, fmt.Errorf("Invalid address %X from pubkey %X", addr, from) + } + + nonce := acc.Sequence + 1 + return NewNameTxWithNonce(from, name, data, amt, fee, nonce), nil +} + +func NewNameTxWithNonce(from account.PubKey, name, data string, amt, fee uint64, nonce uint) *NameTx { + addr := from.Address() + input := &TxInput{ + Address: addr, + Amount: amt, + Sequence: nonce, + Signature: account.SignatureEd25519{}, + PubKey: from, + } + + return &NameTx{ + Input: input, + Name: name, + Data: data, + Fee: fee, + } +} + +func (tx *NameTx) Sign(chainID string, privAccount *account.PrivAccount) { + tx.Input.PubKey = privAccount.PubKey + tx.Input.Signature = privAccount.Sign(chainID, tx) +} + //---------------------------------------------------------------------------- // BondTx interface for adding inputs/outputs and adding signatures @@ -124,23 +153,15 @@ func (tx *BondTx) AddInput(st AccountGetter, pubkey account.PubKey, amt uint64) if acc == nil { return fmt.Errorf("Invalid address %X from pubkey %X", addr, pubkey) } - - tx.Inputs = append(tx.Inputs, &TxInput{ - Address: addr, - Amount: amt, - Sequence: uint(acc.Sequence) + 1, - Signature: account.SignatureEd25519{}, - PubKey: pubkey, - }) - return nil + return tx.AddInputWithNonce(pubkey, amt, acc.Sequence+1) } -func (tx *BondTx) AddInputWithNonce(pubkey account.PubKey, amt, nonce uint64) error { +func (tx *BondTx) AddInputWithNonce(pubkey account.PubKey, amt uint64, nonce uint) error { addr := pubkey.Address() tx.Inputs = append(tx.Inputs, &TxInput{ Address: addr, Amount: amt, - Sequence: uint(nonce) + 1, + Sequence: nonce, Signature: account.SignatureEd25519{}, PubKey: pubkey, }) diff --git a/types/vote.go b/types/vote.go index def488e92..365b5a9af 100644 --- a/types/vote.go +++ b/types/vote.go @@ -46,8 +46,7 @@ const ( ) func (vote *Vote) WriteSignBytes(chainID string, w io.Writer, n *int64, err *error) { - // We hex encode the chain_id name so we don't deal with escaping issues. - binary.WriteTo([]byte(Fmt(`{"chain_id":"%X"`, chainID)), w, n, err) + binary.WriteTo([]byte(Fmt(`{"chain_id":"%s"`, chainID)), w, n, err) binary.WriteTo([]byte(Fmt(`,"vote":{"block_hash":"%X","block_parts":%v`, vote.BlockHash, vote.BlockParts)), w, n, err) binary.WriteTo([]byte(Fmt(`,"height":%v,"round":%v,"type":%v}}`, vote.Height, vote.Round, vote.Type)), w, n, err) }