diff --git a/app/app.go b/app/app.go index a086cf9dc..af21daa62 100644 --- a/app/app.go +++ b/app/app.go @@ -16,13 +16,13 @@ const ( version = "0.1" maxTxSize = 10240 - typeByteBase = 0x01 - typeByteEyes = 0x02 - typeByteGov = 0x03 + PluginTypeByteBase = 0x01 + PluginTypeByteEyes = 0x02 + PluginTypeByteGov = 0x03 - pluginNameBase = "base" - pluginNameEyes = "eyes" - pluginNameGov = "gov" + PluginNameBase = "base" + PluginNameEyes = "eyes" + PluginNameGov = "gov" ) type Basecoin struct { @@ -36,7 +36,7 @@ func NewBasecoin(eyesCli *eyes.Client) *Basecoin { govMint := gov.NewGovernmint(eyesCli) state_ := state.NewState(eyesCli) plugins := types.NewPlugins() - plugins.RegisterPlugin(typeByteGov, pluginNameGov, govMint) // TODO: make constants + plugins.RegisterPlugin(PluginTypeByteGov, PluginNameGov, govMint) return &Basecoin{ eyesCli: eyesCli, govMint: govMint, @@ -52,12 +52,12 @@ func (app *Basecoin) Info() string { // TMSP::SetOption func (app *Basecoin) SetOption(key string, value string) (log string) { - pluginName, key := splitKey(key) - if pluginName != pluginNameBase { + PluginName, key := splitKey(key) + if PluginName != PluginNameBase { // Set option on plugin - plugin := app.plugins.GetByName(pluginName) + plugin := app.plugins.GetByName(PluginName) if plugin == nil { - return "Invalid plugin name: " + pluginName + return "Invalid plugin name: " + PluginName } return plugin.SetOption(key, value) } else { @@ -126,11 +126,11 @@ func (app *Basecoin) Query(query []byte) (res tmsp.Result) { typeByte := query[0] query = query[1:] switch typeByte { - case typeByteBase: + case PluginTypeByteBase: return tmsp.OK.SetLog("This type of query not yet supported") - case typeByteEyes: + case PluginTypeByteEyes: return app.eyesCli.QuerySync(query) - case typeByteGov: + case PluginTypeByteGov: return app.govMint.Query(query) } return tmsp.ErrBaseUnknownPlugin.SetLog( @@ -156,20 +156,35 @@ func (app *Basecoin) Commit() (res tmsp.Result) { // TMSP::InitChain func (app *Basecoin) InitChain(validators []*tmsp.Validator) { - app.govMint.InitChain(validators) + for _, plugin := range app.plugins.GetList() { + if _, ok := plugin.Plugin.(tmsp.BlockchainAware); ok { + plugin.Plugin.(tmsp.BlockchainAware).InitChain(validators) + } + } } // TMSP::BeginBlock func (app *Basecoin) BeginBlock(height uint64) { - // app.govMint.BeginBlock(height) - // TODO other plugins? + app.state.ResetCacheState() + for _, plugin := range app.plugins.GetList() { + if _, ok := plugin.Plugin.(tmsp.BlockchainAware); ok { + plugin.Plugin.(tmsp.BlockchainAware).BeginBlock(height) + } + } } // TMSP::EndBlock -func (app *Basecoin) EndBlock(height uint64) []*tmsp.Validator { - app.state.ResetCacheState() - return app.govMint.EndBlock(height) - // TODO other plugins? +func (app *Basecoin) EndBlock(height uint64) (vals []*tmsp.Validator) { + for _, plugin := range app.plugins.GetList() { + if plugin.Plugin == app.govMint { + vals = plugin.Plugin.(tmsp.BlockchainAware).EndBlock(height) + } else { + if _, ok := plugin.Plugin.(tmsp.BlockchainAware); ok { + plugin.Plugin.(tmsp.BlockchainAware).EndBlock(height) + } + } + } + return } //---------------------------------------- diff --git a/state/.execution.go.swo b/state/.execution.go.swo new file mode 100644 index 000000000..dc5b7ac8e Binary files /dev/null and b/state/.execution.go.swo differ diff --git a/state/execution.go b/state/execution.go index f8f4227af..40e113f5d 100644 --- a/state/execution.go +++ b/state/execution.go @@ -1,8 +1,6 @@ package state import ( - "bytes" - "github.com/tendermint/basecoin/types" . "github.com/tendermint/go-common" "github.com/tendermint/go-events" @@ -28,28 +26,35 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event // Exec tx switch tx := tx.(type) { case *types.SendTx: - // First, get inputs + // Validate inputs and outputs, basic + res := validateInputsBasic(tx.Inputs) + if res.IsErr() { + return res.PrependLog("in validateInputsBasic()") + } + res = validateOutputsBasic(tx.Outputs) + if res.IsErr() { + return res.PrependLog("in validateOutputsBasic()") + } + + // Get inputs accounts, res := getInputs(state, tx.Inputs) if res.IsErr() { return res.PrependLog("in getInputs()") } - // Then, get or make outputs. + // Get or make outputs. accounts, res = getOrMakeOutputs(state, accounts, tx.Outputs) if res.IsErr() { return res.PrependLog("in getOrMakeOutputs()") } - // Validate inputs and outputs + // Validate inputs and outputs, advanced signBytes := tx.SignBytes(chainID) - inTotal, res := validateInputs(accounts, signBytes, tx.Inputs) + inTotal, res := validateInputsAdvanced(accounts, signBytes, tx.Inputs) if res.IsErr() { - return res.PrependLog("in validateInputs()") - } - outTotal, res := validateOutputs(tx.Outputs) - if res.IsErr() { - return res.PrependLog("in validateOutputs()") + return res.PrependLog("in validateInputsAdvanced()") } + outTotal := sumOutputs(tx.Outputs) if !inTotal.IsEqual(outTotal.Plus(types.Coins{{"", tx.Fee}})) { return tmsp.ErrBaseInvalidOutput.AppendLog("Input total != output total + fees") } @@ -78,23 +83,27 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event return tmsp.OK case *types.AppTx: - // First, get input account + // Validate input, basic + res := tx.Input.ValidateBasic() + if res.IsErr() { + return res + } + + // Get input account inAcc := state.GetAccount(tx.Input.Address) if inAcc == nil { return tmsp.ErrBaseUnknownAddress } - - // Validate input - // pubKey should be present in either "inAcc" or "tx.Input" - if res := checkInputPubKey(tx.Input.Address, inAcc, tx.Input); res.IsErr() { - log.Info(Fmt("Can't find pubkey for %X", tx.Input.Address)) - return res + if tx.Input.PubKey != nil { + inAcc.PubKey = tx.Input.PubKey } + + // Validate input, advanced signBytes := tx.SignBytes(chainID) - res := validateInput(inAcc, signBytes, tx.Input) + res = validateInputAdvanced(inAcc, signBytes, tx.Input) if res.IsErr() { - log.Info(Fmt("validateInput failed on %X: %v", tx.Input.Address, res)) - return res.PrependLog("in validateInput()") + log.Info(Fmt("validateInputAdvanced failed on %X: %v", tx.Input.Address, res)) + return res.PrependLog("in validateInputAdvanced()") } if !tx.Input.Coins.IsGTE(types.Coins{{"", tx.Fee}}) { log.Info(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) @@ -160,8 +169,7 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event // 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. +// or it must be specified in the TxInput. func getInputs(state types.AccountGetter, ins []types.TxInput) (map[string]*types.Account, tmsp.Result) { accounts := map[string]*types.Account{} for _, in := range ins { @@ -173,9 +181,8 @@ func getInputs(state types.AccountGetter, ins []types.TxInput) (map[string]*type if acc == nil { return nil, tmsp.ErrBaseUnknownAddress } - // PubKey should be present in either "account" or "in" - if res := checkInputPubKey(in.Address, acc, in); res.IsErr() { - return nil, res + if in.PubKey != nil { + acc.PubKey = in.PubKey } accounts[string(in.Address)] = acc } @@ -205,37 +212,25 @@ func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Acco return accounts, tmsp.OK } -// 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) tmsp.Result { - if acc.PubKey == nil { - if in.PubKey == nil { - return tmsp.ErrBaseUnknownPubKey.AppendLog("PubKey not present in either acc or input") - } - if !bytes.Equal(in.PubKey.Address(), address) { - return tmsp.ErrBaseInvalidPubKey.AppendLog("Input PubKey address does not match address") - } - acc.PubKey = in.PubKey - } else { - if in.PubKey != nil { - // NOTE: allow redundant pubkey. - if !bytes.Equal(in.PubKey.Address(), address) { - return tmsp.ErrBaseInvalidPubKey.AppendLog("Input PubKey address does not match address") - } +// Validate inputs basic structure +func validateInputsBasic(ins []types.TxInput) (res tmsp.Result) { + for _, in := range ins { + // Check TxInput basic + if res := in.ValidateBasic(); res.IsErr() { + return res } } return tmsp.OK } // Validate inputs and compute total amount of coins -func validateInputs(accounts map[string]*types.Account, signBytes []byte, ins []types.TxInput) (total types.Coins, res tmsp.Result) { - +func validateInputsAdvanced(accounts map[string]*types.Account, signBytes []byte, ins []types.TxInput) (total types.Coins, res tmsp.Result) { for _, in := range ins { acc := accounts[string(in.Address)] if acc == nil { - PanicSanity("validateInputs() expects account in accounts") + PanicSanity("validateInputsAdvanced() expects account in accounts") } - res = validateInput(acc, signBytes, in) + res = validateInputAdvanced(acc, signBytes, in) if res.IsErr() { return } @@ -245,11 +240,7 @@ func validateInputs(accounts map[string]*types.Account, signBytes []byte, ins [] return total, tmsp.OK } -func validateInput(acc *types.Account, signBytes []byte, in types.TxInput) (res tmsp.Result) { - // Check TxInput basic - if res := in.ValidateBasic(); res.IsErr() { - return res - } +func validateInputAdvanced(acc *types.Account, signBytes []byte, in types.TxInput) (res tmsp.Result) { // Check sequence/coins seq, balance := acc.Sequence, acc.Balance if seq+1 != in.Sequence { @@ -266,16 +257,21 @@ func validateInput(acc *types.Account, signBytes []byte, in types.TxInput) (res return tmsp.OK } -func validateOutputs(outs []types.TxOutput) (total types.Coins, res tmsp.Result) { +func validateOutputsBasic(outs []types.TxOutput) (res tmsp.Result) { for _, out := range outs { // Check TxOutput basic if res := out.ValidateBasic(); res.IsErr() { - return nil, res + return res } - // Good. Add amount to total + } + return tmsp.OK +} + +func sumOutputs(outs []types.TxOutput) (total types.Coins) { + for _, out := range outs { total = total.Plus(out.Coins) } - return total, tmsp.OK + return total } func adjustByInputs(state types.AccountSetter, accounts map[string]*types.Account, ins []types.TxInput) { diff --git a/tests/tmsp/main.go b/tests/tmsp/main.go index 1bbfd07f6..6a6c0d67c 100644 --- a/tests/tmsp/main.go +++ b/tests/tmsp/main.go @@ -23,17 +23,18 @@ func main() { func testSendTx() { eyesCli := eyescli.NewLocalClient() + chainID := "test_chain_id" bcApp := app.NewBasecoin(eyesCli) + bcApp.SetOption("base/chainID", chainID) fmt.Println(bcApp.Info()) - tPriv := tests.PrivAccountFromSecret("test") - tPriv2 := tests.PrivAccountFromSecret("test2") + test1PrivAcc := tests.PrivAccountFromSecret("test1") + test2PrivAcc := tests.PrivAccountFromSecret("test2") // Seed Basecoin with account - tAcc := tPriv.Account - tAcc.Balance = types.Coins{{"", 1000}} - fmt.Println(bcApp.SetOption("base/chainID", "test_chain_id")) - fmt.Println(bcApp.SetOption("base/account", string(wire.JSONBytes(tAcc)))) + test1Acc := test1PrivAcc.Account + test1Acc.Balance = types.Coins{{"", 1000}} + fmt.Println(bcApp.SetOption("base/account", string(wire.JSONBytes(test1Acc)))) // Construct a SendTx signature tx := &types.SendTx{ @@ -41,24 +42,24 @@ func testSendTx() { Gas: 0, Inputs: []types.TxInput{ types.TxInput{ - Address: tPriv.Account.PubKey.Address(), - PubKey: tPriv.Account.PubKey, // TODO is this needed? + Address: test1PrivAcc.Account.PubKey.Address(), + PubKey: test1PrivAcc.Account.PubKey, // TODO is this needed? Coins: types.Coins{{"", 1}}, Sequence: 1, }, }, Outputs: []types.TxOutput{ types.TxOutput{ - Address: tPriv2.Account.PubKey.Address(), + Address: test2PrivAcc.Account.PubKey.Address(), Coins: types.Coins{{"", 1}}, }, }, } // Sign request - signBytes := tx.SignBytes("test_chain_id") + signBytes := tx.SignBytes(chainID) fmt.Printf("Sign bytes: %X\n", signBytes) - sig := tPriv.PrivKey.Sign(signBytes) + sig := test1PrivAcc.PrivKey.Sign(signBytes) tx.Inputs[0].Signature = sig //fmt.Println("tx:", tx) fmt.Printf("Signed TX bytes: %X\n", wire.BinaryBytes(struct{ types.Tx }{tx})) @@ -74,19 +75,21 @@ func testSendTx() { func testGov() { eyesCli := eyescli.NewLocalClient() + chainID := "test_chain_id" bcApp := app.NewBasecoin(eyesCli) + bcApp.SetOption("base/chainID", chainID) fmt.Println(bcApp.Info()) - tPriv := tests.PrivAccountFromSecret("test") - valPrivKey0 := crypto.GenPrivKeyEd25519FromSecret([]byte("val0")) - valPrivKey1 := crypto.GenPrivKeyEd25519FromSecret([]byte("val1")) - valPrivKey2 := crypto.GenPrivKeyEd25519FromSecret([]byte("val2")) + adminPrivAcc := tests.PrivAccountFromSecret("admin") + val0PrivKey := crypto.GenPrivKeyEd25519FromSecret([]byte("val0")) + val1PrivKey := crypto.GenPrivKeyEd25519FromSecret([]byte("val1")) + val2PrivKey := crypto.GenPrivKeyEd25519FromSecret([]byte("val2")) // Seed Basecoin with admin using PrivAccount - tAcc := tPriv.Account + adminAcc := adminPrivAcc.Account adminEntity := govtypes.Entity{ - Addr: tAcc.PubKey.Address(), - PubKey: tAcc.PubKey, + Addr: adminAcc.PubKey.Address(), + PubKey: adminAcc.PubKey, } log := bcApp.SetOption("gov/admin", string(wire.JSONBytes(adminEntity))) if log != "Success" { @@ -95,15 +98,15 @@ func testGov() { // Call InitChain to initialize the validator set bcApp.InitChain([]*tmsp.Validator{ - {PubKey: valPrivKey0.PubKey().Bytes(), Power: 1}, - {PubKey: valPrivKey1.PubKey().Bytes(), Power: 1}, - {PubKey: valPrivKey2.PubKey().Bytes(), Power: 1}, + {PubKey: val0PrivKey.PubKey().Bytes(), Power: 1}, + {PubKey: val1PrivKey.PubKey().Bytes(), Power: 1}, + {PubKey: val2PrivKey.PubKey().Bytes(), Power: 1}, }) // Query for validator set res := bcApp.Query(expr.MustCompile(`x02 x01 "gov/g/validators"`)) if res.IsErr() { - Exit(Fmt("Failed: %v", res.Error())) + Exit(Fmt("Failed to query validators: %v", res.Error())) } group := govtypes.Group{} err := wire.ReadBinaryBytes(res.Data, &group) @@ -111,22 +114,60 @@ func testGov() { Exit(Fmt("Unexpected query response bytes: %X error: %v", res.Data, err)) } - fmt.Println(">>", group) + // fmt.Println("Initialized gov/g/validators", group) + + // Mutate the validator set. + proposal := govtypes.Proposal{ + ID: "my_proposal_id", + VoteGroupID: "gov/admin", + StartHeight: 0, + EndHeight: 0, + Info: &govtypes.GroupUpdateProposalInfo{ + UpdateGroupID: "gov/g/validators", + NextVersion: 0, + ChangedMembers: []govtypes.Member{ + {nil, 1}, // TODO Fill this out. + }, + }, + } + proposalTx := &govtypes.ProposalTx{ + EntityAddr: adminEntity.Addr, + Proposal: proposal, + } + proposalTx.SetSignature(nil, adminPrivAcc.Sign(proposalTx.SignBytes())) + tx := &types.AppTx{ + Fee: 1, + Gas: 1, + Type: app.PluginTypeByteGov, // XXX Remove typebytes? + Input: types.TxInput{ + Address: adminEntity.Addr, + Coins: types.Coins{{"", 1}}, + Sequence: 1, + }, + Data: nil, + } + tx.SetSignature(nil, adminPrivAcc.Sign(tx.SignBytes(chainID))) + res = bcApp.AppendTx(wire.BinaryBytes(struct{ types.Tx }{tx})) + if res.IsErr() { + Exit(Fmt("Failed to mutate validators: %v", res.Error())) + } + fmt.Println(res) // TODO more tests... } func testSequence() { eyesCli := eyescli.NewLocalClient() - bcApp := app.NewBasecoin(eyesCli) chainID := "test_chain_id" + bcApp := app.NewBasecoin(eyesCli) + bcApp.SetOption("base/chainID", chainID) + fmt.Println(bcApp.Info()) - // Get the root account - root := tests.PrivAccountFromSecret("test") - rootAcc := root.Account - rootAcc.Balance = types.Coins{{"", 1 << 53}} - fmt.Println(bcApp.SetOption("base/chainID", "test_chain_id")) - fmt.Println(bcApp.SetOption("base/account", string(wire.JSONBytes(rootAcc)))) + // Get the test account + test1PrivAcc := tests.PrivAccountFromSecret("test1") + test1Acc := test1PrivAcc.Account + test1Acc.Balance = types.Coins{{"", 1 << 53}} + fmt.Println(bcApp.SetOption("base/account", string(wire.JSONBytes(test1Acc)))) sequence := int(1) // Make a bunch of PrivAccounts @@ -141,8 +182,8 @@ func testSequence() { Gas: 2, Inputs: []types.TxInput{ types.TxInput{ - Address: root.Account.PubKey.Address(), - PubKey: root.Account.PubKey, // TODO is this needed? + Address: test1Acc.PubKey.Address(), + PubKey: test1Acc.PubKey, // TODO is this needed? Coins: types.Coins{{"", 1000002}}, Sequence: sequence, }, @@ -158,7 +199,7 @@ func testSequence() { // Sign request signBytes := tx.SignBytes(chainID) - sig := root.PrivKey.Sign(signBytes) + sig := test1PrivAcc.PrivKey.Sign(signBytes) tx.Inputs[0].Signature = sig // fmt.Printf("ADDR: %X -> %X\n", tx.Inputs[0].Address, tx.Outputs[0].Address) diff --git a/types/tx.go b/types/tx.go index 8e6577432..0e9521a12 100644 --- a/types/tx.go +++ b/types/tx.go @@ -45,7 +45,7 @@ type TxInput struct { Coins Coins `json:"coins"` // Sequence int `json:"sequence"` // Must be 1 greater than the last committed TxInput Signature crypto.Signature `json:"signature"` // Depends on the PubKey type and the whole Tx - PubKey crypto.PubKey `json:"pub_key"` // May be nil + PubKey crypto.PubKey `json:"pub_key"` // Is present iff Sequence == 0 } func (txIn TxInput) ValidateBasic() tmsp.Result { @@ -58,6 +58,15 @@ func (txIn TxInput) ValidateBasic() tmsp.Result { if txIn.Coins.IsZero() { return tmsp.ErrBaseInvalidInput.AppendLog("Coins cannot be zero") } + if txIn.Sequence <= 0 { + return tmsp.ErrBaseInvalidInput.AppendLog("Sequence must be greater than 0") + } + if txIn.Sequence == 1 && txIn.PubKey == nil { + return tmsp.ErrBaseInvalidInput.AppendLog("PubKey must be present when Sequence == 1") + } + if txIn.Sequence > 1 && txIn.PubKey != nil { + return tmsp.ErrBaseInvalidInput.AppendLog("PubKey must be nil when Sequence > 1") + } return tmsp.OK } @@ -112,6 +121,16 @@ func (tx *SendTx) SignBytes(chainID string) []byte { return signBytes } +func (tx *SendTx) SetSignature(pubKey crypto.PubKey, sig crypto.Signature) bool { + for i, input := range tx.Inputs { + if input.PubKey.Equals(pubKey) { + tx.Inputs[i].Signature = sig + return true + } + } + return false +} + func (tx *SendTx) String() string { return Fmt("SendTx{%v/%v %v->%v}", tx.Fee, tx.Gas, tx.Inputs, tx.Outputs) } @@ -135,6 +154,12 @@ func (tx *AppTx) SignBytes(chainID string) []byte { return signBytes } +func (tx *AppTx) SetSignature(pubKey crypto.PubKey, sig crypto.Signature) bool { + // TODO + tx.Input.Signature = sig + return true +} + func (tx *AppTx) String() string { return Fmt("AppTx{%v/%v %v %v %X}", tx.Fee, tx.Gas, tx.Type, tx.Input, tx.Data) }