From fd648a67821afed0c9c96b637ed7a37013de970c Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Tue, 22 Mar 2016 13:07:03 -0700 Subject: [PATCH] Hook into tmsp app --- app/app.go | 37 ++-- app/validate.go | 10 -- state/execution.go | 415 +++++++++++++++++++-------------------------- state/state.go | 48 +++++- types/errors.go | 1 + types/tx.go | 9 +- 6 files changed, 238 insertions(+), 282 deletions(-) delete mode 100644 app/validate.go diff --git a/app/app.go b/app/app.go index 2c4e434e7..42654374c 100644 --- a/app/app.go +++ b/app/app.go @@ -1,10 +1,11 @@ package app import ( + "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/types" . "github.com/tendermint/go-common" "github.com/tendermint/go-wire" - gov "github.com/tendermint/governmint/gov" + "github.com/tendermint/governmint/gov" eyes "github.com/tendermint/merkleeyes/client" tmsp "github.com/tendermint/tmsp/types" ) @@ -15,23 +16,29 @@ const maxTxSize = 10240 type Basecoin struct { eyesCli *eyes.Client govMint *gov.Governmint + state *state.State } func NewBasecoin(eyesCli *eyes.Client) *Basecoin { + govMint := gov.NewGovernmint(eyesCli) return &Basecoin{ eyesCli: eyesCli, - govMint: gov.NewGovernmint(eyesCli), + govMint: govMint, + state: state.NewState(eyesCli), } } // TMSP::Info func (app *Basecoin) Info() string { - return Fmt("Basecoin v%v\n - %v", version, app.govMint.Info()) + return Fmt("Basecoin v%v", version) } // TMSP::SetOption func (app *Basecoin) SetOption(key string, value string) (log string) { - if key == "setAccount" { + switch key { + case "chainID": + app.state.SetChainID(value) + case "account": var err error var setAccount types.Account wire.ReadJSONPtr(&setAccount, []byte(value), &err) @@ -59,16 +66,11 @@ func (app *Basecoin) AppendTx(txBytes []byte) (res tmsp.Result) { if err != nil { return types.ErrEncodingError.AppendLog("Error decoding tx: " + err.Error()) } - // Validate tx - res = validateTx(tx) + // Validate and exec tx + res = state.ExecTx(app.state, tx, false, nil) if !res.IsOK() { - return res.PrependLog("Error validating tx") + return res.PrependLog("Error in AppendTx") } - // Execute tx - // TODO: get or make state with app.eeysCli, pass it to - // state.execution.go ExecTx - // Synchronize the txCache. - //storeAccounts(app.eyesCli, accs) return types.ResultOK } @@ -84,16 +86,11 @@ func (app *Basecoin) CheckTx(txBytes []byte) (res tmsp.Result) { return types.ErrEncodingError.AppendLog("Error decoding tx: " + err.Error()) } // Validate tx - res = validateTx(tx) + res = state.ExecTx(app.state, tx, true, nil) if !res.IsOK() { - return res.PrependLog("Error validating tx") + return res.PrependLog("Error in CheckTx") } - // Execute tx - // TODO: get or make state with app.eeysCli, pass it to - // state.execution.go ExecTx - // Synchronize the txCache. - //storeAccounts(app.eyesCli, accs) - return types.ResultOK.SetLog("Success") + return types.ResultOK } // TMSP::Query diff --git a/app/validate.go b/app/validate.go deleted file mode 100644 index fde0f31d6..000000000 --- a/app/validate.go +++ /dev/null @@ -1,10 +0,0 @@ -package app - -import ( - "github.com/tendermint/basecoin/types" - tmsp "github.com/tendermint/tmsp/types" -) - -func validateTx(tx types.Tx) (res tmsp.Result) { - return -} diff --git a/state/execution.go b/state/execution.go index 3f40682a7..4723942b8 100644 --- a/state/execution.go +++ b/state/execution.go @@ -2,17 +2,149 @@ package state import ( "bytes" + "strings" "github.com/tendermint/basecoin/types" . "github.com/tendermint/go-common" "github.com/tendermint/go-events" + tmsp "github.com/tendermint/tmsp/types" ) +// If the tx is invalid, a TMSP error will be returned. +func ExecTx(state *State, tx types.Tx, isCheckTx bool, evc events.Fireable) tmsp.Result { + + // TODO: do something with fees + fees := int64(0) + + // Exec tx + switch tx := tx.(type) { + case *types.SendTx: + // First, get inputs + accounts, res := getInputs(state, tx.Inputs) + if !res.IsOK() { + return res + } + + // Then, get or make outputs. + accounts, res = getOrMakeOutputs(state, accounts, tx.Outputs) + if !res.IsOK() { + return res + } + + // Validate inputs and outputs + signBytes := tx.SignBytes(state.GetChainID()) + inTotal, res := validateInputs(state, accounts, signBytes, tx.Inputs) + if !res.IsOK() { + return res + } + outTotal, res := validateOutputs(tx.Outputs) + if !res.IsOK() { + return res + } + if outTotal > inTotal { + return types.ErrInsufficientFunds + } + fee := inTotal - outTotal + fees += fee + + // TODO: Fee validation for SendTx + + // Good! Adjust accounts + adjustByInputs(state, accounts, tx.Inputs, isCheckTx) + adjustByOutputs(state, accounts, tx.Outputs, isCheckTx) + + /* + // Fire events + if !isCheckTx { + if evc != nil { + for _, i := range tx.Inputs { + evc.FireEvent(types.EventStringAccInput(i.Address), types.EventDataTx{tx, nil, ""}) + } + for _, o := range tx.Outputs { + evc.FireEvent(types.EventStringAccOutput(o.Address), types.EventDataTx{tx, nil, ""}) + } + } + } + */ + + return types.ResultOK + + case *types.CallTx: + // First, get input account + inAcc := state.GetAccount(tx.Input.Address) + if inAcc == nil { + log.Info(Fmt("Can't find in account %X", tx.Input.Address)) + return types.ErrInvalidAddress + } + + // Validate input + // pubKey should be present in either "inAcc" or "tx.Input" + if res := checkInputPubKey(tx.Input.Address, inAcc, tx.Input); !res.IsOK() { + log.Info(Fmt("Can't find pubkey for %X", tx.Input.Address)) + return res + } + signBytes := tx.SignBytes(state.GetChainID()) + res := validateInput(state, inAcc, signBytes, tx.Input) + if !res.IsOK() { + log.Info(Fmt("validateInput failed on %X: %v", tx.Input.Address, res)) + return res + } + if tx.Input.Amount < tx.Fee { + log.Info(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) + return types.ErrInsufficientFunds + } + + // Validate call address + if strings.HasPrefix(string(tx.Address), "gov/") { + // This is a gov call. + } else { + return types.ErrInvalidAddress.AppendLog(Fmt("Unrecognized address %X", tx.Address)) + } + + // Good! + value := tx.Input.Amount - tx.Fee + inAcc.Sequence += 1 + inAcc.Balance -= tx.Input.Amount + state.SetCheckAccount(tx.Input.Address, inAcc.Sequence, inAcc.Balance) + + // If this is AppendTx, actually save accounts + if !isCheckTx { + state.SetAccount(inAcc) + // NOTE: value is dangling. + // XXX: don't just give it back + inAcc.Balance += value + // TODO: logic. + // TODO: persist + // state.SetAccount(inAcc) + log.Info("Successful execution") + // Fire events + /* + if evc != nil { + exception := "" + if !res.IsOK() { + exception = res.Error() + } + evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventDataTx{tx, ret, exception}) + evc.FireEvent(types.EventStringAccOutput(tx.Address), types.EventDataTx{tx, ret, exception}) + } + */ + } + + return types.ResultOK + + default: + PanicSanity("Unknown Tx type") + return types.ErrInternalError + } +} + +//-------------------------------------------------------------------------------- + // The accounts from the TxInputs must either already have // crypto.PubKey.(type) != nil, (it must be known), // or it must be specified in the TxInput. If redeclared, // the TxInput is modified and input.PubKey set to nil. -func getInputs(state types.AccountGetter, ins []types.TxInput) (map[string]*types.Account, error) { +func getInputs(state types.AccountGetter, ins []types.TxInput) (map[string]*types.Account, tmsp.Result) { accounts := map[string]*types.Account{} for _, in := range ins { // Account shouldn't be duplicated @@ -24,15 +156,15 @@ func getInputs(state types.AccountGetter, ins []types.TxInput) (map[string]*type return nil, types.ErrInvalidAddress } // PubKey should be present in either "account" or "in" - if err := checkInputPubKey(in.Address, acc, in); err != nil { - return nil, err + if res := checkInputPubKey(in.Address, acc, in); !res.IsOK() { + return nil, res } accounts[string(in.Address)] = acc } - return accounts, nil + return accounts, types.ResultOK } -func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Account, outs []types.TxOutput) (map[string]*types.Account, error) { +func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Account, outs []types.TxOutput) (map[string]*types.Account, tmsp.Result) { if accounts == nil { accounts = make(map[string]*types.Account) } @@ -53,12 +185,12 @@ func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Acco } accounts[string(out.Address)] = acc } - return accounts, nil + return accounts, types.ResultOK } // Input must not have a redundant PubKey (i.e. Account already has PubKey). // NOTE: Account has PubKey if Sequence > 0 -func checkInputPubKey(address []byte, acc *types.Account, in types.TxInput) error { +func checkInputPubKey(address []byte, acc *types.Account, in types.TxInput) tmsp.Result { if acc.PubKey == nil { if in.PubKey == nil { return types.ErrUnknownPubKey @@ -72,58 +204,61 @@ func checkInputPubKey(address []byte, acc *types.Account, in types.TxInput) erro return types.ErrInvalidPubKey } } - return nil + return types.ResultOK } -func validateInputs(accounts map[string]*types.Account, signBytes []byte, ins []types.TxInput) (total int64, err error) { +// Validate inputs and compute total amount +func validateInputs(state *State, accounts map[string]*types.Account, signBytes []byte, ins []types.TxInput) (total int64, res tmsp.Result) { + for _, in := range ins { acc := accounts[string(in.Address)] if acc == nil { PanicSanity("validateInputs() expects account in accounts") } - err = validateInput(acc, signBytes, in) - if err != nil { + res = validateInput(state, acc, signBytes, in) + if !res.IsOK() { return } // Good. Add amount to total total += in.Amount } - return total, nil + return total, types.ResultOK } -func validateInput(acc *types.Account, signBytes []byte, in types.TxInput) (err error) { +func validateInput(state *State, acc *types.Account, signBytes []byte, in types.TxInput) (res tmsp.Result) { // Check TxInput basic - if err := in.ValidateBasic(); err != nil { - return err + if res := in.ValidateBasic(); !res.IsOK() { + return res + } + // Check sequence/balance + seq, balance := state.GetCheckAccount(in.Address, acc.Sequence, acc.Balance) + if seq+1 != in.Sequence { + return types.ErrInvalidSequence.AppendLog(Fmt("Got %v, expected %v. (acc.seq=%v)", in.Sequence, seq+1, acc.Sequence)) + } + // Check amount + if balance < in.Amount { + return types.ErrInsufficientFunds } // Check signatures if !acc.PubKey.VerifyBytes(signBytes, in.Signature) { return types.ErrInvalidSignature } - // Check sequences - if acc.Sequence+1 != in.Sequence { - return types.ErrInvalidSequence.AppendLog(Fmt("Got %v, expected %v", in.Sequence, acc.Sequence+1)) - } - // Check amount - if acc.Balance < in.Amount { - return types.ErrInsufficientFunds - } - return nil + return types.ResultOK } -func validateOutputs(outs []types.TxOutput) (total int64, err error) { +func validateOutputs(outs []types.TxOutput) (total int64, res tmsp.Result) { for _, out := range outs { // Check TxOutput basic - if err := out.ValidateBasic(); err != nil { - return 0, err + if res := out.ValidateBasic(); !res.IsOK() { + return 0, res } // Good. Add amount to total total += out.Amount } - return total, nil + return total, types.ResultOK } -func adjustByInputs(accounts map[string]*types.Account, ins []types.TxInput) { +func adjustByInputs(state *State, accounts map[string]*types.Account, ins []types.TxInput, isCheckTx bool) { for _, in := range ins { acc := accounts[string(in.Address)] if acc == nil { @@ -134,229 +269,25 @@ func adjustByInputs(accounts map[string]*types.Account, ins []types.TxInput) { } acc.Balance -= in.Amount acc.Sequence += 1 + state.SetCheckAccount(in.Address, acc.Sequence, acc.Balance) + if !isCheckTx { + // NOTE: Must be set in deterministic order + state.SetAccount(acc) + } } } -func adjustByOutputs(accounts map[string]*types.Account, outs []types.TxOutput) { +func adjustByOutputs(state *State, accounts map[string]*types.Account, outs []types.TxOutput, isCheckTx bool) { for _, out := range outs { acc := accounts[string(out.Address)] if acc == nil { PanicSanity("adjustByOutputs() expects account in accounts") } acc.Balance += out.Amount - } -} - -// If the tx is invalid, an error will be returned. -// Unlike ExecBlock(), state will not be altered. -func ExecTx(state *State, tx types.Tx, runCall bool, evc events.Fireable) (err error) { - - // TODO: do something with fees - fees := int64(0) - - // Exec tx - switch tx := tx.(type) { - case *types.SendTx: - accounts, err := getInputs(state, tx.Inputs) - if err != nil { - return err - } - - // add outputs to accounts map - // if any outputs don't exist, all inputs must have CreateAccount perm - accounts, err = getOrMakeOutputs(state, accounts, tx.Outputs) - if err != nil { - return err - } - - signBytes := tx.SignBytes(state.ChainID()) - inTotal, err := validateInputs(accounts, signBytes, tx.Inputs) - if err != nil { - return err - } - outTotal, err := validateOutputs(tx.Outputs) - if err != nil { - return err - } - if outTotal > inTotal { - return types.ErrInsufficientFunds - } - fee := inTotal - outTotal - fees += fee - - // Good! Adjust accounts - adjustByInputs(accounts, tx.Inputs) - adjustByOutputs(accounts, tx.Outputs) - for _, acc := range accounts { + if !isCheckTx { + state.SetCheckAccount(out.Address, acc.Sequence, acc.Balance) + // NOTE: Must be set in deterministic order state.SetAccount(acc) } - - // if the evc is nil, nothing will happen - /* - if evc != nil { - for _, i := range tx.Inputs { - evc.FireEvent(types.EventStringAccInput(i.Address), types.EventDataTx{tx, nil, ""}) - } - for _, o := range tx.Outputs { - evc.FireEvent(types.EventStringAccOutput(o.Address), types.EventDataTx{tx, nil, ""}) - } - } - */ - return nil - - case *types.CallTx: - var inAcc, outAcc *types.Account - - // Validate input - inAcc = state.GetAccount(tx.Input.Address) - if inAcc == nil { - log.Info(Fmt("Can't find in account %X", tx.Input.Address)) - return types.ErrInvalidAddress - } - - // pubKey should be present in either "inAcc" or "tx.Input" - if err := checkInputPubKey(tx.Input.Address, inAcc, tx.Input); err != nil { - log.Info(Fmt("Can't find pubkey for %X", tx.Input.Address)) - return err - } - signBytes := tx.SignBytes(state.ChainID()) - err := validateInput(inAcc, signBytes, tx.Input) - if err != nil { - log.Info(Fmt("validateInput failed on %X: %v", tx.Input.Address, err)) - return err - } - if tx.Input.Amount < tx.Fee { - log.Info(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) - return types.ErrInsufficientFunds - } - - if len(tx.Address) == 0 { - return types.ErrInvalidAddress.AppendLog("Address cannot be zero") - } - // Validate output - if len(tx.Address) != 20 { - log.Info(Fmt("Destination address is not 20 bytes %X", tx.Address)) - return types.ErrInvalidAddress - } - // check if its a native contract - // XXX if IsNativeContract(tx.Address) {...} - - // Output account may be nil if we are still in mempool and contract was created in same block as this tx - // but that's fine, because the account will be created properly when the create tx runs in the block - // and then this won't return nil. otherwise, we take their fee - outAcc = state.GetAccount(tx.Address) - - log.Info(Fmt("Out account: %v", outAcc)) - - // Good! - value := tx.Input.Amount - tx.Fee - inAcc.Sequence += 1 - inAcc.Balance -= tx.Fee - state.SetAccount(inAcc) - - // The logic in runCall MUST NOT return. - if runCall { - - // VM call variables - var ( - // gas int64 = tx.GasLimit - err error = nil - // caller *vm.Account = toVMAccount(inAcc) - // callee *vm.Account = nil // initialized below - // code []byte = nil - // ret []byte = nil - // txCache = NewTxCache(state) - /* - params = vm.Params{ - BlockHeight: int64(state.LastBlockHeight), - BlockHash: LeftPadWord256(state.LastBlockHash), - BlockTime: state.LastBlockTime.Unix(), - GasLimit: state.GetGasLimit(), - } - */ - ) - - // if you call an account that doesn't exist - // or an account with no code then we take fees (sorry pal) - // NOTE: it's fine to create a contract and call it within one - // block (nonce will prevent re-ordering of those txs) - // but to create with one contract and call with another - // you have to wait a block to avoid a re-ordering attack - // that will take your fees - if outAcc == nil { - log.Info(Fmt("%X tries to call %X but it does not exist.", tx.Input.Address, tx.Address)) - err = types.ErrInvalidAddress - goto CALL_COMPLETE - } - /* - if len(outAcc.Code) == 0 { - log.Info(Fmt("%X tries to call %X but code is blank.", inAcc.Address, tx.Address)) - err = types.ErrInvalidAddress - goto CALL_COMPLETE - } - log.Info(Fmt("Code for this contract: %X", code)) - */ - - // Run VM call and sync txCache to state. - { // Capture scope for goto. - // Write caller/callee to txCache. - // txCache.SetAccount(caller) - // txCache.SetAccount(callee) - // vmach := vm.NewVM(txCache, params, caller.Address, types.TxID(state.ChainID(), tx)) - // vmach.SetFireable(evc) - // NOTE: Call() transfers the value from caller to callee iff call succeeds. - // ret, err = vmach.Call(caller, callee, code, tx.Data, value, &gas) - if err != nil { - // Failure. Charge the gas fee. The 'value' was otherwise not transferred. - log.Info(Fmt("Error on execution: %v", err)) - goto CALL_COMPLETE - } - log.Info("Successful execution") - // txCache.Sync() - } - - CALL_COMPLETE: // err may or may not be nil. - - // Create a receipt from the ret and whether errored. - // log.Notice("VM call complete", "caller", caller, "callee", callee, "return", ret, "err", err) - - // Fire Events for sender and receiver - // a separate event will be fired from vm for each additional call - /* - if evc != nil { - exception := "" - if err != nil { - exception = err.Error() - } - evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventDataTx{tx, ret, exception}) - evc.FireEvent(types.EventStringAccOutput(tx.Address), types.EventDataTx{tx, ret, exception}) - } - */ - } else { - // The mempool does not call txs until - // the proposer determines the order of txs. - // So mempool will skip the actual .Call(), - // and only deduct from the caller's balance. - inAcc.Balance -= value - state.SetAccount(inAcc) - } - - return nil - - default: - // binary decoding should not let this happen - PanicSanity("Unknown Tx type") - return nil } } - -//----------------------------------------------------------------------------- - -type InvalidTxError struct { - Tx types.Tx - Reason error -} - -func (txErr InvalidTxError) Error() string { - return Fmt("Invalid tx: [%v] reason: [%v]", txErr.Tx, txErr.Reason) -} diff --git a/state/state.go b/state/state.go index ed2d06000..420322458 100644 --- a/state/state.go +++ b/state/state.go @@ -2,31 +2,67 @@ package state import ( "github.com/tendermint/basecoin/types" + . "github.com/tendermint/go-common" "github.com/tendermint/go-wire" eyes "github.com/tendermint/merkleeyes/client" ) type State struct { - chainID string - eyesCli *eyes.Client + chainID string + eyesCli *eyes.Client + checkCache map[string]checkAccount LastBlockHeight uint64 LastBlockHash []byte GasLimit int64 } -func NewState(chainID string, eyesCli *eyes.Client) *State { +func NewState(eyesCli *eyes.Client) *State { s := &State{ - chainID: chainID, - eyesCli: eyesCli, + chainID: "", + eyesCli: eyesCli, + checkCache: make(map[string]checkAccount), } return s } -func (s *State) ChainID() string { +func (s *State) SetChainID(chainID string) { + s.chainID = chainID +} + +func (s *State) GetChainID() string { + if s.chainID == "" { + PanicSanity("Expected to have set SetChainID") + } return s.chainID } +//---------------------------------------- +// CheckTx state + +type checkAccount struct { + sequence int + balance int64 +} + +func (s *State) GetCheckAccount(addr []byte, defaultSequence int, defaultBalance int64) (sequence int, balance int64) { + cAcc, ok := s.checkCache[string(addr)] + if !ok { + return defaultSequence, defaultBalance + } + return cAcc.sequence, cAcc.balance +} + +func (s *State) SetCheckAccount(addr []byte, sequence int, balance int64) { + s.checkCache[string(addr)] = checkAccount{sequence, balance} +} + +func (s *State) ResetCacheState() { + s.checkCache = make(map[string]checkAccount) +} + +//---------------------------------------- + func (s *State) GetAccount(addr []byte) *types.Account { accBytes, err := s.eyesCli.GetSync(addr) if err != nil { diff --git a/types/errors.go b/types/errors.go index 534b11c16..fd34602c9 100644 --- a/types/errors.go +++ b/types/errors.go @@ -5,6 +5,7 @@ import ( ) var ( + ErrInternalError = tmsp.NewError(tmsp.CodeType_InternalError, "Internal error") ErrDuplicateAddress = tmsp.NewError(tmsp.CodeType_BaseDuplicateAddress, "Error duplicate address") ErrEncodingError = tmsp.NewError(tmsp.CodeType_BaseEncodingError, "Error encoding error") ErrInsufficientFees = tmsp.NewError(tmsp.CodeType_BaseInsufficientFees, "Error insufficient fees") diff --git a/types/tx.go b/types/tx.go index 1835a3077..62438a1d1 100644 --- a/types/tx.go +++ b/types/tx.go @@ -7,6 +7,7 @@ import ( . "github.com/tendermint/go-common" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" + tmsp "github.com/tendermint/tmsp/types" "golang.org/x/crypto/ripemd160" ) @@ -45,14 +46,14 @@ type TxInput struct { PubKey crypto.PubKey `json:"pub_key"` // May be nil } -func (txIn TxInput) ValidateBasic() error { +func (txIn TxInput) ValidateBasic() tmsp.Result { if len(txIn.Address) != 20 { return ErrInvalidAddress } if txIn.Amount == 0 { return ErrInvalidAmount } - return nil + return ResultOK } func (txIn TxInput) SignBytes() []byte { @@ -71,14 +72,14 @@ type TxOutput struct { Amount int64 `json:"amount"` // The sum of all outputs must not exceed the inputs. } -func (txOut TxOutput) ValidateBasic() error { +func (txOut TxOutput) ValidateBasic() tmsp.Result { if len(txOut.Address) != 20 { return ErrInvalidAddress } if txOut.Amount == 0 { return ErrInvalidAmount } - return nil + return ResultOK } func (txOut TxOutput) SignBytes() []byte {