diff --git a/state/execution.go b/state/execution.go index 529d4b63a..f8f4227af 100644 --- a/state/execution.go +++ b/state/execution.go @@ -13,7 +13,7 @@ import ( func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc events.Fireable) tmsp.Result { // TODO: do something with fees - fees := int64(0) + fees := types.Coins{} chainID := s.GetChainID() // Get the state. If isCheckTx, then we use a cache. @@ -50,11 +50,10 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event if res.IsErr() { return res.PrependLog("in validateOutputs()") } - if outTotal > inTotal { - return tmsp.ErrBaseInsufficientFunds + if !inTotal.IsEqual(outTotal.Plus(types.Coins{{"", tx.Fee}})) { + return tmsp.ErrBaseInvalidOutput.AppendLog("Input total != output total + fees") } - fee := inTotal - outTotal - fees += fee + fees = fees.Plus(types.Coins{{"", tx.Fee}}) // TODO: Fee validation for SendTx @@ -97,7 +96,7 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event log.Info(Fmt("validateInput failed on %X: %v", tx.Input.Address, res)) return res.PrependLog("in validateInput()") } - if tx.Input.Amount < tx.Fee { + 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)) return tmsp.ErrBaseInsufficientFunds } @@ -110,9 +109,9 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event } // Good! - value := tx.Input.Amount - tx.Fee + coins := tx.Input.Coins.Minus(types.Coins{{"", tx.Fee}}) inAcc.Sequence += 1 - inAcc.Balance -= tx.Input.Amount + inAcc.Balance = inAcc.Balance.Minus(tx.Input.Coins) // If this is a CheckTx, stop now. if isCheckTx { @@ -126,8 +125,7 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event // Run the tx. cache := types.NewAccountCache(state) cache.SetAccount(tx.Input.Address, inAcc) - gas := int64(1) // TODO - ctx := types.NewCallContext(cache, inAcc, value, &gas) + ctx := types.NewCallContext(cache, inAcc, coins) res = plugin.RunTx(ctx, tx.Data) if res.IsOK() { cache.Sync() @@ -145,9 +143,10 @@ func ExecTx(s *State, pgz *types.Plugins, tx types.Tx, isCheckTx bool, evc event */ } else { log.Info("AppTx failed", "error", res) - // Just return the value and return. - // TODO: return gas? - inAccCopy.Balance += value + // Just return the coins and return. + inAccCopy.Balance = inAccCopy.Balance.Plus(coins) + // But take the gas + // TODO state.SetAccount(tx.Input.Address, inAccCopy) } return res @@ -199,7 +198,6 @@ func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Acco acc = &types.Account{ PubKey: nil, Sequence: 0, - Balance: 0, } } accounts[string(out.Address)] = acc @@ -229,8 +227,8 @@ func checkInputPubKey(address []byte, acc *types.Account, in types.TxInput) tmsp return tmsp.OK } -// Validate inputs and compute total amount -func validateInputs(accounts map[string]*types.Account, signBytes []byte, ins []types.TxInput) (total int64, res tmsp.Result) { +// 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) { for _, in := range ins { acc := accounts[string(in.Address)] @@ -242,7 +240,7 @@ func validateInputs(accounts map[string]*types.Account, signBytes []byte, ins [] return } // Good. Add amount to total - total += in.Amount + total = total.Plus(in.Coins) } return total, tmsp.OK } @@ -252,13 +250,13 @@ func validateInput(acc *types.Account, signBytes []byte, in types.TxInput) (res if res := in.ValidateBasic(); res.IsErr() { return res } - // Check sequence/balance + // Check sequence/coins seq, balance := acc.Sequence, acc.Balance if seq+1 != in.Sequence { return tmsp.ErrBaseInvalidSequence.AppendLog(Fmt("Got %v, expected %v. (acc.seq=%v)", in.Sequence, seq+1, acc.Sequence)) } // Check amount - if balance < in.Amount { + if !balance.IsGTE(in.Coins) { return tmsp.ErrBaseInsufficientFunds } // Check signatures @@ -268,14 +266,14 @@ func validateInput(acc *types.Account, signBytes []byte, in types.TxInput) (res return tmsp.OK } -func validateOutputs(outs []types.TxOutput) (total int64, res tmsp.Result) { +func validateOutputs(outs []types.TxOutput) (total types.Coins, res tmsp.Result) { for _, out := range outs { // Check TxOutput basic if res := out.ValidateBasic(); res.IsErr() { - return 0, res + return nil, res } // Good. Add amount to total - total += out.Amount + total = total.Plus(out.Coins) } return total, tmsp.OK } @@ -286,10 +284,10 @@ func adjustByInputs(state types.AccountSetter, accounts map[string]*types.Accoun if acc == nil { PanicSanity("adjustByInputs() expects account in accounts") } - if acc.Balance < in.Amount { + if !acc.Balance.IsGTE(in.Coins) { PanicSanity("adjustByInputs() expects sufficient funds") } - acc.Balance -= in.Amount + acc.Balance = acc.Balance.Minus(in.Coins) acc.Sequence += 1 state.SetAccount(in.Address, acc) } @@ -301,7 +299,7 @@ func adjustByOutputs(state types.AccountSetter, accounts map[string]*types.Accou if acc == nil { PanicSanity("adjustByOutputs() expects account in accounts") } - acc.Balance += out.Amount + acc.Balance = acc.Balance.Plus(out.Coins) if !isCheckTx { state.SetAccount(out.Address, acc) } diff --git a/tests/common.go b/tests/common.go index 3f9e8e5e8..1a9f3aee3 100644 --- a/tests/common.go +++ b/tests/common.go @@ -15,7 +15,6 @@ func PrivAccountFromSecret(secret string) types.PrivAccount { Account: types.Account{ PubKey: privKey.PubKey(), Sequence: 0, - Balance: 0, }, } return privAccount @@ -38,7 +37,7 @@ func RandAccounts(num int, minAmount int64, maxAmount int64) []types.PrivAccount Account: types.Account{ PubKey: pubKey, Sequence: 0, - Balance: balance, + Balance: types.Coins{types.Coin{"", balance}}, }, } } diff --git a/tests/tendermint/main.go b/tests/tendermint/main.go index 0dbfedfa6..a221d4b00 100644 --- a/tests/tendermint/main.go +++ b/tests/tendermint/main.go @@ -51,14 +51,14 @@ func main() { types.TxInput{ Address: root.Account.PubKey.Address(), PubKey: root.Account.PubKey, // TODO is this needed? - Amount: 1000002, + Coins: types.Coins{{"", 1000002}}, Sequence: sequence, }, }, Outputs: []types.TxOutput{ types.TxOutput{ Address: privAccount.Account.PubKey.Address(), - Amount: 1000000, + Coins: types.Coins{{"", 1000000}}, }, }, } @@ -102,14 +102,14 @@ func main() { types.TxInput{ Address: privAccountA.Account.PubKey.Address(), PubKey: privAccountA.Account.PubKey, - Amount: 3, + Coins: types.Coins{{"", 3}}, Sequence: privAccountASequence + 1, }, }, Outputs: []types.TxOutput{ types.TxOutput{ Address: privAccountB.Account.PubKey.Address(), - Amount: 1, + Coins: types.Coins{{"", 1}}, }, }, } diff --git a/tests/tmsp/main.go b/tests/tmsp/main.go index c5d1e7254..e2072367d 100644 --- a/tests/tmsp/main.go +++ b/tests/tmsp/main.go @@ -28,24 +28,26 @@ func testSendTx() { // Seed Basecoin with account tAcc := tPriv.Account - tAcc.Balance = 1000 + tAcc.Balance = types.Coins{{"", 1000}} fmt.Println(bcApp.SetOption("base/chainID", "test_chain_id")) fmt.Println(bcApp.SetOption("base/account", string(wire.JSONBytes(tAcc)))) // Construct a SendTx signature tx := &types.SendTx{ + Fee: 0, + Gas: 0, Inputs: []types.TxInput{ types.TxInput{ Address: tPriv.Account.PubKey.Address(), PubKey: tPriv.Account.PubKey, // TODO is this needed? - Amount: 1, + Coins: types.Coins{{"", 1}}, Sequence: 1, }, }, Outputs: []types.TxOutput{ types.TxOutput{ Address: tPriv2.Account.PubKey.Address(), - Amount: 1, + Coins: types.Coins{{"", 1}}, }, }, } @@ -95,7 +97,7 @@ func testSequence() { // Get the root account root := tests.PrivAccountFromSecret("test") rootAcc := root.Account - rootAcc.Balance = 1 << 53 + 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)))) @@ -108,18 +110,20 @@ func testSequence() { for i := 0; i < len(privAccounts); i++ { privAccount := privAccounts[i] tx := &types.SendTx{ + Fee: 2, + Gas: 2, Inputs: []types.TxInput{ types.TxInput{ Address: root.Account.PubKey.Address(), PubKey: root.Account.PubKey, // TODO is this needed? - Amount: 1000002, + Coins: types.Coins{{"", 1000002}}, Sequence: sequence, }, }, Outputs: []types.TxOutput{ types.TxOutput{ Address: privAccount.Account.PubKey.Address(), - Amount: 1000000, + Coins: types.Coins{{"", 1000000}}, }, }, } @@ -155,18 +159,20 @@ func testSequence() { privAccountB := privAccounts[randB] tx := &types.SendTx{ + Fee: 2, + Gas: 2, Inputs: []types.TxInput{ types.TxInput{ Address: privAccountA.Account.PubKey.Address(), PubKey: privAccountA.Account.PubKey, - Amount: 3, + Coins: types.Coins{{"", 3}}, Sequence: privAccountASequence + 1, }, }, Outputs: []types.TxOutput{ types.TxOutput{ Address: privAccountB.Account.PubKey.Address(), - Amount: 1, + Coins: types.Coins{{"", 1}}, }, }, } diff --git a/types/account.go b/types/account.go index 3591407a5..d78f560a6 100644 --- a/types/account.go +++ b/types/account.go @@ -9,7 +9,7 @@ import ( type Account struct { PubKey crypto.PubKey `json:"pub_key"` // May be nil, if not known. Sequence int `json:"sequence"` - Balance int64 `json:"balance"` + Balance Coins `json:"coins"` } func (acc *Account) Copy() *Account { diff --git a/types/coin.go b/types/coin.go new file mode 100644 index 000000000..bc2bd806e --- /dev/null +++ b/types/coin.go @@ -0,0 +1,130 @@ +package types + +import ( + "fmt" + "strings" +) + +type Coin struct { + Denom string `json:"denom"` + Amount int64 `json:"amount"` +} + +func (coin Coin) String() string { + return fmt.Sprintf("(%v %v)", + coin.Denom, coin.Amount) +} + +//---------------------------------------- + +type Coins []Coin + +// Must be sorted, and not have 0 amounts +func (coins Coins) IsValid() bool { + switch len(coins) { + case 0: + return true + case 1: + return coins[0].Amount != 0 + default: + lowDenom := coins[0].Denom + for _, coin := range coins[1:] { + if coin.Denom <= lowDenom { + return false + } + if coin.Amount == 0 { + return false + } + } + return true + } +} + +func (coinsA Coins) Plus(coinsB Coins) Coins { + sum := []Coin{} + indexA, indexB := 0, 0 + lenA, lenB := len(coinsA), len(coinsB) + for { + if indexA == lenA { + if indexB == lenB { + return sum + } else { + return append(sum, coinsB[indexB:]...) + } + } else if indexB == lenB { + return append(sum, coinsA[indexA:]...) + } + coinA, coinB := coinsA[indexA], coinsB[indexB] + switch strings.Compare(coinA.Denom, coinB.Denom) { + case -1: + sum = append(sum, coinA) + indexA += 1 + case 0: + if coinA.Amount+coinB.Amount == 0 { + // ignore 0 sum coin type + } else { + sum = append(sum, Coin{ + Denom: coinA.Denom, + Amount: coinA.Amount + coinB.Amount, + }) + } + indexA += 1 + indexB += 1 + case 1: + sum = append(sum, coinB) + indexB += 1 + } + } + return sum +} + +func (coins Coins) Negative() Coins { + res := make([]Coin, 0, len(coins)) + for _, coin := range coins { + res = append(res, Coin{ + Denom: coin.Denom, + Amount: -coin.Amount, + }) + } + return res +} + +func (coinsA Coins) Minus(coinsB Coins) Coins { + return coinsA.Plus(coinsB.Negative()) +} + +func (coinsA Coins) IsGTE(coinsB Coins) bool { + diff := coinsA.Minus(coinsB) + if len(diff) == 0 { + return true + } + return diff.IsPositive() +} + +func (coins Coins) IsZero() bool { + return len(coins) == 0 +} + +func (coinsA Coins) IsEqual(coinsB Coins) bool { + if len(coinsA) != len(coinsB) { + return false + } + for i := 0; i < len(coinsA); i++ { + if coinsA[i] != coinsB[i] { + return false + } + } + return true +} + +func (coins Coins) IsPositive() bool { + if len(coins) == 0 { + return false + } + for _, coinAmount := range coins { + if coinAmount.Amount <= 0 { + return false + } + } + return true +} diff --git a/types/coin_test.go b/types/coin_test.go new file mode 100644 index 000000000..00a07a6f3 --- /dev/null +++ b/types/coin_test.go @@ -0,0 +1,67 @@ +package types + +import ( + "testing" +) + +func TestCoins(t *testing.T) { + coins := Coins{ + Coin{"GAS", 1}, + Coin{"MINERAL", 1}, + Coin{"TREE", 1}, + } + + if !coins.IsValid() { + t.Fatal("Coins are valid") + } + + if !coins.IsPositive() { + t.Fatalf("Expected coins to be positive: %v", coins) + } + + negCoins := coins.Negative() + if negCoins.IsPositive() { + t.Fatalf("Expected neg coins to not be positive: %v", negCoins) + } + + sumCoins := coins.Plus(negCoins) + if len(sumCoins) != 0 { + t.Fatal("Expected 0 coins") + } +} + +func TestCoinsBadSort(t *testing.T) { + coins := Coins{ + Coin{"TREE", 1}, + Coin{"GAS", 1}, + Coin{"MINERAL", 1}, + } + + if coins.IsValid() { + t.Fatal("Coins are not sorted") + } +} + +func TestCoinsBadAmount(t *testing.T) { + coins := Coins{ + Coin{"GAS", 1}, + Coin{"TREE", 0}, + Coin{"MINERAL", 1}, + } + + if coins.IsValid() { + t.Fatal("Coins cannot include 0 amounts") + } +} + +func TestCoinsDuplicate(t *testing.T) { + coins := Coins{ + Coin{"GAS", 1}, + Coin{"GAS", 1}, + Coin{"MINERAL", 1}, + } + + if coins.IsValid() { + t.Fatal("Duplicate coin") + } +} diff --git a/types/plugin.go b/types/plugin.go index 3ab6f176b..35f2239f7 100644 --- a/types/plugin.go +++ b/types/plugin.go @@ -5,8 +5,6 @@ import ( ) // Value is any floating value. It must be given to someone. -// Gas is a pointer to remainig gas. Decrement as you go, -// if any gas is left the user is type Plugin interface { SetOption(key string, value string) (log string) RunTx(ctx CallContext, txBytes []byte) (res tmsp.Result) @@ -25,16 +23,14 @@ type NamedPlugin struct { type CallContext struct { Cache AccountCacher Caller *Account - Value int64 - Gas *int64 + Coins Coins } -func NewCallContext(cache AccountCacher, caller *Account, value int64, gas *int64) CallContext { +func NewCallContext(cache AccountCacher, caller *Account, coins Coins) CallContext { return CallContext{ Cache: cache, Caller: caller, - Value: value, - Gas: gas, + Coins: coins, } } diff --git a/types/tx.go b/types/tx.go index 774f7d465..8e6577432 100644 --- a/types/tx.go +++ b/types/tx.go @@ -42,7 +42,7 @@ var _ = wire.RegisterInterface( type TxInput struct { Address []byte `json:"address"` // Hash of the PubKey - Amount int64 `json:"amount"` // Must not exceed account balance + 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 @@ -50,42 +50,50 @@ type TxInput struct { func (txIn TxInput) ValidateBasic() tmsp.Result { if len(txIn.Address) != 20 { - return tmsp.ErrBaseInvalidAddress.AppendLog("(in TxInput)") + return tmsp.ErrBaseInvalidInput.AppendLog("Invalid address length") } - if txIn.Amount == 0 { - return tmsp.ErrBaseInvalidAmount.AppendLog("(in TxInput)") + if !txIn.Coins.IsValid() { + return tmsp.ErrBaseInvalidInput.AppendLog(Fmt("Invalid coins %v", txIn.Coins)) + } + if txIn.Coins.IsZero() { + return tmsp.ErrBaseInvalidInput.AppendLog("Coins cannot be zero") } return tmsp.OK } func (txIn TxInput) String() string { - return Fmt("TxInput{%X,%v,%v,%v,%v}", txIn.Address, txIn.Amount, txIn.Sequence, txIn.Signature, txIn.PubKey) + return Fmt("TxInput{%X,%v,%v,%v,%v}", txIn.Address, txIn.Coins, txIn.Sequence, txIn.Signature, txIn.PubKey) } //----------------------------------------------------------------------------- type TxOutput struct { Address []byte `json:"address"` // Hash of the PubKey - Amount int64 `json:"amount"` // The sum of all outputs must not exceed the inputs. + Coins Coins `json:"coins"` // } func (txOut TxOutput) ValidateBasic() tmsp.Result { if len(txOut.Address) != 20 { - return tmsp.ErrBaseInvalidAddress.AppendLog("(in TxOutput)") + return tmsp.ErrBaseInvalidOutput.AppendLog("Invalid address length") } - if txOut.Amount == 0 { - return tmsp.ErrBaseInvalidAmount.AppendLog("(in TxOutput)") + if !txOut.Coins.IsValid() { + return tmsp.ErrBaseInvalidOutput.AppendLog(Fmt("Invalid coins %v", txOut.Coins)) + } + if txOut.Coins.IsZero() { + return tmsp.ErrBaseInvalidOutput.AppendLog("Coins cannot be zero") } return tmsp.OK } func (txOut TxOutput) String() string { - return Fmt("TxOutput{%X,%v}", txOut.Address, txOut.Amount) + return Fmt("TxOutput{%X,%v}", txOut.Address, txOut.Coins) } //----------------------------------------------------------------------------- type SendTx struct { + Fee int64 `json:"fee"` // Fee + Gas int64 `json:"gas"` // Gas Inputs []TxInput `json:"inputs"` Outputs []TxOutput `json:"outputs"` } @@ -105,16 +113,16 @@ func (tx *SendTx) SignBytes(chainID string) []byte { } func (tx *SendTx) String() string { - return Fmt("SendTx{%v -> %v}", tx.Inputs, tx.Outputs) + return Fmt("SendTx{%v/%v %v->%v}", tx.Fee, tx.Gas, tx.Inputs, tx.Outputs) } //----------------------------------------------------------------------------- type AppTx struct { - Type byte `json:"type"` // Which app - Gas int64 `json:"gas"` - Fee int64 `json:"fee"` - Input TxInput `json:"input"` + Fee int64 `json:"fee"` // Fee + Gas int64 `json:"gas"` // Gas + Type byte `json:"type"` // Which app + Input TxInput `json:"input"` // Hmmm do we want coins? Data []byte `json:"data"` } @@ -128,7 +136,7 @@ func (tx *AppTx) SignBytes(chainID string) []byte { } func (tx *AppTx) String() string { - return Fmt("AppTx{%v %v %v %v -> %X}", tx.Type, tx.Gas, tx.Fee, tx.Input, tx.Data) + return Fmt("AppTx{%v/%v %v %v %X}", tx.Fee, tx.Gas, tx.Type, tx.Input, tx.Data) } //----------------------------------------------------------------------------- diff --git a/types/tx_test.go b/types/tx_test.go index 49530424f..92d54883c 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -10,32 +10,34 @@ var chainID string = "test_chain" func TestSendTxSignable(t *testing.T) { sendTx := &SendTx{ + Fee: 111, + Gas: 222, Inputs: []TxInput{ TxInput{ Address: []byte("input1"), - Amount: 12345, + Coins: Coins{{"", 12345}}, Sequence: 67890, }, TxInput{ Address: []byte("input2"), - Amount: 111, + Coins: Coins{{"", 111}}, Sequence: 222, }, }, Outputs: []TxOutput{ TxOutput{ Address: []byte("output1"), - Amount: 333, + Coins: Coins{{"", 333}}, }, TxOutput{ Address: []byte("output2"), - Amount: 444, + Coins: Coins{{"", 444}}, }, }, } signBytes := sendTx.SignBytes(chainID) signBytesHex := Fmt("%X", signBytes) - expected := "010A746573745F636861696E0101020106696E7075743100000000000030390301093200000106696E70757432000000000000006F01DE0000010201076F757470757431000000000000014D01076F75747075743200000000000001BC" + expected := "010A746573745F636861696E01000000000000006F00000000000000DE01020106696E7075743101010000000000000030390301093200000106696E70757432010100000000000000006F01DE0000010201076F757470757431010100000000000000014D01076F75747075743201010000000000000001BC" if signBytesHex != expected { t.Errorf("Got unexpected sign string for SendTx. Expected:\n%v\nGot:\n%v", expected, signBytesHex) } @@ -43,19 +45,19 @@ func TestSendTxSignable(t *testing.T) { func TestAppTxSignable(t *testing.T) { callTx := &AppTx{ + Fee: 111, + Gas: 222, Type: 0x01, - Gas: 111, - Fee: 222, Input: TxInput{ Address: []byte("input1"), - Amount: 12345, + Coins: Coins{{"", 12345}}, Sequence: 67890, }, Data: []byte("data1"), } signBytes := callTx.SignBytes(chainID) signBytesHex := Fmt("%X", signBytes) - expected := "010A746573745F636861696E0101000000000000006F00000000000000DE0106696E70757431000000000000303903010932000001056461746131" + expected := "010A746573745F636861696E01000000000000006F00000000000000DE010106696E70757431010100000000000000303903010932000001056461746131" if signBytesHex != expected { t.Errorf("Got unexpected sign string for AppTx. Expected:\n%v\nGot:\n%v", expected, signBytesHex) }