From afb7de58ddbfc9ba9a44eaaa34d81fd78653db8b Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 8 Jul 2018 23:09:07 +0100 Subject: [PATCH 01/30] initial sketch --- cmd/kvcli/main.go | 3 +- internal/x/paychan/README.md | 15 +++ internal/x/paychan/client/cmd/cmd.go | 68 +++++++++++++ .../x/paychan/client/rest/channel-watcher.go | 7 ++ internal/x/paychan/client/rest/rest.go | 22 +++++ internal/x/paychan/handler.go | 33 +++++++ internal/x/paychan/keeper.go | 62 ++++++++++++ internal/x/paychan/keeper_test.go | 93 ++++++++++++++++++ internal/x/paychan/types.go | 98 +++++++++++++++++++ 9 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 internal/x/paychan/README.md create mode 100644 internal/x/paychan/client/cmd/cmd.go create mode 100644 internal/x/paychan/client/rest/channel-watcher.go create mode 100644 internal/x/paychan/client/rest/rest.go create mode 100644 internal/x/paychan/handler.go create mode 100644 internal/x/paychan/keeper.go create mode 100644 internal/x/paychan/keeper_test.go create mode 100644 internal/x/paychan/types.go diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 48c0876b..90aa90e8 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -55,8 +55,9 @@ func main() { )...) rootCmd.AddCommand( - client.PostCommands( + client.PostCommands( // this just wraps the input cmds with common flags bankcmd.SendTxCmd(cdc), + // paychan commands... //ibccmd.IBCTransferCmd(cdc), //ibccmd.IBCRelayCmd(cdc), //stakecmd.GetCmdCreateValidator(cdc), diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md new file mode 100644 index 00000000..cad4cfea --- /dev/null +++ b/internal/x/paychan/README.md @@ -0,0 +1,15 @@ +Paychan Sketch + +Simplifications: + + - unidirectional paychans + - no top ups or partial withdrawals (only opening and closing) + - no protection against fund lock up from dissapearing receiver + + + TODO + - fill in details + - add tests + - is errors.go needed? + - is wire.go needed? + - remove simplifications \ No newline at end of file diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go new file mode 100644 index 00000000..168be42d --- /dev/null +++ b/internal/x/paychan/client/cmd/cmd.go @@ -0,0 +1,68 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + //"github.com/cosmos/cosmos-sdk/client/context" + //sdk "github.com/cosmos/cosmos-sdk/types" + //"github.com/cosmos/cosmos-sdk/wire" + //"github.com/cosmos/cosmos-sdk/x/auth" +) + +// list of functions that return pointers to cobra commands +// No local storage needed for cli acting as a sender + +// create paychan +// close paychan +// get paychan(s) +// send paychan payment +// get balance from receiver + +// example from x/auth +/* +func GetAccountCmd(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder) *cobra.Command { + return &cobra.Command{ + Use: "account [address]", + Short: "Query account balance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + // find the key to look up the account + addr := args[0] + + key, err := sdk.GetAccAddressBech32(addr) + if err != nil { + return err + } + + // perform query + ctx := context.NewCoreContextFromViper() + res, err := ctx.Query(auth.AddressStoreKey(key), storeName) + if err != nil { + return err + } + + // Check if account was found + if res == nil { + return sdk.ErrUnknownAddress("No account with address " + addr + + " was found in the state.\nAre you sure there has been a transaction involving it?") + } + + // decode the value + account, err := decoder(res) + if err != nil { + return err + } + + // print out whole account + output, err := wire.MarshalJSONIndent(cdc, account) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } +} +*/ diff --git a/internal/x/paychan/client/rest/channel-watcher.go b/internal/x/paychan/client/rest/channel-watcher.go new file mode 100644 index 00000000..d00c7f51 --- /dev/null +++ b/internal/x/paychan/client/rest/channel-watcher.go @@ -0,0 +1,7 @@ +package lcd + +import () + +// implement thing that polls blockchain and handles paychan disputes +// needs plugged into LCD - add a "background processes" slot in the LCD run function? +// eventually LCD evolves into paychan (network) daemon diff --git a/internal/x/paychan/client/rest/rest.go b/internal/x/paychan/client/rest/rest.go new file mode 100644 index 00000000..3565d70a --- /dev/null +++ b/internal/x/paychan/client/rest/rest.go @@ -0,0 +1,22 @@ +package rest + +import ( + "github.com/gorilla/mux" + //"github.com/tendermint/go-crypto/keys" + //"github.com/cosmos/cosmos-sdk/client/context" + //"github.com/cosmos/cosmos-sdk/wire" +) + +// RegisterRoutes registers paychan-related REST handlers to a router +func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { + //r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, ctx)).Methods("POST") +} + +// handler functions ... +// create paychan +// close paychan +// get paychan(s) +// send paychan payment +// get balance from receiver +// get balance from local storage +// handle incoming payment diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go new file mode 100644 index 00000000..e25c68f9 --- /dev/null +++ b/internal/x/paychan/handler.go @@ -0,0 +1,33 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "reflect" +) + +// NewHandler returns a handler for "paychan" type messages. +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MsgSend: + return handleMsgSend(ctx, k, msg) + case MsgIssue: + return handleMsgIssue(ctx, k, msg) + default: + errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// Handle CreateMsg. +func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgSend) sdk.Result { + // k.CreatePaychan(args...) + // handle erros +} + +// Handle CloseMsg. +func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgIssue) sdk.Result { + // k.ClosePaychan(args...) + // handle errors +} diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go new file mode 100644 index 00000000..058ec628 --- /dev/null +++ b/internal/x/paychan/keeper.go @@ -0,0 +1,62 @@ +package paychan + +// keeper of the paychan store +type Keeper struct { + storeKey sdk.StoreKey + cdc *wire.Codec // needed? + coinKeeper bank.Keeper + + // codespace + codespace sdk.CodespaceType // ?? +} + +func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { + keeper := Keeper{ + storeKey: key, + cdc: cdc, + coinKeeper: ck, + codespace: codespace, + } + return keeper +} + +// bunch of business logic ... + +func (keeper Keeper) GetPaychan(paychanID) Paychan { + // load from DB + // unmarshall + // return +} + +func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { + // marshal + // write to db +} + +func (keeer Keeper) CreatePaychan(receiver sdkAddress, amt sdk.Coins) (Paychan, sdk.Error) { + // subtract coins from sender + // create new Paychan struct (create ID) + // save to db + + // validation: + // sender has enough coins + // receiver address exists? + // paychan doesn't exist already +} + +func (keeper Keeper) ClosePaychan() sdk.Error { + // add coins to sender + // add coins to receiver + // delete paychan from db + + // validation: + // paychan exists + // output coins are less than paychan balance + // sender and receiver addresses exist? +} + +func paychanKey(Paychan) { + // concat sender and receiver and integer ID +} + +// maybe getAllPaychans(sender sdk.address) []Paychan diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go new file mode 100644 index 00000000..344c3148 --- /dev/null +++ b/internal/x/paychan/keeper_test.go @@ -0,0 +1,93 @@ +package paychan + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// example from x/bank +func TestKeeper(t *testing.T) { + // ms, authKey := setupMultiStore() + + // cdc := wire.NewCodec() + // auth.RegisterBaseAccount(cdc) + + // ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) + // accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) + // coinKeeper := NewKeeper(accountMapper) + + // addr := sdk.Address([]byte("addr1")) + // addr2 := sdk.Address([]byte("addr2")) + // addr3 := sdk.Address([]byte("addr3")) + // acc := accountMapper.NewAccountWithAddress(ctx, addr) + + // // Test GetCoins/SetCoins + // accountMapper.SetAccount(ctx, acc) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{})) + + // coinKeeper.SetCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + + // // Test HasCoins + // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 5}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 15}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})) + + // // Test AddCoins + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"foocoin", 15}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 25}})) + + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 15}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 15}, {"foocoin", 25}})) + + // // Test SubtractCoins + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + + // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 15}})) + // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 1}})) + + // // Test SendCoins + // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + + // _, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}}) + // assert.Implements(t, (*sdk.Error)(nil), err2) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + + // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 30}}) + // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"barcoin", 10}, {"foocoin", 5}}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 5}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 10}})) + + // // Test InputOutputCoins + // input1 := NewInput(addr2, sdk.Coins{{"foocoin", 2}}) + // output1 := NewOutput(addr, sdk.Coins{{"foocoin", 2}}) + // coinKeeper.InputOutputCoins(ctx, []Input{input1}, []Output{output1}) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 7}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 8}})) + + // inputs := []Input{ + // NewInput(addr, sdk.Coins{{"foocoin", 3}}), + // NewInput(addr2, sdk.Coins{{"barcoin", 3}, {"foocoin", 2}}), + // } + + // outputs := []Output{ + // NewOutput(addr, sdk.Coins{{"barcoin", 1}}), + // NewOutput(addr3, sdk.Coins{{"barcoin", 2}, {"foocoin", 5}}), + // } + // coinKeeper.InputOutputCoins(ctx, inputs, outputs) + // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 21}, {"foocoin", 4}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 7}, {"foocoin", 6}})) + // assert.True(t, coinKeeper.GetCoins(ctx, addr3).IsEqual(sdk.Coins{{"barcoin", 2}, {"foocoin", 5}})) + +} diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go new file mode 100644 index 00000000..aec54ebc --- /dev/null +++ b/internal/x/paychan/types.go @@ -0,0 +1,98 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +//////// + +// probably want to convert this to a general purpose "state" +struct Paychan { + balance sdk.Coins + sender sdk.Address + receiver sdk.Address +} + + +///////////// + +// Message implement the sdk.Msg interface: + +// type Msg interface { + +// // Return the message type. +// // Must be alphanumeric or empty. +// Type() string + +// // Get the canonical byte representation of the Msg. +// GetSignBytes() []byte + +// // ValidateBasic does a simple validation check that +// // doesn't require access to any other information. +// ValidateBasic() Error + +// // Signers returns the addrs of signers that must sign. +// // CONTRACT: All signatures must be present to be valid. +// // CONTRACT: Returns addrs in some deterministic order. +// GetSigners() []Address +// } + +/////////////// CreatePayChan +// find a less confusing name +type CreateMsg struct { + // maybe just wrap a paychan struct + sender sdk.Address + receiver sdk.Address + amount sdk.Balance +} + +func (msg CreatMsg) NewCreateMsg() CreateMsg { + return CreateMsg{ } +} + +func (msg CreateMsg) Type() string { return "paychan" } + +func (msg CreateMsg) GetSigners() []sdk.Address { + // sender + //return []sdk.Address{msg.sender} +} + +func (msg CreateMsg) GetSignBytes() []byte { + +} + +func (msg CreateMsg) ValidateBasic() sdk.Error { + // verify msg as much as possible without using external information (such as account balance) + // are all fields present + // are all fields valid + // maybe check if sender and receiver is different +} + +///////////////// +type CloseMsg struct { + // have to include sender and receiver in msg explicitly (rather than just universal paychanID) + // this gives ability to verify signatures with no external information + sender sdk.Address + receiver sdk.Address + id integer + receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance +} + +func (msg CloseMsg) NewCloseMsg( args... ) CloseMsg { + return CloseMsg{ args... } +} + +func (msg CloseMsg) Type() string { return "paychan" } + +func (msg CloseMsg) GetSigners() []sdk.Address { + // sender and receiver +} + +func (msg CloseMsg) GetSignBytes() []byte { + +} + +func (msg CloseMsg) ValidateBasic() sdk.Error { + return msg.IBCPacket.ValidateBasic() +} + From 757b6eaff4a7fef65188ab138fbbf81ad8770083 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 9 Jul 2018 19:46:51 +0100 Subject: [PATCH 02/30] add basic keeper logic --- internal/x/paychan/handler.go | 1 + internal/x/paychan/keeper.go | 80 ++++++++++++++++++++++++++++------- internal/x/paychan/types.go | 1 + 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index e25c68f9..b32556f9 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -5,6 +5,7 @@ import ( "reflect" ) +// Called when adding routes to a newly created app. // NewHandler returns a handler for "paychan" type messages. func NewHandler(k Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 058ec628..58209f63 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -1,62 +1,112 @@ package paychan +import ( + "strconv" +) + // keeper of the paychan store type Keeper struct { - storeKey sdk.StoreKey - cdc *wire.Codec // needed? + storeKey sdk.StoreKey + //cdc *wire.Codec // needed? coinKeeper bank.Keeper // codespace - codespace sdk.CodespaceType // ?? + //codespace sdk.CodespaceType // ?? } -func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { +// Called when creating new app. +//func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { +func NewKeeper(key sdk.StoreKey, ck bank.Keeper) Keeper { keeper := Keeper{ - storeKey: key, - cdc: cdc, + storeKey: key, + //cdc: cdc, coinKeeper: ck, - codespace: codespace, + //codespace: codespace, } return keeper } // bunch of business logic ... -func (keeper Keeper) GetPaychan(paychanID) Paychan { + +// Reteive a payment channel struct from the blockchain store. +// They are indexed by a concatenation of sender address, receiver address, and an integer. +func (keeper Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id integer) (Paychan, bool) { + // Return error as second argument instead of bool? + var pych Paychan // load from DB - // unmarshall + store := ctx.KVStore(k.storeKey) + bz := store.Get(paychanKey(sender, receiver, id)) + if bz == nil { + return pych, false + } + // unmarshal + k.cdc.MustUnmarshalBinary(bz, &pych) // return + return pych, true } +// Store payment channel struct in blockchain store. func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { + store := ctx.KVStore(k.storeKey) // marshal + bz := k.cdc.MustMarshalBinary(pych) // write to db + pychKey := paychanKey(pych.sender, pych.receiver, pych.id) + store.Set(pychKey, bz) + // TODO handler errors } -func (keeer Keeper) CreatePaychan(receiver sdkAddress, amt sdk.Coins) (Paychan, sdk.Error) { +// Create a new payment channel and lock up sender funds. +func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, id integer, amt sdk.Coins) (Paychan, sdk.Error) { // subtract coins from sender + k.coinKeeper.SubtractCoins(ctx, sender, amt) // create new Paychan struct (create ID) + pych := Paychan{sender, + receiver, + id, + balance: amt} // save to db + err := k.setPaychan(pych) - // validation: - // sender has enough coins + return pych, err + + // TODO validation + // sender has enough coins - done in Subtract method // receiver address exists? // paychan doesn't exist already } -func (keeper Keeper) ClosePaychan() sdk.Error { +// Close a payment channel and distribute funds to participants. +func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id integer, receiverAmt sdk.Coins) sdk.Error { + pych := GetPaychan(ctx, sender, receiver, id) + // compute coin distribution + senderAmt = pych.balance.Minus(receiverAmt) // Minus sdk.Coins method // add coins to sender + k.coinKeeper.AddCoins(ctx, sender, senderAmt) // add coins to receiver + k.coinKeeper.AddCoins(ctx, receiver, receiverAmt) // delete paychan from db + pychKey := paychanKey(pych.sender, pych.receiver, pych.id) + store.Delete(pychKey) - // validation: + + // TODO validation // paychan exists // output coins are less than paychan balance // sender and receiver addresses exist? + + return nil } -func paychanKey(Paychan) { +// Creates a key to reference a paychan in the blockchain store. +func paychanKey(sender sdk.Address, receiver sdk.Address, id integer) []byte { + + //sdk.Address is just a slice of bytes under a different name + //convert id to string then to byte slice + idAsBytes := []byte(strconv.Itoa(id)) // concat sender and receiver and integer ID + return append(sender.Bytes(), receiver.Bytes()..., idAsBytes...) } // maybe getAllPaychans(sender sdk.address) []Paychan diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index aec54ebc..a710c554 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -11,6 +11,7 @@ struct Paychan { balance sdk.Coins sender sdk.Address receiver sdk.Address + id integer } From dfdb06ce6f238e7218e4ba58f51b43f632b91297 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 9 Jul 2018 19:50:59 +0100 Subject: [PATCH 03/30] add pkg doc file --- internal/x/paychan/doc.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 internal/x/paychan/doc.go diff --git a/internal/x/paychan/doc.go b/internal/x/paychan/doc.go new file mode 100644 index 00000000..2b063b08 --- /dev/null +++ b/internal/x/paychan/doc.go @@ -0,0 +1,5 @@ +/* +High level package documentation. +TODO Explain how the payment channels are implemented. +*/ +package paychan From 4732c32ab1aa506801c4bb8c1099815202941e48 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Jul 2018 14:56:04 +0100 Subject: [PATCH 04/30] add handler logic --- internal/x/paychan/handler.go | 40 ++++++++++++++++++++++++----------- internal/x/paychan/keeper.go | 33 +++++++++++++++++++++++------ internal/x/paychan/types.go | 32 ++++++++++++++-------------- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index b32556f9..cb89735d 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -1,19 +1,19 @@ package paychan import ( - sdk "github.com/cosmos/cosmos-sdk/types" "reflect" + sdk "github.com/cosmos/cosmos-sdk/types" ) -// Called when adding routes to a newly created app. // NewHandler returns a handler for "paychan" type messages. +// Called when adding routes to a newly created app. func NewHandler(k Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { - case MsgSend: - return handleMsgSend(ctx, k, msg) - case MsgIssue: - return handleMsgIssue(ctx, k, msg) + case MsgCreate: + return handleMsgCreate(ctx, k, msg) + case MsgClose: + return handleMsgClose(ctx, k, msg) default: errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name() return sdk.ErrUnknownRequest(errMsg).Result() @@ -21,14 +21,30 @@ func NewHandler(k Keeper) sdk.Handler { } } +// TODO does validation go here or in the keeper? + // Handle CreateMsg. -func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgSend) sdk.Result { - // k.CreatePaychan(args...) - // handle erros +func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { + // TODO maybe remove tags for first version + tags, err := k.CreatePaychan(msg.sender, msg.receiver, msg.amount) + if err != nil { + return err.Result() + } + // TODO any other information that should be returned in Result? + return sdk.Result{ + Tags: tags + } } // Handle CloseMsg. -func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgIssue) sdk.Result { - // k.ClosePaychan(args...) - // handle errors +func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { + // TODO maybe remove tags for first version + tags, err := k.ClosePaychan(msg.sender, msg.receiver, msg.id, msg.receiverAmount) + if err != nil { + return err.Result() + } + // These tags can be used to subscribe to channel closures + return sdk.Result{ + Tags: tags + } } diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 58209f63..1a22136b 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -2,12 +2,15 @@ package paychan import ( "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" ) // keeper of the paychan store type Keeper struct { storeKey sdk.StoreKey - //cdc *wire.Codec // needed? + cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper // codespace @@ -16,10 +19,10 @@ type Keeper struct { // Called when creating new app. //func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { -func NewKeeper(key sdk.StoreKey, ck bank.Keeper) Keeper { +func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { keeper := Keeper{ storeKey: key, - //cdc: cdc, + cdc: cdc, coinKeeper: ck, //codespace: codespace, } @@ -58,7 +61,9 @@ func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { } // Create a new payment channel and lock up sender funds. -func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, id integer, amt sdk.Coins) (Paychan, sdk.Error) { +func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { + // Calculate next id (num existing paychans plus 1) + id := len(keeper.GetPaychans(sender, receiver)) + 1 // subtract coins from sender k.coinKeeper.SubtractCoins(ctx, sender, amt) // create new Paychan struct (create ID) @@ -69,16 +74,18 @@ func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, id in // save to db err := k.setPaychan(pych) - return pych, err // TODO validation // sender has enough coins - done in Subtract method // receiver address exists? // paychan doesn't exist already + + tags := sdk.NewTags() + return tags, err } // Close a payment channel and distribute funds to participants. -func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id integer, receiverAmt sdk.Coins) sdk.Error { +func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id integer, receiverAmt sdk.Coins) (sdk.Tags, sdk.Error) { pych := GetPaychan(ctx, sender, receiver, id) // compute coin distribution senderAmt = pych.balance.Minus(receiverAmt) // Minus sdk.Coins method @@ -96,7 +103,13 @@ func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id i // output coins are less than paychan balance // sender and receiver addresses exist? - return nil + //sdk.NewTags( + // "action", []byte("channel closure"), + // "receiver", receiver.Bytes(), + // "sender", sender.Bytes(), + // "id", ??) + tags := sdk.NewTags() + return tags, nil } // Creates a key to reference a paychan in the blockchain store. @@ -109,4 +122,10 @@ func paychanKey(sender sdk.Address, receiver sdk.Address, id integer) []byte { return append(sender.Bytes(), receiver.Bytes()..., idAsBytes...) } +// Get all paychans between a given sender and receiver. +func (keeper Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan { + var paychans []Paychan + // TODO Implement this + return paychans +} // maybe getAllPaychans(sender sdk.address) []Paychan diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index a710c554..d6fc939b 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -8,10 +8,10 @@ import ( // probably want to convert this to a general purpose "state" struct Paychan { - balance sdk.Coins sender sdk.Address receiver sdk.Address id integer + balance sdk.Coins } @@ -40,29 +40,29 @@ struct Paychan { /////////////// CreatePayChan // find a less confusing name -type CreateMsg struct { +type MsgCreate struct { // maybe just wrap a paychan struct sender sdk.Address receiver sdk.Address - amount sdk.Balance + amount sdk.Coins } -func (msg CreatMsg) NewCreateMsg() CreateMsg { - return CreateMsg{ } +func (msg CreatMsg) NewMsgCreate() MsgCreate { + return MsgCreate{ } } -func (msg CreateMsg) Type() string { return "paychan" } +func (msg MsgCreate) Type() string { return "paychan" } -func (msg CreateMsg) GetSigners() []sdk.Address { +func (msg MsgCreate) GetSigners() []sdk.Address { // sender //return []sdk.Address{msg.sender} } -func (msg CreateMsg) GetSignBytes() []byte { +func (msg MsgCreate) GetSignBytes() []byte { } -func (msg CreateMsg) ValidateBasic() sdk.Error { +func (msg MsgCreate) ValidateBasic() sdk.Error { // verify msg as much as possible without using external information (such as account balance) // are all fields present // are all fields valid @@ -70,7 +70,7 @@ func (msg CreateMsg) ValidateBasic() sdk.Error { } ///////////////// -type CloseMsg struct { +type MsgClose struct { // have to include sender and receiver in msg explicitly (rather than just universal paychanID) // this gives ability to verify signatures with no external information sender sdk.Address @@ -79,21 +79,21 @@ type CloseMsg struct { receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance } -func (msg CloseMsg) NewCloseMsg( args... ) CloseMsg { - return CloseMsg{ args... } +func (msg MsgClose) NewMsgClose( args... ) MsgClose { + return MsgClose{ args... } } -func (msg CloseMsg) Type() string { return "paychan" } +func (msg MsgClose) Type() string { return "paychan" } -func (msg CloseMsg) GetSigners() []sdk.Address { +func (msg MsgClose) GetSigners() []sdk.Address { // sender and receiver } -func (msg CloseMsg) GetSignBytes() []byte { +func (msg MsgClose) GetSignBytes() []byte { } -func (msg CloseMsg) ValidateBasic() sdk.Error { +func (msg MsgClose) ValidateBasic() sdk.Error { return msg.IBCPacket.ValidateBasic() } From f146d9ae0c506f71ee7a812dde7b1f012100f112 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 10 Jul 2018 15:44:53 +0100 Subject: [PATCH 05/30] add basic msg logic --- internal/x/paychan/types.go | 118 +++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 29 deletions(-) diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index d6fc939b..3dbb0549 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -4,8 +4,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -//////// - +// Paychan Type +// Used to represent paychan in keeper module and to serialize. // probably want to convert this to a general purpose "state" struct Paychan { sender sdk.Address @@ -15,7 +15,7 @@ struct Paychan { } -///////////// +// Message Types // Message implement the sdk.Msg interface: @@ -38,62 +38,122 @@ struct Paychan { // GetSigners() []Address // } -/////////////// CreatePayChan -// find a less confusing name + +// A message to create a payment channel. type MsgCreate struct { // maybe just wrap a paychan struct - sender sdk.Address + sender sdk.Address receiver sdk.Address - amount sdk.Coins + amount sdk.Coins } -func (msg CreatMsg) NewMsgCreate() MsgCreate { - return MsgCreate{ } -} +// Create a new message. +// Called in client code when constructing transaction from cli args to send to the network. +// maybe just a placeholder for more advanced future functionality? +// func (msg CreatMsg) NewMsgCreate(sender sdk.Address, receiver sdk.Address, amount sdk.Coins) MsgCreate { +// return MsgCreate{ +// sender +// receiver +// amount +// } +// } func (msg MsgCreate) Type() string { return "paychan" } -func (msg MsgCreate) GetSigners() []sdk.Address { - // sender - //return []sdk.Address{msg.sender} -} - func (msg MsgCreate) GetSignBytes() []byte { - + // TODO create msgCdc in wire.go + b, err := msgCdc.MarshalJSON(struct { + SenderAddr string `json:"sender_addr"` + ReceiverAddr string `json:"receiver_addr"` + Amount sdk.Coins `json:"amount"` + }{ + SenderAddr: sdk.MustBech32ifyAcc(msg.sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), + Amount: msg.amount, + }) + if err != nil { + panic(err) + } + return b } func (msg MsgCreate) ValidateBasic() sdk.Error { + // TODO implement // verify msg as much as possible without using external information (such as account balance) // are all fields present // are all fields valid // maybe check if sender and receiver is different + + // maybe add custom errors + // learn how the errors work + + // example from bank + // if len(in.Address) == 0 { + // return sdk.ErrInvalidAddress(in.Address.String()) + // } + // if !in.Coins.IsValid() { + // return sdk.ErrInvalidCoins(in.Coins.String()) + // } + // if !in.Coins.IsPositive() { + // return sdk.ErrInvalidCoins(in.Coins.String()) + // } } -///////////////// +func (msg MsgCreate) GetSigners() []sdk.Address { + // Only sender must sign to create a paychan + return []sdk.Address{msg.sender} +} + + + + +// A message to close a payment channel. type MsgClose struct { // have to include sender and receiver in msg explicitly (rather than just universal paychanID) // this gives ability to verify signatures with no external information - sender sdk.Address - receiver sdk.Address - id integer + sender sdk.Address + receiver sdk.Address + id integer receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance } -func (msg MsgClose) NewMsgClose( args... ) MsgClose { - return MsgClose{ args... } -} +// func (msg MsgClose) NewMsgClose(sender sdk.Address, receiver sdk.Address, id integer, receiverAmount sdk.Coins) MsgClose { +// return MsgClose{ +// sender +// receiver +// id +// receiverAmount +// } +// } func (msg MsgClose) Type() string { return "paychan" } -func (msg MsgClose) GetSigners() []sdk.Address { - // sender and receiver -} - func (msg MsgClose) GetSignBytes() []byte { - + // TODO create msgCdc in wire.go + b, err := msgCdc.MarshalJSON(struct { + SenderAddr string `json:"sender_addr"` + ReceiverAddr string `json:"receiver_addr"` + Id integer `json:"id"` + ReceiverAmount sdk.Coins `json:"receiver_amount"` + }{ + SenderAddr: sdk.MustBech32ifyAcc(msg.sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), + Id: msg.id + Amount: msg.receiverAmount, + }) + if err != nil { + panic(err) + } + return b } func (msg MsgClose) ValidateBasic() sdk.Error { - return msg.IBCPacket.ValidateBasic() + // TODO implement + //return msg.IBCPacket.ValidateBasic() +} + +func (msg MsgClose) GetSigners() []sdk.Address { + // Both sender and receiver must sign in order to close a channel + retutn []sdk.Address{sender, receiver} } From af4c28e1b7e57b09d7680da6f42e87ec5731abba Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Jul 2018 22:02:07 +0100 Subject: [PATCH 06/30] plan out validation --- internal/x/paychan/handler.go | 3 ++- internal/x/paychan/keeper.go | 13 +++++++++++-- internal/x/paychan/types.go | 17 ++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index cb89735d..47840412 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -21,9 +21,9 @@ func NewHandler(k Keeper) sdk.Handler { } } -// TODO does validation go here or in the keeper? // Handle CreateMsg. +// Leaves validation to the keeper methods. func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // TODO maybe remove tags for first version tags, err := k.CreatePaychan(msg.sender, msg.receiver, msg.amount) @@ -37,6 +37,7 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { } // Handle CloseMsg. +// Leaves validation to the keeper methods. func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { // TODO maybe remove tags for first version tags, err := k.ClosePaychan(msg.sender, msg.receiver, msg.id, msg.receiverAmount) diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 1a22136b..74aa0ddb 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -8,6 +8,8 @@ import ( ) // keeper of the paychan store +// Handles validation internally. Does not rely on calling code to do validation. +// Aim to keep public methids safe, private ones not necessaily. type Keeper struct { storeKey sdk.StoreKey cdc *wire.Codec // needed to serialize objects before putting them in the store @@ -63,7 +65,7 @@ func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { // Create a new payment channel and lock up sender funds. func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { // Calculate next id (num existing paychans plus 1) - id := len(keeper.GetPaychans(sender, receiver)) + 1 + id := len(keeper.GetPaychans(sender, receiver)) + 1 // TODO check for overflow? // subtract coins from sender k.coinKeeper.SubtractCoins(ctx, sender, amt) // create new Paychan struct (create ID) @@ -76,9 +78,13 @@ func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, amt s // TODO validation + // coins valid and positive + // sender has enough coins - done in Subtract method // receiver address exists? // paychan doesn't exist already + // sender and receiver different? + tags := sdk.NewTags() return tags, err @@ -99,9 +105,12 @@ func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id i // TODO validation + // id ≥ 0 + // coins valid and positive // paychan exists - // output coins are less than paychan balance + // output coins are equal to paychan balance // sender and receiver addresses exist? + // overflow in sender and receiver balances? //sdk.NewTags( // "action", []byte("channel closure"), diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 3dbb0549..5eb3bf10 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -79,13 +79,12 @@ func (msg MsgCreate) GetSignBytes() []byte { func (msg MsgCreate) ValidateBasic() sdk.Error { // TODO implement - // verify msg as much as possible without using external information (such as account balance) - // are all fields present - // are all fields valid - // maybe check if sender and receiver is different + // Validate msg as an optimisation to avoid all validation going to keeper. It's run before the sigs are checked by the auth module. + // Validate without external information (such as account balance) - // maybe add custom errors - // learn how the errors work + // check if all fields present / not 0 valued + // do coin checks for amount + // check if Address valid? // example from bank // if len(in.Address) == 0 { @@ -149,7 +148,11 @@ func (msg MsgClose) GetSignBytes() []byte { func (msg MsgClose) ValidateBasic() sdk.Error { // TODO implement - //return msg.IBCPacket.ValidateBasic() + + // check if all fields present / not 0 valued + // check id ≥ 0 + // do coin checks for amount + // check if Address valid? } func (msg MsgClose) GetSigners() []sdk.Address { From fbffc2d008d45a1e2fd241b9863f69dcf01cff36 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Jul 2018 14:32:36 +0100 Subject: [PATCH 07/30] add validation --- internal/x/paychan/keeper.go | 106 +++++++++++++++++++++++++---------- internal/x/paychan/types.go | 56 ++++++++++++------ 2 files changed, 115 insertions(+), 47 deletions(-) diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 74aa0ddb..1faa569f 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -58,60 +58,108 @@ func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { bz := k.cdc.MustMarshalBinary(pych) // write to db pychKey := paychanKey(pych.sender, pych.receiver, pych.id) - store.Set(pychKey, bz) - // TODO handler errors + store.Set(pychKey, bz) // panics if something goes wrong } // Create a new payment channel and lock up sender funds. -func (keeer Keeper) CreatePaychan(sender sdk.Address, receiver sdkAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { +func (keeer Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdkAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { + // TODO move validation somewhere nicer + // args present + if len(sender) == 0 { + return sdk.ErrInvalidAddress(sender.String()) + } + if len(receiver) == 0 { + return sdk.ErrInvalidAddress(receiver.String()) + } + if len(amount) == 0 { + return sdk.ErrInvalidCoins(amount.String()) + } + // Check if coins are sorted, non zero, positive + if !amount.IsValid() { + return sdk.ErrInvalidCoins(amount.String()) + } + if !amount.IsPositive() { + return sdk.ErrInvalidCoins(amount.String()) + } + // sender should exist already as they had to sign. + // receiver address exists. am is the account mapper in the coin keeper. + // TODO automatically create account if not present? + if k.coinKepper.am.GetAccount(ctx, receiver) == nil { + return sdk.ErrUnknownAddress(receiver.String()) + } + // sender has enough coins - done in Subtract method + // TODO check if sender and receiver different? + + // Calculate next id (num existing paychans plus 1) id := len(keeper.GetPaychans(sender, receiver)) + 1 // TODO check for overflow? // subtract coins from sender - k.coinKeeper.SubtractCoins(ctx, sender, amt) - // create new Paychan struct (create ID) + coins, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, amt) + if err != nil { + return nil, err + } + // create new Paychan struct pych := Paychan{sender, - receiver, - id, - balance: amt} + receiver, + id, + balance: amt} // save to db - err := k.setPaychan(pych) - - - // TODO validation - // coins valid and positive - - // sender has enough coins - done in Subtract method - // receiver address exists? - // paychan doesn't exist already - // sender and receiver different? - + k.setPaychan(pych) + + // TODO create tags tags := sdk.NewTags() return tags, err } // Close a payment channel and distribute funds to participants. func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id integer, receiverAmt sdk.Coins) (sdk.Tags, sdk.Error) { - pych := GetPaychan(ctx, sender, receiver, id) + if len(msg.sender) == 0 { + return sdk.ErrInvalidAddress(msg.sender.String()) + } + if len(msg.receiver) == 0 { + return sdk.ErrInvalidAddress(msg.receiver.String()) + } + if len(msg.receiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } + // check id ≥ 0 + if msg.id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(id)) // TODO implement custom errors + } + + // Check if coins are sorted, non zero, non negative + if !msg.receiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } + if !msg.receiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } + + + store := ctx.KVStore(k.storeKey) + + pych, exists := GetPaychan(ctx, sender, receiver, id) + if !exists { + return nil, sdk.ErrUnknownAddress() // TODO implement custom errors + } // compute coin distribution senderAmt = pych.balance.Minus(receiverAmt) // Minus sdk.Coins method + // check that receiverAmt not greater than paychan balance + if !senderAmt.IsNotNegative() { + return nil, sdk.ErrInsufficientFunds(pych.balance.String()) + } // add coins to sender + // creating account if it doesn't exist k.coinKeeper.AddCoins(ctx, sender, senderAmt) // add coins to receiver k.coinKeeper.AddCoins(ctx, receiver, receiverAmt) + // delete paychan from db pychKey := paychanKey(pych.sender, pych.receiver, pych.id) store.Delete(pychKey) - - // TODO validation - // id ≥ 0 - // coins valid and positive - // paychan exists - // output coins are equal to paychan balance - // sender and receiver addresses exist? - // overflow in sender and receiver balances? - + // TODO create tags //sdk.NewTags( // "action", []byte("channel closure"), // "receiver", receiver.Bytes(), diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 5eb3bf10..91cc2c8b 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -1,6 +1,7 @@ package paychan import ( + "strconv" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -78,24 +79,27 @@ func (msg MsgCreate) GetSignBytes() []byte { } func (msg MsgCreate) ValidateBasic() sdk.Error { - // TODO implement // Validate msg as an optimisation to avoid all validation going to keeper. It's run before the sigs are checked by the auth module. // Validate without external information (such as account balance) // check if all fields present / not 0 valued - // do coin checks for amount - // check if Address valid? - - // example from bank - // if len(in.Address) == 0 { - // return sdk.ErrInvalidAddress(in.Address.String()) - // } - // if !in.Coins.IsValid() { - // return sdk.ErrInvalidCoins(in.Coins.String()) - // } - // if !in.Coins.IsPositive() { - // return sdk.ErrInvalidCoins(in.Coins.String()) - // } + if len(msg.sender) == 0 { + return sdk.ErrInvalidAddress(msg.sender.String()) + } + if len(msg.receiver) == 0 { + return sdk.ErrInvalidAddress(msg.receiver.String()) + } + if len(msg.amount) == 0 { + return sdk.ErrInvalidCoins(msg.amount.String()) + } + // Check if coins are sorted, non zero, non negative + if !msg.amount.IsValid() { + return sdk.ErrInvalidCoins(msg.amount.String()) + } + if !msg.amount.IsPositive() { + return sdk.ErrInvalidCoins(msg.amount.String()) + } + // TODO check if Address valid? } func (msg MsgCreate) GetSigners() []sdk.Address { @@ -147,12 +151,28 @@ func (msg MsgClose) GetSignBytes() []byte { } func (msg MsgClose) ValidateBasic() sdk.Error { - // TODO implement - // check if all fields present / not 0 valued + if len(msg.sender) == 0 { + return sdk.ErrInvalidAddress(msg.sender.String()) + } + if len(msg.receiver) == 0 { + return sdk.ErrInvalidAddress(msg.receiver.String()) + } + if len(msg.receiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } // check id ≥ 0 - // do coin checks for amount - // check if Address valid? + if msg.id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(id)) // TODO implement custom errors + } + // Check if coins are sorted, non zero, non negative + if !msg.receiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } + if !msg.receiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + } + // TODO check if Address valid? } func (msg MsgClose) GetSigners() []sdk.Address { From 315a0cefe978748addc1c49d52da00f4ac238f94 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Jul 2018 14:49:13 +0100 Subject: [PATCH 08/30] add minimal cmds plan --- internal/x/paychan/client/cmd/cmd.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 168be42d..5a262519 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -19,6 +19,11 @@ import ( // send paychan payment // get balance from receiver +// minimum +// create paychan (sender signs) +// create state update (sender signs) (just a half signed close tx, (json encoded?)) +// close paychan (receiver signs) (provide state update as arg) + // example from x/auth /* func GetAccountCmd(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder) *cobra.Command { From 469dd84a326bfa758ba8cce2f6b612a06f704191 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 13 Jul 2018 14:27:55 +0100 Subject: [PATCH 09/30] fix syntax errors --- internal/x/paychan/handler.go | 11 ++-- internal/x/paychan/keeper.go | 104 ++++++++++++++++-------------- internal/x/paychan/keeper_test.go | 3 +- internal/x/paychan/types.go | 34 +++++----- internal/x/paychan/wire.go | 18 ++++++ 5 files changed, 93 insertions(+), 77 deletions(-) create mode 100644 internal/x/paychan/wire.go diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index 47840412..751fe36a 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -1,8 +1,8 @@ package paychan import ( - "reflect" sdk "github.com/cosmos/cosmos-sdk/types" + "reflect" ) // NewHandler returns a handler for "paychan" type messages. @@ -21,18 +21,17 @@ func NewHandler(k Keeper) sdk.Handler { } } - // Handle CreateMsg. // Leaves validation to the keeper methods. func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.CreatePaychan(msg.sender, msg.receiver, msg.amount) + tags, err := k.CreatePaychan(ctx, msg.sender, msg.receiver, msg.amount) if err != nil { return err.Result() } // TODO any other information that should be returned in Result? return sdk.Result{ - Tags: tags + Tags: tags, } } @@ -40,12 +39,12 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // Leaves validation to the keeper methods. func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.ClosePaychan(msg.sender, msg.receiver, msg.id, msg.receiverAmount) + tags, err := k.ClosePaychan(ctx, msg.sender, msg.receiver, msg.id, msg.receiverAmount) if err != nil { return err.Result() } // These tags can be used to subscribe to channel closures return sdk.Result{ - Tags: tags + Tags: tags, } } diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 1faa569f..1fc12b6a 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -5,13 +5,14 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank" ) // keeper of the paychan store // Handles validation internally. Does not rely on calling code to do validation. // Aim to keep public methids safe, private ones not necessaily. type Keeper struct { - storeKey sdk.StoreKey + storeKey sdk.StoreKey cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper @@ -23,7 +24,7 @@ type Keeper struct { //func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { keeper := Keeper{ - storeKey: key, + storeKey: key, cdc: cdc, coinKeeper: ck, //codespace: codespace, @@ -33,10 +34,9 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { // bunch of business logic ... - // Reteive a payment channel struct from the blockchain store. // They are indexed by a concatenation of sender address, receiver address, and an integer. -func (keeper Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id integer) (Paychan, bool) { +func (k Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64) (Paychan, bool) { // Return error as second argument instead of bool? var pych Paychan // load from DB @@ -52,108 +52,109 @@ func (keeper Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sd } // Store payment channel struct in blockchain store. -func (keeper Keeper) setPaychan(pych Paychan) sdk.Error { +func (k Keeper) setPaychan(ctx sdk.Context, pych Paychan) { store := ctx.KVStore(k.storeKey) // marshal - bz := k.cdc.MustMarshalBinary(pych) + bz := k.cdc.MustMarshalBinary(pych) // panics if something goes wrong // write to db pychKey := paychanKey(pych.sender, pych.receiver, pych.id) store.Set(pychKey, bz) // panics if something goes wrong } // Create a new payment channel and lock up sender funds. -func (keeer Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdkAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { +func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, amount sdk.Coins) (sdk.Tags, sdk.Error) { // TODO move validation somewhere nicer // args present if len(sender) == 0 { - return sdk.ErrInvalidAddress(sender.String()) + return nil, sdk.ErrInvalidAddress(sender.String()) } if len(receiver) == 0 { - return sdk.ErrInvalidAddress(receiver.String()) + return nil, sdk.ErrInvalidAddress(receiver.String()) } if len(amount) == 0 { - return sdk.ErrInvalidCoins(amount.String()) + return nil, sdk.ErrInvalidCoins(amount.String()) } // Check if coins are sorted, non zero, positive if !amount.IsValid() { - return sdk.ErrInvalidCoins(amount.String()) + return nil, sdk.ErrInvalidCoins(amount.String()) } if !amount.IsPositive() { - return sdk.ErrInvalidCoins(amount.String()) + return nil, sdk.ErrInvalidCoins(amount.String()) } // sender should exist already as they had to sign. // receiver address exists. am is the account mapper in the coin keeper. // TODO automatically create account if not present? - if k.coinKepper.am.GetAccount(ctx, receiver) == nil { - return sdk.ErrUnknownAddress(receiver.String()) - } + // TODO remove as account mapper not available to this pkg + //if k.coinKeeper.am.GetAccount(ctx, receiver) == nil { + // return nil, sdk.ErrUnknownAddress(receiver.String()) + //} + // sender has enough coins - done in Subtract method // TODO check if sender and receiver different? - // Calculate next id (num existing paychans plus 1) - id := len(keeper.GetPaychans(sender, receiver)) + 1 // TODO check for overflow? + id := int64(len(k.GetPaychans(sender, receiver)) + 1) // TODO check for overflow? // subtract coins from sender - coins, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, amt) + _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, amount) if err != nil { return nil, err } // create new Paychan struct - pych := Paychan{sender, - receiver, - id, - balance: amt} + pych := Paychan{ + sender: sender, + receiver: receiver, + id: id, + balance: amount, + } // save to db - k.setPaychan(pych) - + k.setPaychan(ctx, pych) // TODO create tags - tags := sdk.NewTags() + //tags := sdk.NewTags() return tags, err } // Close a payment channel and distribute funds to participants. -func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id integer, receiverAmt sdk.Coins) (sdk.Tags, sdk.Error) { - if len(msg.sender) == 0 { - return sdk.ErrInvalidAddress(msg.sender.String()) +func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64, receiverAmount sdk.Coins) (sdk.Tags, sdk.Error) { + if len(sender) == 0 { + return nil, sdk.ErrInvalidAddress(sender.String()) } - if len(msg.receiver) == 0 { - return sdk.ErrInvalidAddress(msg.receiver.String()) + if len(receiver) == 0 { + return nil, sdk.ErrInvalidAddress(receiver.String()) } - if len(msg.receiverAmount) == 0 { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if len(receiverAmount) == 0 { + return nil, sdk.ErrInvalidCoins(receiverAmount.String()) } // check id ≥ 0 - if msg.id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(id)) // TODO implement custom errors + if id < 0 { + return nil, sdk.ErrInvalidAddress(strconv.Itoa(int(id))) // TODO implement custom errors } // Check if coins are sorted, non zero, non negative - if !msg.receiverAmount.IsValid() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !receiverAmount.IsValid() { + return nil, sdk.ErrInvalidCoins(receiverAmount.String()) } - if !msg.receiverAmount.IsPositive() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !receiverAmount.IsPositive() { + return nil, sdk.ErrInvalidCoins(receiverAmount.String()) } - store := ctx.KVStore(k.storeKey) - pych, exists := GetPaychan(ctx, sender, receiver, id) + pych, exists := k.GetPaychan(ctx, sender, receiver, id) if !exists { - return nil, sdk.ErrUnknownAddress() // TODO implement custom errors + return nil, sdk.ErrUnknownAddress("paychan not found") // TODO implement custom errors } // compute coin distribution - senderAmt = pych.balance.Minus(receiverAmt) // Minus sdk.Coins method + senderAmount := pych.balance.Minus(receiverAmount) // Minus sdk.Coins method // check that receiverAmt not greater than paychan balance - if !senderAmt.IsNotNegative() { + if !senderAmount.IsNotNegative() { return nil, sdk.ErrInsufficientFunds(pych.balance.String()) } // add coins to sender // creating account if it doesn't exist - k.coinKeeper.AddCoins(ctx, sender, senderAmt) + k.coinKeeper.AddCoins(ctx, sender, senderAmount) // add coins to receiver - k.coinKeeper.AddCoins(ctx, receiver, receiverAmt) + k.coinKeeper.AddCoins(ctx, receiver, receiverAmount) // delete paychan from db pychKey := paychanKey(pych.sender, pych.receiver, pych.id) @@ -170,19 +171,22 @@ func (keeper Keeper) ClosePaychan(sender sdk.Address, receiver sdk.Address, id i } // Creates a key to reference a paychan in the blockchain store. -func paychanKey(sender sdk.Address, receiver sdk.Address, id integer) []byte { - +func paychanKey(sender sdk.Address, receiver sdk.Address, id int64) []byte { + //sdk.Address is just a slice of bytes under a different name //convert id to string then to byte slice - idAsBytes := []byte(strconv.Itoa(id)) + idAsBytes := []byte(strconv.Itoa(int(id))) // concat sender and receiver and integer ID - return append(sender.Bytes(), receiver.Bytes()..., idAsBytes...) + key := append(sender.Bytes(), receiver.Bytes()...) + key = append(key, idAsBytes...) + return key } // Get all paychans between a given sender and receiver. -func (keeper Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan { +func (k Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan { var paychans []Paychan // TODO Implement this return paychans } + // maybe getAllPaychans(sender sdk.address) []Paychan diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 344c3148..a4fd2525 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -2,8 +2,7 @@ package paychan import ( "testing" - - "github.com/stretchr/testify/assert" + //"github.com/stretchr/testify/assert" ) // example from x/bank diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 91cc2c8b..dc7e5129 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -1,21 +1,20 @@ package paychan import ( + sdk "github.com/cosmos/cosmos-sdk/types" "strconv" - sdk "github.com/cosmos/cosmos-sdk/types" ) // Paychan Type // Used to represent paychan in keeper module and to serialize. // probably want to convert this to a general purpose "state" -struct Paychan { - sender sdk.Address +type Paychan struct { + sender sdk.Address receiver sdk.Address - id integer - balance sdk.Coins + id int64 + balance sdk.Coins } - // Message Types // Message implement the sdk.Msg interface: @@ -39,7 +38,6 @@ struct Paychan { // GetSigners() []Address // } - // A message to create a payment channel. type MsgCreate struct { // maybe just wrap a paychan struct @@ -100,6 +98,7 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { return sdk.ErrInvalidCoins(msg.amount.String()) } // TODO check if Address valid? + return nil } func (msg MsgCreate) GetSigners() []sdk.Address { @@ -107,16 +106,13 @@ func (msg MsgCreate) GetSigners() []sdk.Address { return []sdk.Address{msg.sender} } - - - // A message to close a payment channel. type MsgClose struct { // have to include sender and receiver in msg explicitly (rather than just universal paychanID) // this gives ability to verify signatures with no external information sender sdk.Address receiver sdk.Address - id integer + id int64 // TODO is another int type better? receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance } @@ -136,13 +132,13 @@ func (msg MsgClose) GetSignBytes() []byte { b, err := msgCdc.MarshalJSON(struct { SenderAddr string `json:"sender_addr"` ReceiverAddr string `json:"receiver_addr"` - Id integer `json:"id"` + Id int64 `json:"id"` ReceiverAmount sdk.Coins `json:"receiver_amount"` }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), - Id: msg.id - Amount: msg.receiverAmount, + SenderAddr: sdk.MustBech32ifyAcc(msg.sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), + Id: msg.id, + ReceiverAmount: msg.receiverAmount, }) if err != nil { panic(err) @@ -163,7 +159,7 @@ func (msg MsgClose) ValidateBasic() sdk.Error { } // check id ≥ 0 if msg.id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(id)) // TODO implement custom errors + return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.id))) // TODO implement custom errors } // Check if coins are sorted, non zero, non negative if !msg.receiverAmount.IsValid() { @@ -173,10 +169,10 @@ func (msg MsgClose) ValidateBasic() sdk.Error { return sdk.ErrInvalidCoins(msg.receiverAmount.String()) } // TODO check if Address valid? + return nil } func (msg MsgClose) GetSigners() []sdk.Address { // Both sender and receiver must sign in order to close a channel - retutn []sdk.Address{sender, receiver} + return []sdk.Address{msg.sender, msg.receiver} } - diff --git a/internal/x/paychan/wire.go b/internal/x/paychan/wire.go new file mode 100644 index 00000000..dfb2d468 --- /dev/null +++ b/internal/x/paychan/wire.go @@ -0,0 +1,18 @@ +package paychan + +import ( + "github.com/cosmos/cosmos-sdk/wire" +) + +func RegisterWire(cdc *wire.Codec) { + cdc.RegisterConcrete(MsgCreate{}, "paychan/MsgCreate", nil) + cdc.RegisterConcrete(MsgClose{}, "paychan/MsgClose", nil) +} + +var msgCdc = wire.NewCodec() + +func init() { + RegisterWire(msgCdc) + // TODO is this needed? + //wire.RegisterCrypto(msgCdc) +} From 97a7f79ed879e9b2b5dbc440868cfa54273f3b7c Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 13 Jul 2018 16:22:58 +0100 Subject: [PATCH 10/30] add initial keeper tests --- internal/x/paychan/keeper_test.go | 249 +++++++++++++++++++++--------- 1 file changed, 179 insertions(+), 70 deletions(-) diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index a4fd2525..b965f9de 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -3,90 +3,199 @@ package paychan import ( "testing" //"github.com/stretchr/testify/assert" + + abci "github.com/tendermint/abci/types" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" ) -// example from x/bank +// GetPaychan +// - gets a paychan if it exists, and not if it doesn't +// setPaychan +// - sets a paychan +// CreatePaychan +// - creates a paychan under normal conditions +// ClosePaychan +// - closes a paychan under normal conditions +// GetPaychans +// paychanKey + +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { + // create db + db := dbm.NewMemDB() + // create keys + authKey := sdk.NewKVStoreKey("authkey") + paychanKey := sdk.NewKVStoreKey("paychankey") + // create new multistore around db + ms := store.NewCommitMultiStore(db) // DB handle plus store key maps + // register separate stores in the multistore + ms.MountStoreWithDB(authKey, sdk.StoreTypeIAVL, db) // sets store key map + ms.MountStoreWithDB(paychanKey, sdk.StoreTypeIAVL, db) + ms.LoadLatestVersion() + + return ms, authKey, paychanKey +} + +func setupCodec() *wire.Codec { + cdc := wire.NewCodec() + auth.RegisterBaseAccount(cdc) + // TODO might need to register paychan struct + return cdc +} + func TestKeeper(t *testing.T) { - // ms, authKey := setupMultiStore() + // Setup + // create multistore and key + ms, authKey, paychanKey := setupMultiStore() - // cdc := wire.NewCodec() - // auth.RegisterBaseAccount(cdc) + // create and initialise codec(s) + cdc := setupCodec() - // ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) - // accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) - // coinKeeper := NewKeeper(accountMapper) + // create context + ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) - // addr := sdk.Address([]byte("addr1")) - // addr2 := sdk.Address([]byte("addr2")) - // addr3 := sdk.Address([]byte("addr3")) - // acc := accountMapper.NewAccountWithAddress(ctx, addr) + // create accountMapper + accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) - // // Test GetCoins/SetCoins - // accountMapper.SetAccount(ctx, acc) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{})) + // create coinkeeper + coinKeeper := bank.NewKeeper(accountMapper) - // coinKeeper.SetCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + // create keeper + paychanKeeper := NewKeeper(cdc, paychanKey, coinKeeper) - // // Test HasCoins - // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 10}})) - // assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 5}})) - // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 15}})) - // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})) + // Test no paychans exist + _, exists := paychanKeeper.GetPaychan(ctx, sdk.Address{}, sdk.Address{}, 0) + if exists { + t.Error("payment channel found when none exist") + } - // // Test AddCoins - // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"foocoin", 15}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 25}})) + // Test paychan can be set and get + p := Paychan{ + sender: sdk.Address([]byte("senderAddress")), + receiver: sdk.Address([]byte("receiverAddress")), + id: 0, + balance: sdk.Coins{{"KVA", 100}}, + } + paychanKeeper.setPaychan(ctx, p) - // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 15}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 15}, {"foocoin", 25}})) + _, exists = paychanKeeper.GetPaychan(ctx, p.sender, p.receiver, p.id) + if !exists { + t.Error("payment channel not found") + } - // // Test SubtractCoins - // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) - // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + // Test create paychan under normal conditions + senderAddress := sdk.Address([]byte("senderAddress")) + senderFunds := sdk.Coins{{"KVA", 100}} + receiverAddress := sdk.Address([]byte("receiverAddress")) + balance := sdk.Coins{{"KVA", 10}} - // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + coinKeeper.SetCoins(ctx, senderAddress, senderFunds) - // coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 15}})) - // assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 1}})) + _, err := paychanKeeper.CreatePaychan(ctx, senderAddress, receiverAddress, balance) + if err != nil { + t.Error("unexpected error created payment channel", err) + } - // // Test SendCoins - // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 5}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) - - // _, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}}) - // assert.Implements(t, (*sdk.Error)(nil), err2) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) - - // coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 30}}) - // coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"barcoin", 10}, {"foocoin", 5}}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 5}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 10}})) - - // // Test InputOutputCoins - // input1 := NewInput(addr2, sdk.Coins{{"foocoin", 2}}) - // output1 := NewOutput(addr, sdk.Coins{{"foocoin", 2}}) - // coinKeeper.InputOutputCoins(ctx, []Input{input1}, []Output{output1}) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 7}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 8}})) - - // inputs := []Input{ - // NewInput(addr, sdk.Coins{{"foocoin", 3}}), - // NewInput(addr2, sdk.Coins{{"barcoin", 3}, {"foocoin", 2}}), - // } - - // outputs := []Output{ - // NewOutput(addr, sdk.Coins{{"barcoin", 1}}), - // NewOutput(addr3, sdk.Coins{{"barcoin", 2}, {"foocoin", 5}}), - // } - // coinKeeper.InputOutputCoins(ctx, inputs, outputs) - // assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 21}, {"foocoin", 4}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 7}, {"foocoin", 6}})) - // assert.True(t, coinKeeper.GetCoins(ctx, addr3).IsEqual(sdk.Coins{{"barcoin", 2}, {"foocoin", 5}})) + p, _ = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) + if !p.balance.IsEqual(balance) { + t.Error("payment channel balance incorrect", p.balance, balance) + } + expectedNewSenderFunds := senderFunds.Minus(balance) + if !coinKeeper.GetCoins(ctx, senderAddress).IsEqual(expectedNewSenderFunds) { + t.Error("sender has incorrect balance after paychan creation") + } } + +// example from x/bank + +//func TestKeeper(t *testing.T) { +// ms, authKey := setupMultiStore() + +// cdc := wire.NewCodec() +// auth.RegisterBaseAccount(cdc) + +// ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) +// accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) +// coinKeeper := NewKeeper(accountMapper) + +// addr := sdk.Address([]byte("addr1")) +// addr2 := sdk.Address([]byte("addr2")) +// addr3 := sdk.Address([]byte("addr3")) +// acc := accountMapper.NewAccountWithAddress(ctx, addr) + +// // Test GetCoins/SetCoins +// accountMapper.SetAccount(ctx, acc) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{})) + +// coinKeeper.SetCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) + +// // Test HasCoins +// assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 10}})) +// assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 5}})) +// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 15}})) +// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})) + +// // Test AddCoins +// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"foocoin", 15}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 25}})) + +// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 15}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 15}, {"foocoin", 25}})) + +// // Test SubtractCoins +// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) +// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + +// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) + +// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 15}})) +// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 1}})) + +// // Test SendCoins +// coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 5}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + +// _, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}}) +// assert.Implements(t, (*sdk.Error)(nil), err2) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) + +// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 30}}) +// coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"barcoin", 10}, {"foocoin", 5}}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 5}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 10}})) + +// // Test InputOutputCoins +// input1 := NewInput(addr2, sdk.Coins{{"foocoin", 2}}) +// output1 := NewOutput(addr, sdk.Coins{{"foocoin", 2}}) +// coinKeeper.InputOutputCoins(ctx, []Input{input1}, []Output{output1}) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 7}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 8}})) + +// inputs := []Input{ +// NewInput(addr, sdk.Coins{{"foocoin", 3}}), +// NewInput(addr2, sdk.Coins{{"barcoin", 3}, {"foocoin", 2}}), +// } + +// outputs := []Output{ +// NewOutput(addr, sdk.Coins{{"barcoin", 1}}), +// NewOutput(addr3, sdk.Coins{{"barcoin", 2}, {"foocoin", 5}}), +// } +// coinKeeper.InputOutputCoins(ctx, inputs, outputs) +// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 21}, {"foocoin", 4}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 7}, {"foocoin", 6}})) +// assert.True(t, coinKeeper.GetCoins(ctx, addr3).IsEqual(sdk.Coins{{"barcoin", 2}, {"foocoin", 5}})) + +//} From db7d440ca10ef6a0ca16afc65f52d3721165b077 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 14 Jul 2018 01:09:02 +0100 Subject: [PATCH 11/30] add minimal cli functions --- internal/x/paychan/client/cmd/cmd.go | 197 ++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 34 deletions(-) diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 5a262519..0c704389 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -3,11 +3,12 @@ package cli import ( "fmt" + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" "github.com/spf13/cobra" - //"github.com/cosmos/cosmos-sdk/client/context" - //sdk "github.com/cosmos/cosmos-sdk/types" - //"github.com/cosmos/cosmos-sdk/wire" //"github.com/cosmos/cosmos-sdk/x/auth" + "github.com/kava-labs/kava/internal/x/paychan" ) // list of functions that return pointers to cobra commands @@ -24,50 +25,178 @@ import ( // create state update (sender signs) (just a half signed close tx, (json encoded?)) // close paychan (receiver signs) (provide state update as arg) -// example from x/auth -/* -func GetAccountCmd(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder) *cobra.Command { - return &cobra.Command{ - Use: "account [address]", - Short: "Query account balance", - Args: cobra.ExactArgs(1), +func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { + flagTo := "to" + flagAmount := "amount" + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new payment channel", + Long: "Create a new payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the payment channel.", + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - - // find the key to look up the account - addr := args[0] - - key, err := sdk.GetAccAddressBech32(addr) - if err != nil { - return err - } - - // perform query + // get args: from, to, amount + // create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() - res, err := ctx.Query(auth.AddressStoreKey(key), storeName) + // ChainID: chainID, + // Height: viper.GetInt64(client.FlagHeight), + // Gas: viper.GetInt64(client.FlagGas), + // TrustNode: viper.GetBool(client.FlagTrustNode), + // FromAddressName: viper.GetString(client.FlagName), + // NodeURI: nodeURI, + // AccountNumber: viper.GetInt64(client.FlagAccountNumber), + // Sequence: viper.GetInt64(client.FlagSequence), + // Client: rpc, + // Decoder: nil, + // AccountStore: "acc", + + // Get sender adress + senderAddress, err := ctx.GetFromAddress() if err != nil { return err } - // Check if account was found - if res == nil { - return sdk.ErrUnknownAddress("No account with address " + addr + - " was found in the state.\nAre you sure there has been a transaction involving it?") - } - - // decode the value - account, err := decoder(res) + // Get receiver address + toStr := viper.GetString(flagTo) + receiverAddress, err := sdk.GetAccAddressBech32(toStr) if err != nil { return err } - // print out whole account - output, err := wire.MarshalJSONIndent(cdc, account) + // Get channel funding amount + amountString := viper.GetString(flagAmount) + amount, err := sdk.ParseCoins(amountString) if err != nil { return err } - fmt.Println(string(output)) - return nil + + // Create the create channel msg to send + // TODO write NewMsgCreate func? + msg := paychan.MsgCreate{ + sender: senderAddress, + receiver: receiverAddress, + amount: amount, + } + // Build and sign the transaction, then broadcast to Tendermint + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) }, } + cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") + cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + return cmd +} + +func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { + flagId := "id" + flagTo := "to" + flagAmount := "amount" + + cmd := &cobra.Command{ + Use: "localstate", + Short: "Create a payment channel claim", + Long: "", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // sender(name) receiver id + + // create a "client context" stuct populated with info from common flags + ctx := context.NewCoreContextFromViper() + // Get sender adress + senderAddress, err := ctx.GetFromAddress() + if err != nil { + return err + } + // Get id + id := viper.GetInt(flagId) + // Get receiver address + toStr := viper.GetString(flagTo) + receiverAddress, err := sdk.GetAccAddressBech32(toStr) + if err != nil { + return err + } + // Get channel receiver amount + amountString := viper.GetString(flagAmount) + amount, err := sdk.ParseCoins(amountString) + if err != nil { + return err + } + + // create MsgClose + + msg := paychan.MsgClose{ + sender: senderAddress, + receiver: receiverAddress, + id: id, + receiverAmount: amount, + } + + // half sign it + + txBytes, err := EnsureSignBuild(ctx, ctx.FromAddressName, msg, cdc) + if err != nil { + return err + } + + // print it out + + fmt.Println(txBytes) + + }, + } + cmd.Flags().Int(flagId, 0, "ID of the payment channel.") + cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") + cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + return cmd +} + +// sign and build the transaction from the msg +func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { + + ctx, err = EnsureAccountNumber(ctx) + if err != nil { + return nil, err + } + // default to next sequence number if none provided + ctx, err = EnsureSequence(ctx) + if err != nil { + return nil, err + } + + passphrase, err := ctx.GetPassphraseFromStdin(name) + if err != nil { + return nil, err + } + + txBytes, err := ctx.SignAndBuild(name, passphrase, msg, cdc) + if err != nil { + return nil, err + } + + return txBytes +} + +func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { + flagId := "id" + flagTo := "to" + flagState := "state" + + cmd := &cobra.Command{ + Use: "close", + Short: "Close a payment channel", + Long: "", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // get sender, reciver, id + // get state + // sign the state tx with receiver + // broadcast to tendermint + }, + } + //cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") + //cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + return cmd } -*/ From e595382288d5c782bd78e6ccb4e950943d8941d1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 14 Jul 2018 23:27:56 +0100 Subject: [PATCH 12/30] tidy cli commands --- cmd/kvcli/main.go | 15 +- internal/x/paychan/client/cmd/cmd.go | 257 ++++++++++++++++++--------- internal/x/paychan/handler.go | 4 +- internal/x/paychan/types.go | 76 ++++---- 4 files changed, 231 insertions(+), 121 deletions(-) diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 90aa90e8..4074a523 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -17,6 +17,7 @@ import ( bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" //ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli" //stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli" + paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cli" "github.com/kava-labs/kava/internal/app" "github.com/kava-labs/kava/internal/lcd" @@ -57,7 +58,6 @@ func main() { rootCmd.AddCommand( client.PostCommands( // this just wraps the input cmds with common flags bankcmd.SendTxCmd(cdc), - // paychan commands... //ibccmd.IBCTransferCmd(cdc), //ibccmd.IBCRelayCmd(cdc), //stakecmd.GetCmdCreateValidator(cdc), @@ -66,6 +66,19 @@ func main() { //stakecmd.GetCmdUnbond(cdc), )...) + paychanCmd := &cobra.Command{ + Use: "paychan", + Short: "Payment channel subcommands", + } + stakeCmd.AddCommand( + client.PostCommands( + paychancmd.CreatePaychanCmd(cdc), + paychancmd.GenerateNewStateCmd(cdc), + paychancmd.ClosePaychanCmd(cdc), + )...) + rootCmd.AddCommand( + paychanCmd, + ) // add proxy, version and key info rootCmd.AddCommand( client.LineBreak, diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 0c704389..398dc2df 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -1,30 +1,37 @@ package cli import ( + "encoding/base64" "fmt" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/keys" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" - "github.com/spf13/cobra" - //"github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/kava-labs/kava/internal/x/paychan" ) // list of functions that return pointers to cobra commands // No local storage needed for cli acting as a sender +// Currently minimum set of cli commands are implemented: +// create paychan - create and fund +// generate new paychan state - print a half signed close tx (sender signs) +// close paychan - close using state (receiver signs) + +// Future cli commands: // create paychan // close paychan // get paychan(s) // send paychan payment // get balance from receiver -// minimum -// create paychan (sender signs) -// create state update (sender signs) (just a half signed close tx, (json encoded?)) -// close paychan (receiver signs) (provide state update as arg) - func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" flagAmount := "amount" @@ -35,20 +42,9 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { Long: "Create a new payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the payment channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // get args: from, to, amount - // create a "client context" stuct populated with info from common flags + + // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() - // ChainID: chainID, - // Height: viper.GetInt64(client.FlagHeight), - // Gas: viper.GetInt64(client.FlagGas), - // TrustNode: viper.GetBool(client.FlagTrustNode), - // FromAddressName: viper.GetString(client.FlagName), - // NodeURI: nodeURI, - // AccountNumber: viper.GetInt64(client.FlagAccountNumber), - // Sequence: viper.GetInt64(client.FlagSequence), - // Client: rpc, - // Decoder: nil, - // AccountStore: "acc", // Get sender adress senderAddress, err := ctx.GetFromAddress() @@ -73,16 +69,17 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { // Create the create channel msg to send // TODO write NewMsgCreate func? msg := paychan.MsgCreate{ - sender: senderAddress, - receiver: receiverAddress, - amount: amount, + Sender: senderAddress, + Receiver: receiverAddress, + Amount: amount, } - // Build and sign the transaction, then broadcast to Tendermint + // Build and sign the transaction, then broadcast to the blockchain res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) if err != nil { return err } fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil }, } cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") @@ -90,34 +87,37 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { return cmd } -func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { +func GenerateNewStateCmd(cdc *wire.Codec) *cobra.Command { flagId := "id" flagTo := "to" flagAmount := "amount" cmd := &cobra.Command{ - Use: "localstate", - Short: "Create a payment channel claim", - Long: "", + Use: "new-state", + Short: "Generate a new payment channel state.", + Long: "Generate a new state for an existing payment channel and print it out. The new state is represented as a half signed close transaction.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // sender(name) receiver id - // create a "client context" stuct populated with info from common flags + // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper() + // Get sender adress senderAddress, err := ctx.GetFromAddress() if err != nil { return err } - // Get id - id := viper.GetInt(flagId) + + // Get the paychan id + id := viper.GetInt64(flagId) + // Get receiver address toStr := viper.GetString(flagTo) receiverAddress, err := sdk.GetAccAddressBech32(toStr) if err != nil { return err } + // Get channel receiver amount amountString := viper.GetString(flagAmount) amount, err := sdk.ParseCoins(amountString) @@ -125,26 +125,26 @@ func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { return err } - // create MsgClose - + // create close paychan msg msg := paychan.MsgClose{ - sender: senderAddress, - receiver: receiverAddress, - id: id, - receiverAmount: amount, + Sender: senderAddress, + Receiver: receiverAddress, + Id: id, + ReceiverAmount: amount, } - // half sign it - + // Sign the msg as the sender txBytes, err := EnsureSignBuild(ctx, ctx.FromAddressName, msg, cdc) if err != nil { return err } - // print it out - - fmt.Println(txBytes) - + // Print out the signed msg + fmt.Println("txBytes:", txBytes) + encodedTxBytes := make([]byte, base64.StdEncoding.EncodedLen(len(txBytes))) + base64.StdEncoding.Encode(encodedTxBytes, txBytes) + fmt.Println("base64TxBytes:", encodedTxBytes) + return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") @@ -153,50 +153,147 @@ func CreateNewStateCmd(cdc *wire.Codec) *cobra.Command { return cmd } -// sign and build the transaction from the msg -func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { - - ctx, err = EnsureAccountNumber(ctx) - if err != nil { - return nil, err - } - // default to next sequence number if none provided - ctx, err = EnsureSequence(ctx) - if err != nil { - return nil, err - } - - passphrase, err := ctx.GetPassphraseFromStdin(name) - if err != nil { - return nil, err - } - - txBytes, err := ctx.SignAndBuild(name, passphrase, msg, cdc) - if err != nil { - return nil, err - } - - return txBytes -} - func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { - flagId := "id" - flagTo := "to" flagState := "state" cmd := &cobra.Command{ Use: "close", - Short: "Close a payment channel", - Long: "", + Short: "Close a payment channel, given a state", + Long: "Close an existing payment channel with a state received from a sender.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - // get sender, reciver, id - // get state - // sign the state tx with receiver - // broadcast to tendermint + ctx := context.NewCoreContextFromViper() + + // Get the sender-signed close tx + state := viper.GetString(flagState) + txBytes, err := base64.StdEncoding.DecodeString(state) + if err != nil { + return err + } + stdTx := auth.StdTx{} + cdc.UnmarshalBinary(txBytes, stdTx) + + // Sign close tx + + // ensure contxt has up to date account and sequence numbers + ctx, err = Ensure(ctx) + if err != nil { + return err + } + // Sign message (asks user for password) + _, sig, err := UserSignMsg(ctx, ctx.FromAddressName, stdTx.Msg) + if err != nil { + return err + } + + // Append signature to close tx + + stdTx.Signatures = append(stdTx.Signatures, sig) + // encode close tx + txBytes, err = cdc.MarshalBinary(stdTx) + if err != nil { + return err + } + + // Broadcast close tx to the blockchain + + res, err := ctx.BroadcastTx(txBytes) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil }, } - //cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") - //cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + cmd.Flags().String(flagState, "", "State received from sender.") return cmd } + +// HELPER FUNCTIONS +// This is a partial refactor of cosmos-sdk/client/context. +// Existing API was awkard to use for paychans. + +func Ensure(ctx context.CoreContext) (context.CoreContext, error) { + + ctx, err := context.EnsureAccountNumber(ctx) + if err != nil { + return ctx, err + } + // default to next sequence number if none provided + ctx, err = context.EnsureSequence(ctx) + if err != nil { + return ctx, err + } + return ctx, nil +} + +func UserSignMsg(ctx context.CoreContext, name string, msg sdk.Msg) (signMsg auth.StdSignMsg, stdSig auth.StdSignature, err error) { + + // TODO check how to handle non error return values on error. Returning empty versions doesn't seem right. + + passphrase, err := ctx.GetPassphraseFromStdin(name) + if err != nil { + return signMsg, stdSig, err + } + + // build the Sign Messsage from the Standard Message + chainID := ctx.ChainID + if chainID == "" { + return signMsg, stdSig, errors.Errorf("Chain ID required but not specified") + } + accnum := ctx.AccountNumber + sequence := ctx.Sequence + + signMsg = auth.StdSignMsg{ + ChainID: chainID, + AccountNumbers: []int64{accnum}, + Sequences: []int64{sequence}, + Msg: msg, + Fee: auth.NewStdFee(ctx.Gas, sdk.Coin{}), // TODO run simulate to estimate gas? + } + + keybase, err := keys.GetKeyBase() + if err != nil { + return signMsg, stdSig, err + } + + // sign and build + bz := signMsg.Bytes() + + sig, pubkey, err := keybase.Sign(name, passphrase, bz) + if err != nil { + return signMsg, stdSig, err + } + stdSig = auth.StdSignature{ + PubKey: pubkey, + Signature: sig, + AccountNumber: accnum, + Sequence: sequence, + } + + return signMsg, stdSig, nil +} + +func Build(cdc *wire.Codec, signMsg auth.StdSignMsg, sig auth.StdSignature) ([]byte, error) { + tx := auth.NewStdTx(signMsg.Msg, signMsg.Fee, []auth.StdSignature{sig}) + return cdc.MarshalBinary(tx) +} + +func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { + //Ensure context has up to date account and sequence numbers + ctx, err := Ensure(ctx) + if err != nil { + return nil, err + } + // Sign message (asks user for password) + signMsg, sig, err := UserSignMsg(ctx, name, msg) + if err != nil { + return nil, err + } + // Create tx and marshal + txBytes, err := Build(cdc, signMsg, sig) + if err != nil { + return nil, err + } + return txBytes, nil +} diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index 751fe36a..cf649cbb 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -25,7 +25,7 @@ func NewHandler(k Keeper) sdk.Handler { // Leaves validation to the keeper methods. func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.CreatePaychan(ctx, msg.sender, msg.receiver, msg.amount) + tags, err := k.CreatePaychan(ctx, msg.Sender, msg.Receiver, msg.Amount) if err != nil { return err.Result() } @@ -39,7 +39,7 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // Leaves validation to the keeper methods. func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { // TODO maybe remove tags for first version - tags, err := k.ClosePaychan(ctx, msg.sender, msg.receiver, msg.id, msg.receiverAmount) + tags, err := k.ClosePaychan(ctx, msg.Sender, msg.Receiver, msg.Id, msg.ReceiverAmount) if err != nil { return err.Result() } diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index dc7e5129..bb1c42b3 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -41,9 +41,9 @@ type Paychan struct { // A message to create a payment channel. type MsgCreate struct { // maybe just wrap a paychan struct - sender sdk.Address - receiver sdk.Address - amount sdk.Coins + Sender sdk.Address + Receiver sdk.Address + Amount sdk.Coins } // Create a new message. @@ -66,9 +66,9 @@ func (msg MsgCreate) GetSignBytes() []byte { ReceiverAddr string `json:"receiver_addr"` Amount sdk.Coins `json:"amount"` }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), - Amount: msg.amount, + SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), + Amount: msg.Amount, }) if err != nil { panic(err) @@ -81,21 +81,21 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { // Validate without external information (such as account balance) // check if all fields present / not 0 valued - if len(msg.sender) == 0 { - return sdk.ErrInvalidAddress(msg.sender.String()) + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) } - if len(msg.receiver) == 0 { - return sdk.ErrInvalidAddress(msg.receiver.String()) + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) } - if len(msg.amount) == 0 { - return sdk.ErrInvalidCoins(msg.amount.String()) + if len(msg.Amount) == 0 { + return sdk.ErrInvalidCoins(msg.Amount.String()) } // Check if coins are sorted, non zero, non negative - if !msg.amount.IsValid() { - return sdk.ErrInvalidCoins(msg.amount.String()) + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) } - if !msg.amount.IsPositive() { - return sdk.ErrInvalidCoins(msg.amount.String()) + if !msg.Amount.IsPositive() { + return sdk.ErrInvalidCoins(msg.Amount.String()) } // TODO check if Address valid? return nil @@ -103,17 +103,17 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { func (msg MsgCreate) GetSigners() []sdk.Address { // Only sender must sign to create a paychan - return []sdk.Address{msg.sender} + return []sdk.Address{msg.Sender} } // A message to close a payment channel. type MsgClose struct { // have to include sender and receiver in msg explicitly (rather than just universal paychanID) // this gives ability to verify signatures with no external information - sender sdk.Address - receiver sdk.Address - id int64 // TODO is another int type better? - receiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance + Sender sdk.Address + Receiver sdk.Address + Id int64 // TODO is another int type better? + ReceiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance } // func (msg MsgClose) NewMsgClose(sender sdk.Address, receiver sdk.Address, id integer, receiverAmount sdk.Coins) MsgClose { @@ -135,10 +135,10 @@ func (msg MsgClose) GetSignBytes() []byte { Id int64 `json:"id"` ReceiverAmount sdk.Coins `json:"receiver_amount"` }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.receiver), - Id: msg.id, - ReceiverAmount: msg.receiverAmount, + SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), + ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), + Id: msg.Id, + ReceiverAmount: msg.ReceiverAmount, }) if err != nil { panic(err) @@ -148,25 +148,25 @@ func (msg MsgClose) GetSignBytes() []byte { func (msg MsgClose) ValidateBasic() sdk.Error { // check if all fields present / not 0 valued - if len(msg.sender) == 0 { - return sdk.ErrInvalidAddress(msg.sender.String()) + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) } - if len(msg.receiver) == 0 { - return sdk.ErrInvalidAddress(msg.receiver.String()) + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) } - if len(msg.receiverAmount) == 0 { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if len(msg.ReceiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } // check id ≥ 0 - if msg.id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.id))) // TODO implement custom errors + if msg.Id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors } // Check if coins are sorted, non zero, non negative - if !msg.receiverAmount.IsValid() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !msg.ReceiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } - if !msg.receiverAmount.IsPositive() { - return sdk.ErrInvalidCoins(msg.receiverAmount.String()) + if !msg.ReceiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } // TODO check if Address valid? return nil @@ -174,5 +174,5 @@ func (msg MsgClose) ValidateBasic() sdk.Error { func (msg MsgClose) GetSigners() []sdk.Address { // Both sender and receiver must sign in order to close a channel - return []sdk.Address{msg.sender, msg.receiver} + return []sdk.Address{msg.Sender, msg.Receiver} } From 68b9591042fec0987d7ba4c9cf8cfdfa09593d2e Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 15 Jul 2018 12:41:55 +0100 Subject: [PATCH 13/30] complete basic keeper tests --- internal/x/paychan/keeper.go | 20 ++--- internal/x/paychan/keeper_test.go | 130 ++++++++---------------------- internal/x/paychan/types.go | 8 +- 3 files changed, 49 insertions(+), 109 deletions(-) diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 1fc12b6a..11a50db2 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -57,7 +57,7 @@ func (k Keeper) setPaychan(ctx sdk.Context, pych Paychan) { // marshal bz := k.cdc.MustMarshalBinary(pych) // panics if something goes wrong // write to db - pychKey := paychanKey(pych.sender, pych.receiver, pych.id) + pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id) store.Set(pychKey, bz) // panics if something goes wrong } @@ -92,8 +92,8 @@ func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk. // sender has enough coins - done in Subtract method // TODO check if sender and receiver different? - // Calculate next id (num existing paychans plus 1) - id := int64(len(k.GetPaychans(sender, receiver)) + 1) // TODO check for overflow? + // Calculate next id (just num of existing paychans - zero indexed) + id := int64(len(k.GetPaychans(sender, receiver))) // subtract coins from sender _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, amount) if err != nil { @@ -101,10 +101,10 @@ func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk. } // create new Paychan struct pych := Paychan{ - sender: sender, - receiver: receiver, - id: id, - balance: amount, + Sender: sender, + Receiver: receiver, + Id: id, + Balance: amount, } // save to db k.setPaychan(ctx, pych) @@ -145,10 +145,10 @@ func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.A return nil, sdk.ErrUnknownAddress("paychan not found") // TODO implement custom errors } // compute coin distribution - senderAmount := pych.balance.Minus(receiverAmount) // Minus sdk.Coins method + senderAmount := pych.Balance.Minus(receiverAmount) // Minus sdk.Coins method // check that receiverAmt not greater than paychan balance if !senderAmount.IsNotNegative() { - return nil, sdk.ErrInsufficientFunds(pych.balance.String()) + return nil, sdk.ErrInsufficientFunds(pych.Balance.String()) } // add coins to sender // creating account if it doesn't exist @@ -157,7 +157,7 @@ func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.A k.coinKeeper.AddCoins(ctx, receiver, receiverAmount) // delete paychan from db - pychKey := paychanKey(pych.sender, pych.receiver, pych.id) + pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id) store.Delete(pychKey) // TODO create tags diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index b965f9de..8ba25b79 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -51,6 +51,7 @@ func setupCodec() *wire.Codec { func TestKeeper(t *testing.T) { // Setup + // create multistore and key ms, authKey, paychanKey := setupMultiStore() @@ -77,14 +78,14 @@ func TestKeeper(t *testing.T) { // Test paychan can be set and get p := Paychan{ - sender: sdk.Address([]byte("senderAddress")), - receiver: sdk.Address([]byte("receiverAddress")), - id: 0, - balance: sdk.Coins{{"KVA", 100}}, + Sender: sdk.Address([]byte("senderAddress")), + Receiver: sdk.Address([]byte("receiverAddress")), + Id: 0, + Balance: sdk.Coins{{"KVA", 100}}, } paychanKeeper.setPaychan(ctx, p) - _, exists = paychanKeeper.GetPaychan(ctx, p.sender, p.receiver, p.id) + _, exists = paychanKeeper.GetPaychan(ctx, p.Sender, p.Receiver, p.Id) if !exists { t.Error("payment channel not found") } @@ -102,100 +103,39 @@ func TestKeeper(t *testing.T) { t.Error("unexpected error created payment channel", err) } - p, _ = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) - if !p.balance.IsEqual(balance) { - t.Error("payment channel balance incorrect", p.balance, balance) + p, exists = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) + if !exists { + t.Error("payment channel missing") + } + if !p.Balance.IsEqual(balance) { + t.Error("payment channel balance incorrect", p.Balance, balance) } expectedNewSenderFunds := senderFunds.Minus(balance) if !coinKeeper.GetCoins(ctx, senderAddress).IsEqual(expectedNewSenderFunds) { t.Error("sender has incorrect balance after paychan creation") } + // Test close paychan under normal conditions + senderFunds = coinKeeper.GetCoins(ctx, senderAddress) + receiverAmount := sdk.Coins{{"KVA", 9}} + _, err = paychanKeeper.ClosePaychan(ctx, senderAddress, receiverAddress, 0, receiverAmount) + if err != nil { + t.Error("unexpected error closing payment channel", err) + } + // paychan shouldn't exist + _, exists = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) + if exists { + t.Error("payment channel should not exist") + } + // sender's funds should have increased + expectedNewSenderFunds = senderFunds.Plus(balance.Minus(receiverAmount)) + if !coinKeeper.GetCoins(ctx, senderAddress).IsEqual(expectedNewSenderFunds) { + t.Error("sender has incorrect balance after paychan creation", expectedNewSenderFunds) + } + // receiver's funds should have increased + expectedNewReceiverFunds := receiverAmount // started at zero + if !coinKeeper.GetCoins(ctx, receiverAddress).IsEqual(expectedNewReceiverFunds) { + t.Error("receiver has incorrect balance after paychan creation") + } + } - -// example from x/bank - -//func TestKeeper(t *testing.T) { -// ms, authKey := setupMultiStore() - -// cdc := wire.NewCodec() -// auth.RegisterBaseAccount(cdc) - -// ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) -// accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) -// coinKeeper := NewKeeper(accountMapper) - -// addr := sdk.Address([]byte("addr1")) -// addr2 := sdk.Address([]byte("addr2")) -// addr3 := sdk.Address([]byte("addr3")) -// acc := accountMapper.NewAccountWithAddress(ctx, addr) - -// // Test GetCoins/SetCoins -// accountMapper.SetAccount(ctx, acc) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{})) - -// coinKeeper.SetCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) - -// // Test HasCoins -// assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 10}})) -// assert.True(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 5}})) -// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"foocoin", 15}})) -// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})) - -// // Test AddCoins -// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"foocoin", 15}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 25}})) - -// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 15}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 15}, {"foocoin", 25}})) - -// // Test SubtractCoins -// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"foocoin", 10}}) -// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) - -// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}})) - -// coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 10}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 15}})) -// assert.False(t, coinKeeper.HasCoins(ctx, addr, sdk.Coins{{"barcoin", 1}})) - -// // Test SendCoins -// coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 5}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) - -// _, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}}) -// assert.Implements(t, (*sdk.Error)(nil), err2) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}})) - -// coinKeeper.AddCoins(ctx, addr, sdk.Coins{{"barcoin", 30}}) -// coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"barcoin", 10}, {"foocoin", 5}}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 5}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 10}})) - -// // Test InputOutputCoins -// input1 := NewInput(addr2, sdk.Coins{{"foocoin", 2}}) -// output1 := NewOutput(addr, sdk.Coins{{"foocoin", 2}}) -// coinKeeper.InputOutputCoins(ctx, []Input{input1}, []Output{output1}) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 20}, {"foocoin", 7}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 8}})) - -// inputs := []Input{ -// NewInput(addr, sdk.Coins{{"foocoin", 3}}), -// NewInput(addr2, sdk.Coins{{"barcoin", 3}, {"foocoin", 2}}), -// } - -// outputs := []Output{ -// NewOutput(addr, sdk.Coins{{"barcoin", 1}}), -// NewOutput(addr3, sdk.Coins{{"barcoin", 2}, {"foocoin", 5}}), -// } -// coinKeeper.InputOutputCoins(ctx, inputs, outputs) -// assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 21}, {"foocoin", 4}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"barcoin", 7}, {"foocoin", 6}})) -// assert.True(t, coinKeeper.GetCoins(ctx, addr3).IsEqual(sdk.Coins{{"barcoin", 2}, {"foocoin", 5}})) - -//} diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index bb1c42b3..832495d3 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -9,10 +9,10 @@ import ( // Used to represent paychan in keeper module and to serialize. // probably want to convert this to a general purpose "state" type Paychan struct { - sender sdk.Address - receiver sdk.Address - id int64 - balance sdk.Coins + Sender sdk.Address + Receiver sdk.Address + Id int64 + Balance sdk.Coins } // Message Types From 266d61eb4b5cc1b9e0a5117c507d3676a7d90427 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 15 Jul 2018 15:08:08 +0100 Subject: [PATCH 14/30] add paychan module to app --- cmd/kvcli/main.go | 4 ++-- internal/app/app.go | 11 +++++++++-- internal/x/paychan/client/cmd/cmd.go | 18 +++++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 4074a523..8ec4e22a 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -17,7 +17,7 @@ import ( bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" //ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli" //stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli" - paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cli" + paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cmd" "github.com/kava-labs/kava/internal/app" "github.com/kava-labs/kava/internal/lcd" @@ -70,7 +70,7 @@ func main() { Use: "paychan", Short: "Payment channel subcommands", } - stakeCmd.AddCommand( + paychanCmd.AddCommand( client.PostCommands( paychancmd.CreatePaychanCmd(cdc), paychancmd.GenerateNewStateCmd(cdc), diff --git a/internal/app/app.go b/internal/app/app.go index e62665db..2125a3bd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -18,6 +18,7 @@ import ( //"github.com/cosmos/cosmos-sdk/x/slashing" //"github.com/cosmos/cosmos-sdk/x/stake" "github.com/kava-labs/kava/internal/types" + "github.com/kava-labs/kava/internal/x/paychan" ) const ( @@ -32,6 +33,7 @@ type KavaApp struct { // keys to access the substores keyMain *sdk.KVStoreKey keyAccount *sdk.KVStoreKey + keyPaychan *sdk.KVStoreKey //keyIBC *sdk.KVStoreKey //keyStake *sdk.KVStoreKey //keySlashing *sdk.KVStoreKey @@ -40,6 +42,7 @@ type KavaApp struct { accountMapper auth.AccountMapper feeCollectionKeeper auth.FeeCollectionKeeper coinKeeper bank.Keeper + paychanKeeper paychan.Keeper //ibcMapper ibc.Mapper //stakeKeeper stake.Keeper //slashingKeeper slashing.Keeper @@ -56,6 +59,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB) *KavaApp { cdc: cdc, keyMain: sdk.NewKVStoreKey("main"), keyAccount: sdk.NewKVStoreKey("acc"), + keyPaychan: sdk.NewKVStoreKey("paychan"), //keyIBC: sdk.NewKVStoreKey("ibc"), //keyStake: sdk.NewKVStoreKey("stake"), //keySlashing: sdk.NewKVStoreKey("slashing"), @@ -70,6 +74,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB) *KavaApp { // add accountMapper/handlers app.coinKeeper = bank.NewKeeper(app.accountMapper) + app.paychanKeeper = paychan.NewKeeper(app.cdc, app.keyPaychan, app.coinKeeper) //app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace)) //app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace)) //app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace)) @@ -77,7 +82,8 @@ func NewKavaApp(logger log.Logger, db dbm.DB) *KavaApp { // register message routes app.Router(). AddRoute("auth", auth.NewHandler(app.accountMapper)). - AddRoute("bank", bank.NewHandler(app.coinKeeper)) + AddRoute("bank", bank.NewHandler(app.coinKeeper)). + AddRoute("paychan", paychan.NewHandler(app.paychanKeeper)) //AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)). //AddRoute("stake", stake.NewHandler(app.stakeKeeper)) @@ -86,7 +92,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB) *KavaApp { app.SetBeginBlocker(app.BeginBlocker) app.SetEndBlocker(app.EndBlocker) app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper)) - app.MountStoresIAVL(app.keyMain, app.keyAccount) //, app.keyIBC, app.keyStake, app.keySlashing) + app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyPaychan) //, app.keyIBC, app.keyStake, app.keySlashing) err := app.LoadLatestVersion(app.keyMain) if err != nil { cmn.Exit(err.Error()) @@ -100,6 +106,7 @@ func MakeCodec() *wire.Codec { wire.RegisterCrypto(cdc) // Register crypto. sdk.RegisterWire(cdc) // Register Msgs bank.RegisterWire(cdc) + paychan.RegisterWire(cdc) //stake.RegisterWire(cdc) //slashing.RegisterWire(cdc) //ibc.RegisterWire(cdc) diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 398dc2df..dd4ccbfc 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -13,6 +13,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/kava-labs/kava/internal/x/paychan" ) @@ -44,7 +45,7 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags - ctx := context.NewCoreContextFromViper() + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // Get sender adress senderAddress, err := ctx.GetFromAddress() @@ -95,12 +96,12 @@ func GenerateNewStateCmd(cdc *wire.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "new-state", Short: "Generate a new payment channel state.", - Long: "Generate a new state for an existing payment channel and print it out. The new state is represented as a half signed close transaction.", + Long: "Generate a new state for an existing payment channel and print it out. The new state is represented as a half signed close transaction, signed by the sender.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags - ctx := context.NewCoreContextFromViper() + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // Get sender adress senderAddress, err := ctx.GetFromAddress() @@ -141,8 +142,8 @@ func GenerateNewStateCmd(cdc *wire.Codec) *cobra.Command { // Print out the signed msg fmt.Println("txBytes:", txBytes) - encodedTxBytes := make([]byte, base64.StdEncoding.EncodedLen(len(txBytes))) - base64.StdEncoding.Encode(encodedTxBytes, txBytes) + //encodedTxBytes := make([]byte, base64.StdEncoding.EncodedLen(len(txBytes))) + encodedTxBytes := base64.StdEncoding.EncodeToString(txBytes) fmt.Println("base64TxBytes:", encodedTxBytes) return nil }, @@ -159,10 +160,10 @@ func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "close", Short: "Close a payment channel, given a state", - Long: "Close an existing payment channel with a state received from a sender.", + Long: "Close an existing payment channel with a state received from a sender. This signs it as the receiver before submitting to the blockchain.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewCoreContextFromViper() + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) // Get the sender-signed close tx state := viper.GetString(flagState) @@ -171,7 +172,7 @@ func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { return err } stdTx := auth.StdTx{} - cdc.UnmarshalBinary(txBytes, stdTx) + cdc.UnmarshalBinary(txBytes, &stdTx) // Sign close tx @@ -187,7 +188,6 @@ func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { } // Append signature to close tx - stdTx.Signatures = append(stdTx.Signatures, sig) // encode close tx txBytes, err = cdc.MarshalBinary(stdTx) From 2405614e3f897e855863c5fe77d1193cde1b2a11 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 17 Jul 2018 15:00:44 +0100 Subject: [PATCH 15/30] update readme --- internal/x/paychan/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index cad4cfea..68ec7016 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -1,4 +1,4 @@ -Paychan Sketch +Payment channel implementation sketch Simplifications: @@ -8,8 +8,8 @@ Simplifications: TODO - - fill in details - - add tests - - is errors.go needed? - - is wire.go needed? - - remove simplifications \ No newline at end of file + - fix issue with multisig accounts and sequence numbers + - create a nicer paychan store key for querying (and implement query) + - expand client code + - tidy up - add return tags + - start removing simplifications, refactor From 3549c754746b254ef711e724d1568656c43122e4 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 24 Aug 2018 19:18:41 -0400 Subject: [PATCH 16/30] initial refactor sketch --- internal/x/paychan/README.md | 7 +- internal/x/paychan/client/cmd/cmd.go | 2 + internal/x/paychan/client/rest/rest.go | 2 + internal/x/paychan/endblocker.go | 13 ++ internal/x/paychan/handler.go | 23 ++-- internal/x/paychan/keeper.go | 117 +++++++++++------- internal/x/paychan/types.go | 159 ++++++++++++------------- internal/x/paychan/wire.go | 2 + 8 files changed, 186 insertions(+), 139 deletions(-) create mode 100644 internal/x/paychan/endblocker.go diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 68ec7016..c3812ab3 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -4,12 +4,7 @@ Simplifications: - unidirectional paychans - no top ups or partial withdrawals (only opening and closing) - - no protection against fund lock up from dissapearing receiver TODO - - fix issue with multisig accounts and sequence numbers - - create a nicer paychan store key for querying (and implement query) - - expand client code - - tidy up - add return tags - - start removing simplifications, refactor + - chnge module name to "channel"? diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index dd4ccbfc..f262cbf8 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -33,6 +33,7 @@ import ( // send paychan payment // get balance from receiver +/* func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" flagAmount := "amount" @@ -297,3 +298,4 @@ func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wir } return txBytes, nil } +*/ diff --git a/internal/x/paychan/client/rest/rest.go b/internal/x/paychan/client/rest/rest.go index 3565d70a..d25e01cb 100644 --- a/internal/x/paychan/client/rest/rest.go +++ b/internal/x/paychan/client/rest/rest.go @@ -7,6 +7,7 @@ import ( //"github.com/cosmos/cosmos-sdk/wire" ) +/* // RegisterRoutes registers paychan-related REST handlers to a router func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { //r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, ctx)).Methods("POST") @@ -20,3 +21,4 @@ func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb // get balance from receiver // get balance from local storage // handle incoming payment +*/ diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go new file mode 100644 index 00000000..5a753a59 --- /dev/null +++ b/internal/x/paychan/endblocker.go @@ -0,0 +1,13 @@ +package paychan + +import () + +func EndBlocker(ctx sdk.Context k Keeper) sdk.Tags { + + // Iterate through submittedUpdates and for each + // if current block height >= executionDate + // k.CloseChannel(...) + + tags := sdk.NewTags() + return tags +} \ No newline at end of file diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index cf649cbb..d639f99a 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -12,8 +12,8 @@ func NewHandler(k Keeper) sdk.Handler { switch msg := msg.(type) { case MsgCreate: return handleMsgCreate(ctx, k, msg) - case MsgClose: - return handleMsgClose(ctx, k, msg) + case MsgSubmitUpdate: + return handleMsgSubmitUpdate(ctx, k, msg) default: errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name() return sdk.ErrUnknownRequest(errMsg).Result() @@ -21,11 +21,10 @@ func NewHandler(k Keeper) sdk.Handler { } } -// Handle CreateMsg. +// Handle MsgCreate // Leaves validation to the keeper methods. func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { - // TODO maybe remove tags for first version - tags, err := k.CreatePaychan(ctx, msg.Sender, msg.Receiver, msg.Amount) + tags, err := k.CreateChannel(ctx, msg.Participants[0], msg.Participants[len(msg.Participants)-1], msg.Coins) if err != nil { return err.Result() } @@ -35,15 +34,19 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { } } -// Handle CloseMsg. +// Handle MsgSubmitUpdate // Leaves validation to the keeper methods. -func handleMsgClose(ctx sdk.Context, k Keeper, msg MsgClose) sdk.Result { - // TODO maybe remove tags for first version - tags, err := k.ClosePaychan(ctx, msg.Sender, msg.Receiver, msg.Id, msg.ReceiverAmount) +func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.Result { + + // if only sender sig then + tags, err := k.InitChannelCloseBySender() + // else (if there are both) + tags, err := k.ChannelCloseByReceiver() + if err != nil { return err.Result() } - // These tags can be used to subscribe to channel closures + // These tags can be used by clients to subscribe to channel close attempts return sdk.Result{ Tags: tags, } diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 11a50db2..d3648884 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -8,20 +8,19 @@ import ( "github.com/cosmos/cosmos-sdk/x/bank" ) -// keeper of the paychan store +// Keeper of the paychan store // Handles validation internally. Does not rely on calling code to do validation. -// Aim to keep public methids safe, private ones not necessaily. +// Aim to keep public methods safe, private ones not necessaily. type Keeper struct { storeKey sdk.StoreKey cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper - // codespace - //codespace sdk.CodespaceType // ?? + // TODO investigate codespace + //codespace sdk.CodespaceType } // Called when creating new app. -//func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, codespace sdk.CodespaceType) Keeper { func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { keeper := Keeper{ storeKey: key, @@ -33,7 +32,7 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { } // bunch of business logic ... - +/* // Reteive a payment channel struct from the blockchain store. // They are indexed by a concatenation of sender address, receiver address, and an integer. func (k Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64) (Paychan, bool) { @@ -60,60 +59,91 @@ func (k Keeper) setPaychan(ctx sdk.Context, pych Paychan) { pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id) store.Set(pychKey, bz) // panics if something goes wrong } +*/ // Create a new payment channel and lock up sender funds. -func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, amount sdk.Coins) (sdk.Tags, sdk.Error) { - // TODO move validation somewhere nicer - // args present - if len(sender) == 0 { - return nil, sdk.ErrInvalidAddress(sender.String()) - } - if len(receiver) == 0 { - return nil, sdk.ErrInvalidAddress(receiver.String()) - } - if len(amount) == 0 { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - // Check if coins are sorted, non zero, positive - if !amount.IsValid() { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - if !amount.IsPositive() { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - // sender should exist already as they had to sign. - // receiver address exists. am is the account mapper in the coin keeper. - // TODO automatically create account if not present? - // TODO remove as account mapper not available to this pkg - //if k.coinKeeper.am.GetAccount(ctx, receiver) == nil { - // return nil, sdk.ErrUnknownAddress(receiver.String()) - //} +func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, coins sdk.Coins) (sdk.Tags, sdk.Error) { + // TODO do validation and maybe move somewhere nicer + /* + // args present + if len(sender) == 0 { + return nil, sdk.ErrInvalidAddress(sender.String()) + } + if len(receiver) == 0 { + return nil, sdk.ErrInvalidAddress(receiver.String()) + } + if len(amount) == 0 { + return nil, sdk.ErrInvalidCoins(amount.String()) + } + // Check if coins are sorted, non zero, positive + if !amount.IsValid() { + return nil, sdk.ErrInvalidCoins(amount.String()) + } + if !amount.IsPositive() { + return nil, sdk.ErrInvalidCoins(amount.String()) + } + // sender should exist already as they had to sign. + // receiver address exists. am is the account mapper in the coin keeper. + // TODO automatically create account if not present? + // TODO remove as account mapper not available to this pkg + //if k.coinKeeper.am.GetAccount(ctx, receiver) == nil { + // return nil, sdk.ErrUnknownAddress(receiver.String()) + //} - // sender has enough coins - done in Subtract method - // TODO check if sender and receiver different? + // sender has enough coins - done in Subtract method + // TODO check if sender and receiver different? + */ - // Calculate next id (just num of existing paychans - zero indexed) - id := int64(len(k.GetPaychans(sender, receiver))) + // Calculate next id + id := k.getNewChannelID(ctx) // subtract coins from sender - _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, amount) + _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins) if err != nil { return nil, err } // create new Paychan struct - pych := Paychan{ - Sender: sender, - Receiver: receiver, - Id: id, - Balance: amount, + channel := Channel{ + ID: id + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, } // save to db - k.setPaychan(ctx, pych) + k.setChannel(ctx, channel) // TODO create tags //tags := sdk.NewTags() return tags, err } +/* This is how gov manages creating unique IDs. Needs to be deterministic - can't use UUID +func (keeper Keeper) getNewChannelID(ctx sdk.Context) (channelID int64, err sdk.Error) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyNextProposalID) + if bz == nil { + return -1, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") + } + keeper.cdc.MustUnmarshalBinary(bz, &proposalID) + bz = keeper.cdc.MustMarshalBinary(proposalID + 1) + store.Set(KeyNextProposalID, bz) + return proposalID, nil +*/ + +func (k Keeper) ChannelCloseByReceiver() () { + // Validate inputs + // k.closeChannel +} + +func (k Keeper) InitChannelCloseBySender() () { + // Validate inputs + // Create SubmittedUpdate from Update and add to queue +} + +func (k Keeper) closeChannel() () { + // Remove corresponding SubmittedUpdate from queue (if it exist) + // Add coins to sender and receiver + // Delete Channel +} +/* // Close a payment channel and distribute funds to participants. func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64, receiverAmount sdk.Coins) (sdk.Tags, sdk.Error) { if len(sender) == 0 { @@ -190,3 +220,4 @@ func (k Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan } // maybe getAllPaychans(sender sdk.address) []Paychan +*/ diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 832495d3..bbe6738b 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -5,81 +5,91 @@ import ( "strconv" ) -// Paychan Type -// Used to represent paychan in keeper module and to serialize. -// probably want to convert this to a general purpose "state" -type Paychan struct { - Sender sdk.Address - Receiver sdk.Address - Id int64 - Balance sdk.Coins +/* CHANNEL TYPES */ + +// Used to represent a channel in the keeper module. +// Participants is limited to two as currently these are unidirectional channels. +// Last participant is designated as receiver. +type Channel struct { + ID int64 + Participants [2]sdk.AccAddress + Coins sdk.Coins } -// Message Types +// The data that is passed between participants as payments, and submitted to the blockchain to close a channel. +type Update struct { + ChannelID int64 + CoinsUpdate //TODO type + Sequence int64 + sig // TODO type, only sender needs to sign +} -// Message implement the sdk.Msg interface: +// An update that has been submitted to the blockchain, but not yet acted on. +type SubmittedUpdate { + Update + executionDate int64 // BlockHeight +} -// type Msg interface { +/* MESSAGE TYPES */ +/* +Message implement the sdk.Msg interface: +type Msg interface { -// // Return the message type. -// // Must be alphanumeric or empty. -// Type() string + // Return the message type. + // Must be alphanumeric or empty. + Type() string -// // Get the canonical byte representation of the Msg. -// GetSignBytes() []byte + // Get the canonical byte representation of the Msg. + GetSignBytes() []byte -// // ValidateBasic does a simple validation check that -// // doesn't require access to any other information. -// ValidateBasic() Error + // ValidateBasic does a simple validation check that + // doesn't require access to any other information. + ValidateBasic() Error -// // Signers returns the addrs of signers that must sign. -// // CONTRACT: All signatures must be present to be valid. -// // CONTRACT: Returns addrs in some deterministic order. -// GetSigners() []Address -// } + // Signers returns the addrs of signers that must sign. + // CONTRACT: All signatures must be present to be valid. + // CONTRACT: Returns addrs in some deterministic order. + GetSigners() []Address +} +*/ // A message to create a payment channel. type MsgCreate struct { - // maybe just wrap a paychan struct - Sender sdk.Address - Receiver sdk.Address - Amount sdk.Coins + Participants [2]sdk.AccAddress + Coins sdk.Coins } -// Create a new message. -// Called in client code when constructing transaction from cli args to send to the network. -// maybe just a placeholder for more advanced future functionality? -// func (msg CreatMsg) NewMsgCreate(sender sdk.Address, receiver sdk.Address, amount sdk.Coins) MsgCreate { -// return MsgCreate{ -// sender -// receiver -// amount -// } -// } +//Create a new message. +/* +Called in client code when constructing transaction from cli args to send to the network. +maybe just a placeholder for more advanced future functionality? +func (msg CreatMsg) NewMsgCreate(sender sdk.Address, receiver sdk.Address, amount sdk.Coins) MsgCreate { + return MsgCreate{ + sender + receiver + amount + } +} +*/ func (msg MsgCreate) Type() string { return "paychan" } func (msg MsgCreate) GetSignBytes() []byte { // TODO create msgCdc in wire.go - b, err := msgCdc.MarshalJSON(struct { - SenderAddr string `json:"sender_addr"` - ReceiverAddr string `json:"receiver_addr"` - Amount sdk.Coins `json:"amount"` - }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), - Amount: msg.Amount, - }) + bz, err := msgCdc.Marshal(msg) if err != nil { panic(err) } - return b + return MustSortJSON(bz) } func (msg MsgCreate) ValidateBasic() sdk.Error { // Validate msg as an optimisation to avoid all validation going to keeper. It's run before the sigs are checked by the auth module. // Validate without external information (such as account balance) + //TODO implement + + /* // check if all fields present / not 0 valued if len(msg.Sender) == 0 { return sdk.ErrInvalidAddress(msg.Sender.String()) @@ -98,55 +108,42 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { return sdk.ErrInvalidCoins(msg.Amount.String()) } // TODO check if Address valid? + */ return nil } func (msg MsgCreate) GetSigners() []sdk.Address { // Only sender must sign to create a paychan - return []sdk.Address{msg.Sender} + return []sdk.AccAddress{msg.Participants[0]} // select sender address } // A message to close a payment channel. -type MsgClose struct { - // have to include sender and receiver in msg explicitly (rather than just universal paychanID) - // this gives ability to verify signatures with no external information - Sender sdk.Address - Receiver sdk.Address - Id int64 // TODO is another int type better? - ReceiverAmount sdk.Coins // amount the receiver should get - sender amount implicit with paychan balance +type MsgSubmitUpdate struct { + Update + // might need a "signer" to be able to say who is signing this as either can or not } -// func (msg MsgClose) NewMsgClose(sender sdk.Address, receiver sdk.Address, id integer, receiverAmount sdk.Coins) MsgClose { -// return MsgClose{ -// sender -// receiver -// id -// receiverAmount +// func (msg MsgSubmitUpdate) NewMsgSubmitUpdate(update Update) MsgSubmitUpdate { +// return MsgSubmitUpdate{ +// update // } // } -func (msg MsgClose) Type() string { return "paychan" } +func (msg MsgSubmitUpdate) Type() string { return "paychan" } -func (msg MsgClose) GetSignBytes() []byte { +func (msg MsgSubmitUpdate) GetSignBytes() []byte { // TODO create msgCdc in wire.go - b, err := msgCdc.MarshalJSON(struct { - SenderAddr string `json:"sender_addr"` - ReceiverAddr string `json:"receiver_addr"` - Id int64 `json:"id"` - ReceiverAmount sdk.Coins `json:"receiver_amount"` - }{ - SenderAddr: sdk.MustBech32ifyAcc(msg.Sender), - ReceiverAddr: sdk.MustBech32ifyAcc(msg.Receiver), - Id: msg.Id, - ReceiverAmount: msg.ReceiverAmount, - }) + bz, err := msgCdc.Marshal(msg) if err != nil { panic(err) } - return b + return MustSortJSON(bz) } -func (msg MsgClose) ValidateBasic() sdk.Error { +func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error { + + // TODO implement + /* // check if all fields present / not 0 valued if len(msg.Sender) == 0 { return sdk.ErrInvalidAddress(msg.Sender.String()) @@ -169,10 +166,12 @@ func (msg MsgClose) ValidateBasic() sdk.Error { return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) } // TODO check if Address valid? + */ return nil } -func (msg MsgClose) GetSigners() []sdk.Address { - // Both sender and receiver must sign in order to close a channel - return []sdk.Address{msg.Sender, msg.Receiver} +func (msg MsgSubmitUpdate) GetSigners() []sdk.Address { + // Signing not strictly necessary as signatures contained within the channel update. + // TODO add signature by submitting address + return []sdk.Address{} } diff --git a/internal/x/paychan/wire.go b/internal/x/paychan/wire.go index dfb2d468..9d9e5eb3 100644 --- a/internal/x/paychan/wire.go +++ b/internal/x/paychan/wire.go @@ -4,6 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" ) +/* func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(MsgCreate{}, "paychan/MsgCreate", nil) cdc.RegisterConcrete(MsgClose{}, "paychan/MsgClose", nil) @@ -16,3 +17,4 @@ func init() { // TODO is this needed? //wire.RegisterCrypto(msgCdc) } +*/ From a4baa34ee0003cd680c2ae2642c30ccfc1aa57be Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Aug 2018 17:58:58 -0400 Subject: [PATCH 17/30] fill out rough implementation --- internal/x/paychan/README.md | 3 + internal/x/paychan/endblocker.go | 19 ++- internal/x/paychan/handler.go | 13 +- internal/x/paychan/keeper.go | 268 +++++++++++++++++++++++++------ internal/x/paychan/types.go | 36 ++++- 5 files changed, 274 insertions(+), 65 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index c3812ab3..45222831 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -7,4 +7,7 @@ Simplifications: TODO + - error handling (getter setter return values? and what happens in failures) - chnge module name to "channel"? + - Find a better name for Queue - clarify distinction between int slice and abstract queue concept + - Do all the small functions need to be methods on the keeper or can they just be floating around? diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go index 5a753a59..74947561 100644 --- a/internal/x/paychan/endblocker.go +++ b/internal/x/paychan/endblocker.go @@ -2,12 +2,21 @@ package paychan import () -func EndBlocker(ctx sdk.Context k Keeper) sdk.Tags { +func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { - // Iterate through submittedUpdates and for each - // if current block height >= executionDate - // k.CloseChannel(...) + // Iterate through submittedUpdatesQueue + // TODO optimise so it doesn't pull every update from DB every block + var sUpdate SubmittedUpdate + q := k.getSubmittedUpdatesQueue(ctx) + for _, id := range q { + // close the channel if the update has reached its execution time. + // Using >= in case some are somehow missed. + sUpdate = k.getSubmittedUpdate(ctx, id) + if ctx.BlockHeight() >= sUpdate.ExecutionTime { + k.closeChannel(ctx, sUpdate.Update) + } + } tags := sdk.NewTags() return tags -} \ No newline at end of file +} diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index d639f99a..d442f7c1 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -38,10 +38,15 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // Leaves validation to the keeper methods. func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.Result { - // if only sender sig then - tags, err := k.InitChannelCloseBySender() - // else (if there are both) - tags, err := k.ChannelCloseByReceiver() + participants := k.getChannel(ctx, msg.Update.ChannelID).Participants + + // if only sender signed + if msg.submitter == participants[0] { + tags, err := k.InitCloseChannelBySender() + // else if receiver signed + } else if msg.submitter == participants[len(participants)-1] { + tags, err := k.CloseChannelByReceiver() + } if err != nil { return err.Result() diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index d3648884..dfcf99c6 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -31,38 +31,11 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { return keeper } -// bunch of business logic ... -/* -// Reteive a payment channel struct from the blockchain store. -// They are indexed by a concatenation of sender address, receiver address, and an integer. -func (k Keeper) GetPaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64) (Paychan, bool) { - // Return error as second argument instead of bool? - var pych Paychan - // load from DB - store := ctx.KVStore(k.storeKey) - bz := store.Get(paychanKey(sender, receiver, id)) - if bz == nil { - return pych, false - } - // unmarshal - k.cdc.MustUnmarshalBinary(bz, &pych) - // return - return pych, true -} +// ============================================== Main Business Logic -// Store payment channel struct in blockchain store. -func (k Keeper) setPaychan(ctx sdk.Context, pych Paychan) { - store := ctx.KVStore(k.storeKey) - // marshal - bz := k.cdc.MustMarshalBinary(pych) // panics if something goes wrong - // write to db - pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id) - store.Set(pychKey, bz) // panics if something goes wrong -} -*/ // Create a new payment channel and lock up sender funds. -func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, coins sdk.Coins) (sdk.Tags, sdk.Error) { +func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, coins sdk.Coins) (sdk.Tags, sdk.Error) { // TODO do validation and maybe move somewhere nicer /* // args present @@ -110,38 +83,229 @@ func (k Keeper) CreatePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk. // save to db k.setChannel(ctx, channel) - // TODO create tags - //tags := sdk.NewTags() + // TODO add to tags + return tags, err } -/* This is how gov manages creating unique IDs. Needs to be deterministic - can't use UUID -func (keeper Keeper) getNewChannelID(ctx sdk.Context) (channelID int64, err sdk.Error) { - store := ctx.KVStore(keeper.storeKey) - bz := store.Get(KeyNextProposalID) - if bz == nil { - return -1, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") + + +func (k Keeper) InitCloseChannelBySender(update Update) { + // This is roughly the default path for non unidirectional channels + + // TODO Validate update - e.g. check signed by sender + + q := k.getSubmittedUpdateQueue(ctx) + if q.Contains(update.ChannelID) { + // Someone has previously tried to update channel + existingSUpdate := k.getSubmittedUpdate(ctx, update.ChannelID) + k.addToSubmittedUpdateQueue(ctx, k.applyNewUpdate(existingSUpdate, update)) + } else { + // No one has tried to update channel. + submittedUpdate := SubmittedUpdate{ + Update: update + executionTime: ctx.BlockHeight()+ChannelDisputeTime //TODO check what exactly BlockHeight refers to + } + k.addToSubmittedUpdateQueue(ctx, submittedUpdate) } - keeper.cdc.MustUnmarshalBinary(bz, &proposalID) - bz = keeper.cdc.MustMarshalBinary(proposalID + 1) - store.Set(KeyNextProposalID, bz) - return proposalID, nil -*/ - -func (k Keeper) ChannelCloseByReceiver() () { - // Validate inputs - // k.closeChannel } -func (k Keeper) InitChannelCloseBySender() () { - // Validate inputs - // Create SubmittedUpdate from Update and add to queue +func (k Keeper) CloseChannelByReceiver(update Update) () { + // TODO Validate update + + // Check if there is an update in the queue already + q := k.getSubmittedUpdateQueue(ctx) + if q.Contains(update.ChannelID) { + // Someone has previously tried to update channel but receiver has final say + k.removeFromSubmittedUpdateQueue(ctx, update.ChannelID) + } + + k.closeChannel(ctx, update) } -func (k Keeper) closeChannel() () { - // Remove corresponding SubmittedUpdate from queue (if it exist) +// Main function that compare updates against each other. +// Pure function +func (k Keeper) applyNewUpdate(existingSUpdate, proposedUpdate) SubmittedUpdate { + var returnUpdate SubmittedUpdate + + if existingSUpdate.sequence > proposedUpdate.sequence { + // update accepted + returnUpdate = SubmittedUpdate{ + Update: proposedUpdate + ExecutionTime: existingSUpdate.ExecutionTime + } + } else { + // update rejected + returnUpdate = existingSUpdate + } + return returnUpdate +} + +func (k Keeper) closeChannel(ctx sdk.Context, update Update) { + channel := k.getChannel(ctx, update.ChannelID) + // Add coins to sender and receiver - // Delete Channel + for address, coins := range update.CoinsUpdate { + // TODO check somewhere if coins are not negative? + k.ck.AddCoins(ctx, address, coins) + } + + k.deleteChannel(ctx, update.ChannelID) +} + + + +// =========================================== QUEUE + + +func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { + // always overwrite prexisting values - leave paychan logic to higher levels + // get current queue + q := k.getSubmittedUpdateQueue(ctx) + // append ID to queue + if q.Contains(sUpdate.ChannelID)! { + q = append(q, sUpdate.ChannelID) + } + // set queue + k.setSubmittedUpdateQueue(ctx, q) + // store submittedUpdate + k.setSubmittedUpdate(ctx, sUpdate) +} +func (k Keeper) removeFromSubmittdUpdatesQueue(ctx sdk.Context, channelID) { + // get current queue + q := k.getSubmittedUpdateQueue(ctx) + // remove id + q.RemoveMatchingElements(channelID) + // set queue + k.setSubmittedUpdateQueue(ctx, q) + // delete submittedUpdate + k.deleteSubmittedUpdate(ctx, channelID) +} + +func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (Queue, bool) { + // load from DB + store := ctx.KVStore(k.storeKey) + bz := store.Get(k.getSubmittedUpdatesQueueKey()) + + var q Queue + if bz == nil { + return q, false + } + // unmarshal + k.cdc.MustUnmarshalBinary(bz, &q) + // return + return q, true +} +func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, q Queue) { + store := ctx.KVStore(k.storeKey) + // marshal + bz := k.cdc.MustMarshalBinary(q) + // write to db + key := k.getSubmittedUpdatesQueueKey() + store.Set(key, bz) +} +func (k Keeper) getSubmittedUpdatesQueueKey() []byte { + return []byte("submittedUpdatesQueue") +} + +// ============= SUBMITTED UPDATES +// These are keyed by the IDs of thei associated Channels +// This section deals with only setting and getting + +func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) { + + // load from DB + store := ctx.KVStore(k.storeKey) + bz := store.Get(k.getSubmittedUpdateKey(channelID)) + + var sUpdate SubmittedUpdate + if bz == nil { + return sUpdate, false + } + // unmarshal + k.cdc.MustUnmarshalBinary(bz, &sUpdate) + // return + return sUpdate, true +} + +// Store payment channel struct in blockchain store. +func (k Keeper) setSubmittedUpdate(ctx sdk.Context, sUpdate SubmittedUpdate) { + store := ctx.KVStore(k.storeKey) + // marshal + bz := k.cdc.MustMarshalBinary(sUpdate) // panics if something goes wrong + // write to db + key := k.getSubmittedUpdateKey(sUpdate.channelID) + store.Set(key, bz) // panics if something goes wrong +} + +func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ) { + store := ctx.KVStore(k.storeKey) + store.Delete(k.getSubmittedUpdateKey(channelID)) + // TODO does this have return values? What happens when key doesn't exist? +} +func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte { + return []byte(fmt.Sprintf("submittedUpdate:%d", channelID)) +} + + +// ========================================== CHANNELS + +// Reteive a payment channel struct from the blockchain store. +func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) { + + // load from DB + store := ctx.KVStore(k.storeKey) + bz := store.Get(k.getChannelKey(channelID)) + + var channel Channel + if bz == nil { + return channel, false + } + // unmarshal + k.cdc.MustUnmarshalBinary(bz, &channel) + // return + return channel, true +} + +// Store payment channel struct in blockchain store. +func (k Keeper) setChannel(ctx sdk.Context, channel Channel) { + store := ctx.KVStore(k.storeKey) + // marshal + bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong + // write to db + key := sdk.getChannelKey(channel.ID) + store.Set(key, bz) // panics if something goes wrong +} + +func (k Keeper) deleteChannel(ctx sdk.Context, channelID ) { + store := ctx.KVStore(k.storeKey) + store.Delete(k.getChannelKey(channelID)) + // TODO does this have return values? What happens when key doesn't exist? +} + +func (k Keeper) getNewChannelID(ctx sdk.Context) (int64, error) { + // get last channel ID + store := k.KVStore(k.storeKey) + bz := store.Get(k.getLastChannelIDKey()) + if bz == nil { + return nil, // TODO throw some error (assumes this has been initialized elsewhere) or just set to zero here + } + var lastID ChannelID + k.cdc.MustUnmarshalBinary(bz, &lastID) + // increment to create new one + newID := lastID+1 + bz = k.cdc.MustMarshalBinary(newID) + // set last channel id again + store.Set(k.getLastChannelIDKey(), bz) + // return + return newID +} + +func (k Keeper) getChannelKey(channelID ChannelID) []byte { + return []bytes(fmt.Sprintf("channel:%d", channelID)) +} +func (k Keeper) getLastChannelIDKey() []byte { + return []bytes("lastChannelID") } /* // Close a payment channel and distribute funds to participants. diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index bbe6738b..2b532c74 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -11,15 +11,17 @@ import ( // Participants is limited to two as currently these are unidirectional channels. // Last participant is designated as receiver. type Channel struct { - ID int64 + ID ChannelID Participants [2]sdk.AccAddress Coins sdk.Coins } +type ChannelID int64 // TODO should this be positive only + // The data that is passed between participants as payments, and submitted to the blockchain to close a channel. type Update struct { ChannelID int64 - CoinsUpdate //TODO type + CoinsUpdate map[sdk.AccAddress]sdk.Coins Sequence int64 sig // TODO type, only sender needs to sign } @@ -27,9 +29,35 @@ type Update struct { // An update that has been submitted to the blockchain, but not yet acted on. type SubmittedUpdate { Update - executionDate int64 // BlockHeight + executionTime int64 // BlockHeight } +type SubmittedUpdateQueue []ChannelID +// Check if value is in queue +func (suq SubmittedChannelID) Contains(channelID ChannelID) bool { + found := false + for _, id := range(suq) { + if id == channelID { + found = true + break + } + } + return found +} +// Remove all values from queue that match argument +func (suq SubmittedUpdateQueue) RemoveMatchingElements(channelID ChannelID) { + newSUQ := SubmittedUpdateQueue{} + + for _, id := range(suq) { + if id != channelID { + newSUQ = append(newSUQ, id) + } + } + suq = newSUQ +} + +var ChannelDisputeTime = 2000 // measured in blocks + /* MESSAGE TYPES */ /* Message implement the sdk.Msg interface: @@ -120,7 +148,7 @@ func (msg MsgCreate) GetSigners() []sdk.Address { // A message to close a payment channel. type MsgSubmitUpdate struct { Update - // might need a "signer" to be able to say who is signing this as either can or not + submitter sdk.AccAddress } // func (msg MsgSubmitUpdate) NewMsgSubmitUpdate(update Update) MsgSubmitUpdate { From c2680882609196cf7eeafb96331b6feda455bd62 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 27 Aug 2018 23:48:48 -0400 Subject: [PATCH 18/30] add error handling and fix compile errors --- internal/x/paychan/README.md | 5 +- internal/x/paychan/endblocker.go | 24 ++++- internal/x/paychan/handler.go | 14 ++- internal/x/paychan/keeper.go | 158 ++++++++++++++++++------------ internal/x/paychan/keeper_test.go | 2 + internal/x/paychan/types.go | 142 +++++++++++++-------------- internal/x/paychan/wire.go | 4 +- 7 files changed, 200 insertions(+), 149 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 45222831..d85b83b7 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -7,7 +7,10 @@ Simplifications: TODO - - error handling (getter setter return values? and what happens in failures) - chnge module name to "channel"? - Find a better name for Queue - clarify distinction between int slice and abstract queue concept - Do all the small functions need to be methods on the keeper or can they just be floating around? + - Tidy up - standardise var names, comments and method descriptions + - is having all the get functions return a bool if not found reasonable? + - any problem in signing your own address? + - Gas diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go index 74947561..cb793d2a 100644 --- a/internal/x/paychan/endblocker.go +++ b/internal/x/paychan/endblocker.go @@ -1,22 +1,36 @@ package paychan -import () +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { + var err sdk.Error + var channelTags sdk.Tags + tags := sdk.EmptyTags() // Iterate through submittedUpdatesQueue // TODO optimise so it doesn't pull every update from DB every block var sUpdate SubmittedUpdate - q := k.getSubmittedUpdatesQueue(ctx) + q, found := k.getSubmittedUpdatesQueue(ctx) + if !found { + panic("SubmittedUpdatesQueue not found.") + } for _, id := range q { // close the channel if the update has reached its execution time. // Using >= in case some are somehow missed. - sUpdate = k.getSubmittedUpdate(ctx, id) + sUpdate, found = k.getSubmittedUpdate(ctx, id) + if !found { + panic("can't find element in queue that should exist") + } if ctx.BlockHeight() >= sUpdate.ExecutionTime { - k.closeChannel(ctx, sUpdate.Update) + channelTags, err = k.closeChannel(ctx, sUpdate.Update) + if err != nil { + panic(err) + } + tags.AppendTags(channelTags) } } - tags := sdk.NewTags() return tags } diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index d442f7c1..c4d949a8 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -37,15 +37,19 @@ func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result { // Handle MsgSubmitUpdate // Leaves validation to the keeper methods. func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.Result { + var err sdk.Error + tags := sdk.EmptyTags() - participants := k.getChannel(ctx, msg.Update.ChannelID).Participants + // TODO refactor signer detection - move to keeper or find nicer setup + channel, _ := k.getChannel(ctx, msg.Update.ChannelID) + participants := channel.Participants // if only sender signed - if msg.submitter == participants[0] { - tags, err := k.InitCloseChannelBySender() + if reflect.DeepEqual(msg.submitter, participants[0]) { + tags, err = k.InitCloseChannelBySender(ctx, msg.Update) // else if receiver signed - } else if msg.submitter == participants[len(participants)-1] { - tags, err := k.CloseChannelByReceiver() + } else if reflect.DeepEqual(msg.submitter, participants[len(participants)-1]) { + tags, err = k.CloseChannelByReceiver(ctx, msg.Update) } if err != nil { diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index dfcf99c6..7bab151e 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -1,8 +1,7 @@ package paychan import ( - "strconv" - + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/bank" @@ -33,9 +32,8 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { // ============================================== Main Business Logic - // Create a new payment channel and lock up sender funds. -func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, coins sdk.Coins) (sdk.Tags, sdk.Error) { +func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver sdk.AccAddress, coins sdk.Coins) (sdk.Tags, sdk.Error) { // TODO do validation and maybe move somewhere nicer /* // args present @@ -67,18 +65,18 @@ func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.Address, receiver sdk. // TODO check if sender and receiver different? */ - // Calculate next id - id := k.getNewChannelID(ctx) // subtract coins from sender _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins) if err != nil { return nil, err } + // Calculate next id + id := k.getNewChannelID(ctx) // create new Paychan struct channel := Channel{ - ID: id - Participants: [2]sdk.AccAddress{sender, receiver}, - Coins: coins, + ID: id, + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, } // save to db k.setChannel(ctx, channel) @@ -88,51 +86,64 @@ func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.Address, receiver sdk. return tags, err } - - -func (k Keeper) InitCloseChannelBySender(update Update) { +func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // This is roughly the default path for non unidirectional channels // TODO Validate update - e.g. check signed by sender - q := k.getSubmittedUpdateQueue(ctx) + q, found := k.getSubmittedUpdatesQueue(ctx) + if !found { + panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors + } if q.Contains(update.ChannelID) { // Someone has previously tried to update channel - existingSUpdate := k.getSubmittedUpdate(ctx, update.ChannelID) - k.addToSubmittedUpdateQueue(ctx, k.applyNewUpdate(existingSUpdate, update)) - } else { - // No one has tried to update channel. - submittedUpdate := SubmittedUpdate{ - Update: update - executionTime: ctx.BlockHeight()+ChannelDisputeTime //TODO check what exactly BlockHeight refers to + existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID) + if !found { + panic("can't find element in queue that should exist") } - k.addToSubmittedUpdateQueue(ctx, submittedUpdate) + k.addToSubmittedUpdatesQueue(ctx, k.applyNewUpdate(existingSUpdate, update)) + } else { + // No one has tried to update channel + submittedUpdate := SubmittedUpdate{ + Update: update, + ExecutionTime: ctx.BlockHeight() + ChannelDisputeTime, //TODO check what exactly BlockHeight refers to + } + k.addToSubmittedUpdatesQueue(ctx, submittedUpdate) } + + tags := sdk.EmptyTags() // TODO tags + + return tags, nil } -func (k Keeper) CloseChannelByReceiver(update Update) () { +func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // TODO Validate update // Check if there is an update in the queue already - q := k.getSubmittedUpdateQueue(ctx) + q, found := k.getSubmittedUpdatesQueue(ctx) + if !found { + panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors + } if q.Contains(update.ChannelID) { // Someone has previously tried to update channel but receiver has final say - k.removeFromSubmittedUpdateQueue(ctx, update.ChannelID) + k.removeFromSubmittedUpdatesQueue(ctx, update.ChannelID) } - - k.closeChannel(ctx, update) + + tags, err := k.closeChannel(ctx, update) + + return tags, err } // Main function that compare updates against each other. // Pure function -func (k Keeper) applyNewUpdate(existingSUpdate, proposedUpdate) SubmittedUpdate { +func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate { var returnUpdate SubmittedUpdate - if existingSUpdate.sequence > proposedUpdate.sequence { + if existingSUpdate.Sequence > proposedUpdate.Sequence { // update accepted returnUpdate = SubmittedUpdate{ - Update: proposedUpdate - ExecutionTime: existingSUpdate.ExecutionTime + Update: proposedUpdate, + ExecutionTime: existingSUpdate.ExecutionTime, } } else { // update rejected @@ -141,62 +152,79 @@ func (k Keeper) applyNewUpdate(existingSUpdate, proposedUpdate) SubmittedUpdate return returnUpdate } -func (k Keeper) closeChannel(ctx sdk.Context, update Update) { - channel := k.getChannel(ctx, update.ChannelID) +// unsafe close channel - doesn't check if update matches existing channel TODO make safer? +func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { + var err error + var sdkErr sdk.Error + var tags sdk.Tags // Add coins to sender and receiver - for address, coins := range update.CoinsUpdate { + // TODO check for possible errors first to avoid coins being half paid out? + var address sdk.AccAddress + for bech32Address, coins := range update.CoinsUpdate { + address, err = sdk.AccAddressFromBech32(bech32Address) + if err != nil { + panic(err) + } // TODO check somewhere if coins are not negative? - k.ck.AddCoins(ctx, address, coins) + _, tags, sdkErr = k.coinKeeper.AddCoins(ctx, address, coins) + if sdkErr != nil { + panic(sdkErr) + } } - + k.deleteChannel(ctx, update.ChannelID) + + return tags, nil } - - // =========================================== QUEUE - func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { // always overwrite prexisting values - leave paychan logic to higher levels // get current queue - q := k.getSubmittedUpdateQueue(ctx) + q, found := k.getSubmittedUpdatesQueue(ctx) + if !found { + panic("SubmittedUpdatesQueue not found.") + } // append ID to queue - if q.Contains(sUpdate.ChannelID)! { + if !q.Contains(sUpdate.ChannelID) { q = append(q, sUpdate.ChannelID) } // set queue - k.setSubmittedUpdateQueue(ctx, q) + k.setSubmittedUpdatesQueue(ctx, q) // store submittedUpdate k.setSubmittedUpdate(ctx, sUpdate) } -func (k Keeper) removeFromSubmittdUpdatesQueue(ctx sdk.Context, channelID) { +func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID ChannelID) { // get current queue - q := k.getSubmittedUpdateQueue(ctx) + q, found := k.getSubmittedUpdatesQueue(ctx) + if !found { + panic("SubmittedUpdatesQueue not found.") + } // remove id q.RemoveMatchingElements(channelID) // set queue - k.setSubmittedUpdateQueue(ctx, q) + k.setSubmittedUpdatesQueue(ctx, q) // delete submittedUpdate k.deleteSubmittedUpdate(ctx, channelID) } -func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (Queue, bool) { +func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (SubmittedUpdatesQueue, bool) { // load from DB store := ctx.KVStore(k.storeKey) bz := store.Get(k.getSubmittedUpdatesQueueKey()) - var q Queue + var suq SubmittedUpdatesQueue if bz == nil { - return q, false + return suq, false // TODO maybe create custom error to pass up here } // unmarshal - k.cdc.MustUnmarshalBinary(bz, &q) + k.cdc.MustUnmarshalBinary(bz, &suq) // return - return q, true + return suq, true } -func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, q Queue) { +func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, q SubmittedUpdatesQueue) { store := ctx.KVStore(k.storeKey) // marshal bz := k.cdc.MustMarshalBinary(q) @@ -213,7 +241,7 @@ func (k Keeper) getSubmittedUpdatesQueueKey() []byte { // This section deals with only setting and getting func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) { - + // load from DB store := ctx.KVStore(k.storeKey) bz := store.Get(k.getSubmittedUpdateKey(channelID)) @@ -234,11 +262,11 @@ func (k Keeper) setSubmittedUpdate(ctx sdk.Context, sUpdate SubmittedUpdate) { // marshal bz := k.cdc.MustMarshalBinary(sUpdate) // panics if something goes wrong // write to db - key := k.getSubmittedUpdateKey(sUpdate.channelID) + key := k.getSubmittedUpdateKey(sUpdate.ChannelID) store.Set(key, bz) // panics if something goes wrong } -func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ) { +func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ChannelID) { store := ctx.KVStore(k.storeKey) store.Delete(k.getSubmittedUpdateKey(channelID)) // TODO does this have return values? What happens when key doesn't exist? @@ -247,12 +275,10 @@ func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte { return []byte(fmt.Sprintf("submittedUpdate:%d", channelID)) } - // ========================================== CHANNELS // Reteive a payment channel struct from the blockchain store. func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) { - // load from DB store := ctx.KVStore(k.storeKey) bz := store.Get(k.getChannelKey(channelID)) @@ -273,27 +299,28 @@ func (k Keeper) setChannel(ctx sdk.Context, channel Channel) { // marshal bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong // write to db - key := sdk.getChannelKey(channel.ID) + key := k.getChannelKey(channel.ID) store.Set(key, bz) // panics if something goes wrong } -func (k Keeper) deleteChannel(ctx sdk.Context, channelID ) { +func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) { store := ctx.KVStore(k.storeKey) store.Delete(k.getChannelKey(channelID)) // TODO does this have return values? What happens when key doesn't exist? } -func (k Keeper) getNewChannelID(ctx sdk.Context) (int64, error) { +func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID { // get last channel ID - store := k.KVStore(k.storeKey) + var lastID ChannelID + store := ctx.KVStore(k.storeKey) bz := store.Get(k.getLastChannelIDKey()) if bz == nil { - return nil, // TODO throw some error (assumes this has been initialized elsewhere) or just set to zero here + lastID = -1 // TODO is just setting to zero if uninitialized ok? + } else { + k.cdc.MustUnmarshalBinary(bz, &lastID) } - var lastID ChannelID - k.cdc.MustUnmarshalBinary(bz, &lastID) // increment to create new one - newID := lastID+1 + newID := lastID + 1 bz = k.cdc.MustMarshalBinary(newID) // set last channel id again store.Set(k.getLastChannelIDKey(), bz) @@ -302,11 +329,12 @@ func (k Keeper) getNewChannelID(ctx sdk.Context) (int64, error) { } func (k Keeper) getChannelKey(channelID ChannelID) []byte { - return []bytes(fmt.Sprintf("channel:%d", channelID)) + return []byte(fmt.Sprintf("channel:%d", channelID)) } func (k Keeper) getLastChannelIDKey() []byte { - return []bytes("lastChannelID") + return []byte("lastChannelID") } + /* // Close a payment channel and distribute funds to participants. func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64, receiverAmount sdk.Coins) (sdk.Tags, sdk.Error) { diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 8ba25b79..41fa21b4 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -1,5 +1,6 @@ package paychan +/* import ( "testing" //"github.com/stretchr/testify/assert" @@ -139,3 +140,4 @@ func TestKeeper(t *testing.T) { } } +*/ diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 2b532c74..68531b9c 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -2,7 +2,7 @@ package paychan import ( sdk "github.com/cosmos/cosmos-sdk/types" - "strconv" + "github.com/tendermint/tendermint/crypto" ) /* CHANNEL TYPES */ @@ -11,32 +11,35 @@ import ( // Participants is limited to two as currently these are unidirectional channels. // Last participant is designated as receiver. type Channel struct { - ID ChannelID - Participants [2]sdk.AccAddress - Coins sdk.Coins + ID ChannelID + Participants [2]sdk.AccAddress // [senderAddr, receiverAddr] + Coins sdk.Coins } type ChannelID int64 // TODO should this be positive only // The data that is passed between participants as payments, and submitted to the blockchain to close a channel. type Update struct { - ChannelID int64 - CoinsUpdate map[sdk.AccAddress]sdk.Coins - Sequence int64 - sig // TODO type, only sender needs to sign + ChannelID ChannelID + CoinsUpdate map[string]sdk.Coins // map of bech32 addresses to coins + Sequence int64 + Sigs [1]{crypto.Signature} // only sender needs to sign } +var ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time + // An update that has been submitted to the blockchain, but not yet acted on. -type SubmittedUpdate { +type SubmittedUpdate struct { Update - executionTime int64 // BlockHeight + ExecutionTime int64 // BlockHeight } -type SubmittedUpdateQueue []ChannelID +type SubmittedUpdatesQueue []ChannelID + // Check if value is in queue -func (suq SubmittedChannelID) Contains(channelID ChannelID) bool { +func (suq SubmittedUpdatesQueue) Contains(channelID ChannelID) bool { found := false - for _, id := range(suq) { + for _, id := range suq { if id == channelID { found = true break @@ -44,11 +47,12 @@ func (suq SubmittedChannelID) Contains(channelID ChannelID) bool { } return found } -// Remove all values from queue that match argument -func (suq SubmittedUpdateQueue) RemoveMatchingElements(channelID ChannelID) { - newSUQ := SubmittedUpdateQueue{} - for _, id := range(suq) { +// Remove all values from queue that match argument +func (suq SubmittedUpdatesQueue) RemoveMatchingElements(channelID ChannelID) { + newSUQ := SubmittedUpdatesQueue{} + + for _, id := range suq { if id != channelID { newSUQ = append(newSUQ, id) } @@ -56,8 +60,6 @@ func (suq SubmittedUpdateQueue) RemoveMatchingElements(channelID ChannelID) { suq = newSUQ } -var ChannelDisputeTime = 2000 // measured in blocks - /* MESSAGE TYPES */ /* Message implement the sdk.Msg interface: @@ -77,14 +79,14 @@ type Msg interface { // Signers returns the addrs of signers that must sign. // CONTRACT: All signatures must be present to be valid. // CONTRACT: Returns addrs in some deterministic order. - GetSigners() []Address + GetSigners() []AccAddress } */ // A message to create a payment channel. type MsgCreate struct { - Participants [2]sdk.AccAddress - Coins sdk.Coins + Participants [2]sdk.AccAddress + Coins sdk.Coins } //Create a new message. @@ -103,12 +105,11 @@ func (msg CreatMsg) NewMsgCreate(sender sdk.Address, receiver sdk.Address, amoun func (msg MsgCreate) Type() string { return "paychan" } func (msg MsgCreate) GetSignBytes() []byte { - // TODO create msgCdc in wire.go - bz, err := msgCdc.Marshal(msg) + bz, err := msgCdc.MarshalJSON(msg) if err != nil { panic(err) } - return MustSortJSON(bz) + return sdk.MustSortJSON(bz) } func (msg MsgCreate) ValidateBasic() sdk.Error { @@ -118,29 +119,29 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { //TODO implement /* - // check if all fields present / not 0 valued - if len(msg.Sender) == 0 { - return sdk.ErrInvalidAddress(msg.Sender.String()) - } - if len(msg.Receiver) == 0 { - return sdk.ErrInvalidAddress(msg.Receiver.String()) - } - if len(msg.Amount) == 0 { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - // Check if coins are sorted, non zero, non negative - if !msg.Amount.IsValid() { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - if !msg.Amount.IsPositive() { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - // TODO check if Address valid? + // check if all fields present / not 0 valued + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) + } + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) + } + if len(msg.Amount) == 0 { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + // Check if coins are sorted, non zero, non negative + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + if !msg.Amount.IsPositive() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + // TODO check if Address valid? */ return nil } -func (msg MsgCreate) GetSigners() []sdk.Address { +func (msg MsgCreate) GetSigners() []sdk.AccAddress { // Only sender must sign to create a paychan return []sdk.AccAddress{msg.Participants[0]} // select sender address } @@ -160,46 +161,45 @@ type MsgSubmitUpdate struct { func (msg MsgSubmitUpdate) Type() string { return "paychan" } func (msg MsgSubmitUpdate) GetSignBytes() []byte { - // TODO create msgCdc in wire.go - bz, err := msgCdc.Marshal(msg) + bz, err := msgCdc.MarshalJSON(msg) if err != nil { panic(err) } - return MustSortJSON(bz) + return sdk.MustSortJSON(bz) } func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error { // TODO implement /* - // check if all fields present / not 0 valued - if len(msg.Sender) == 0 { - return sdk.ErrInvalidAddress(msg.Sender.String()) - } - if len(msg.Receiver) == 0 { - return sdk.ErrInvalidAddress(msg.Receiver.String()) - } - if len(msg.ReceiverAmount) == 0 { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - // check id ≥ 0 - if msg.Id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors - } - // Check if coins are sorted, non zero, non negative - if !msg.ReceiverAmount.IsValid() { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - if !msg.ReceiverAmount.IsPositive() { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - // TODO check if Address valid? + // check if all fields present / not 0 valued + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) + } + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) + } + if len(msg.ReceiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + // check id ≥ 0 + if msg.Id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors + } + // Check if coins are sorted, non zero, non negative + if !msg.ReceiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + if !msg.ReceiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + // TODO check if Address valid? */ return nil } -func (msg MsgSubmitUpdate) GetSigners() []sdk.Address { +func (msg MsgSubmitUpdate) GetSigners() []sdk.AccAddress { // Signing not strictly necessary as signatures contained within the channel update. // TODO add signature by submitting address - return []sdk.Address{} + return []sdk.AccAddress{msg.submitter} } diff --git a/internal/x/paychan/wire.go b/internal/x/paychan/wire.go index 9d9e5eb3..98c7817e 100644 --- a/internal/x/paychan/wire.go +++ b/internal/x/paychan/wire.go @@ -4,14 +4,14 @@ import ( "github.com/cosmos/cosmos-sdk/wire" ) -/* func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(MsgCreate{}, "paychan/MsgCreate", nil) - cdc.RegisterConcrete(MsgClose{}, "paychan/MsgClose", nil) + cdc.RegisterConcrete(MsgSubmitUpdate{}, "paychan/MsgSubmitUpdate", nil) } var msgCdc = wire.NewCodec() +/* func init() { RegisterWire(msgCdc) // TODO is this needed? From b147232be7d9435b30f0110a9d759bd11b375775 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 28 Aug 2018 23:45:26 -0400 Subject: [PATCH 19/30] add minimal test coverage --- internal/x/paychan/README.md | 5 +- internal/x/paychan/client/cmd/cmd.go | 8 +- internal/x/paychan/endblocker.go | 1 + internal/x/paychan/endblocker_test.go | 61 ++++++++++ internal/x/paychan/keeper.go | 18 +-- internal/x/paychan/keeper_test.go | 166 ++++++++++++++++++++++---- internal/x/paychan/test_common.go | 38 ++++++ internal/x/paychan/types.go | 32 +++-- internal/x/paychan/types_test.go | 26 ++++ 9 files changed, 308 insertions(+), 47 deletions(-) create mode 100644 internal/x/paychan/endblocker_test.go create mode 100644 internal/x/paychan/test_common.go create mode 100644 internal/x/paychan/types_test.go diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index d85b83b7..7cb74683 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -9,8 +9,11 @@ Simplifications: TODO - chnge module name to "channel"? - Find a better name for Queue - clarify distinction between int slice and abstract queue concept + - refactor queue into one object - Do all the small functions need to be methods on the keeper or can they just be floating around? - - Tidy up - standardise var names, comments and method descriptions + - Tidy up - standardise var names, method descriptions, heading comments - is having all the get functions return a bool if not found reasonable? - any problem in signing your own address? - Gas + - Codespace + - find nicer name for payouts diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index f262cbf8..ccc1fb2c 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -21,10 +21,10 @@ import ( // list of functions that return pointers to cobra commands // No local storage needed for cli acting as a sender -// Currently minimum set of cli commands are implemented: -// create paychan - create and fund -// generate new paychan state - print a half signed close tx (sender signs) -// close paychan - close using state (receiver signs) +// Current minimal set of cli commands: +// create paychan - create and fund (sender signs tx) +// generate new update - print a signed update (from sender) +// submit update - send update to chain (either can sign tx) // Future cli commands: // create paychan diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go index cb793d2a..d4c9c777 100644 --- a/internal/x/paychan/endblocker.go +++ b/internal/x/paychan/endblocker.go @@ -24,6 +24,7 @@ func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { panic("can't find element in queue that should exist") } if ctx.BlockHeight() >= sUpdate.ExecutionTime { + k.removeFromSubmittedUpdatesQueue(ctx, sUpdate.ChannelID) channelTags, err = k.closeChannel(ctx, sUpdate.Update) if err != nil { panic(err) diff --git a/internal/x/paychan/endblocker_test.go b/internal/x/paychan/endblocker_test.go new file mode 100644 index 00000000..079cb7e7 --- /dev/null +++ b/internal/x/paychan/endblocker_test.go @@ -0,0 +1,61 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/crypto" + "testing" +) + +func TestEndBlocker(t *testing.T) { + // SETUP + ctx, _, channelKeeper, addrs, _ := createMockApp() + sender := addrs[0] + receiver := addrs[1] + coins := sdk.Coins{sdk.NewCoin("KVA", 10)} + + // create new channel + channelID := ChannelID(0) // should be 0 as first channel + channel := Channel{ + ID: channelID, + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, + } + channelKeeper.setChannel(ctx, channel) + + // create closing update and submittedUpdate + payouts := Payouts{ + {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, + {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, + } + update := Update{ + ChannelID: channelID, + Payouts: payouts, + Sequence: 0, + Sigs: [1]crypto.Signature{}, + } + sUpdate := SubmittedUpdate{ + Update: update, + ExecutionTime: 0, // current blocktime + } + // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation + channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) + // flag channel for closure + channelKeeper.addToSubmittedUpdatesQueue(ctx, sUpdate) + + // ACTION + EndBlocker(ctx, channelKeeper) + + // CHECK RESULTS + // ideally just check if keeper.channelClose was called, but can't + // writing endBlocker to accept an interface of which keeper is implementation would make this possible + // check channel is gone + _, found := channelKeeper.getChannel(ctx, channelID) + assert.False(t, found) + // check queue is empty, NOTE: due to encoding, an empty queue (underneath just an int slice) will be decoded as nil slice rather than an empty slice + suq, _ := channelKeeper.getSubmittedUpdatesQueue(ctx) + assert.Equal(t, SubmittedUpdatesQueue(nil), suq) + // check submittedUpdate is gone + _, found = channelKeeper.getSubmittedUpdate(ctx, channelID) + assert.False(t, found) +} diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 7bab151e..d8520fb1 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -154,23 +154,17 @@ func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate U // unsafe close channel - doesn't check if update matches existing channel TODO make safer? func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { - var err error - var sdkErr sdk.Error + var err sdk.Error var tags sdk.Tags // Add coins to sender and receiver // TODO check for possible errors first to avoid coins being half paid out? - var address sdk.AccAddress - for bech32Address, coins := range update.CoinsUpdate { - address, err = sdk.AccAddressFromBech32(bech32Address) + for _, payout := range update.Payouts { + // TODO check somewhere if coins are not negative? + _, tags, err = k.coinKeeper.AddCoins(ctx, payout.Address, payout.Coins) if err != nil { panic(err) } - // TODO check somewhere if coins are not negative? - _, tags, sdkErr = k.coinKeeper.AddCoins(ctx, address, coins) - if sdkErr != nil { - panic(sdkErr) - } } k.deleteChannel(ctx, update.ChannelID) @@ -224,10 +218,10 @@ func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (SubmittedUpdatesQueue // return return suq, true } -func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, q SubmittedUpdatesQueue) { +func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, suq SubmittedUpdatesQueue) { store := ctx.KVStore(k.storeKey) // marshal - bz := k.cdc.MustMarshalBinary(q) + bz := k.cdc.MustMarshalBinary(suq) // write to db key := k.getSubmittedUpdatesQueueKey() store.Set(key, bz) diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 41fa21b4..392f24a8 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -1,31 +1,153 @@ package paychan -/* import ( - "testing" - //"github.com/stretchr/testify/assert" - - abci "github.com/tendermint/abci/types" - dbm "github.com/tendermint/tmlibs/db" - "github.com/tendermint/tmlibs/log" - - "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/wire" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/stretchr/testify/assert" + //"github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + "testing" ) -// GetPaychan -// - gets a paychan if it exists, and not if it doesn't -// setPaychan -// - sets a paychan -// CreatePaychan -// - creates a paychan under normal conditions -// ClosePaychan -// - closes a paychan under normal conditions -// GetPaychans -// paychanKey +func TestKeeper(t *testing.T) { + + t.Run("CreateChannel", func(t *testing.T) { + + // + ////// SETUP + // create basic mock app + ctx, coinKeeper, channelKeeper, addrs, genAccFunding := createMockApp() + + sender := addrs[0] + receiver := addrs[1] + coins := sdk.Coins{sdk.NewCoin("KVA", 10)} + + // + ////// ACTION + _, err := channelKeeper.CreateChannel(ctx, sender, receiver, coins) + + // + ////// CHECK RESULTS + assert.Nil(t, err) + // channel exists with correct attributes + channelID := ChannelID(0) // should be 0 as first channel + expectedChan := Channel{ + ID: channelID, + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, + } + createdChan, _ := channelKeeper.getChannel(ctx, channelID) + assert.Equal(t, expectedChan, createdChan) + // check coins deducted from sender + assert.Equal(t, genAccFunding.Minus(coins), coinKeeper.GetCoins(ctx, sender)) + // check no coins deducted from receiver + assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, receiver)) + // check next chan id + assert.Equal(t, ChannelID(1), channelKeeper.getNewChannelID(ctx)) + }) + + t.Run("ReceiverCloseChannel", func(t *testing.T) { + // SETUP + ctx, coinKeeper, channelKeeper, addrs, genAccFunding := createMockApp() + + sender := addrs[0] + receiver := addrs[1] + coins := sdk.Coins{sdk.NewCoin("KVA", 10)} + + // create new channel + channelID := ChannelID(0) // should be 0 as first channel + channel := Channel{ + ID: channelID, + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, + } + channelKeeper.setChannel(ctx, channel) + + // create closing update + payouts := Payouts{ + {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, + {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, + } + update := Update{ + ChannelID: channelID, + Payouts: payouts, + Sequence: 0, + Sigs: [1]crypto.Signature{}, + } + // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation + channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) + + // ACTION + _, err := channelKeeper.CloseChannelByReceiver(ctx, update) + + // CHECK RESULTS + // no error + assert.Nil(t, err) + // coins paid out + senderPayout, _ := payouts.Get(sender) + assert.Equal(t, genAccFunding.Plus(senderPayout), coinKeeper.GetCoins(ctx, sender)) + receiverPayout, _ := payouts.Get(receiver) + assert.Equal(t, genAccFunding.Plus(receiverPayout), coinKeeper.GetCoins(ctx, receiver)) + // channel deleted + _, found := channelKeeper.getChannel(ctx, channelID) + assert.False(t, found) + + }) + + t.Run("SenderInitCloseChannel", func(t *testing.T) { + // SETUP + ctx, _, channelKeeper, addrs, _ := createMockApp() + + sender := addrs[0] + receiver := addrs[1] + coins := sdk.Coins{sdk.NewCoin("KVA", 10)} + + // create new channel + channelID := ChannelID(0) // should be 0 as first channel + channel := Channel{ + ID: channelID, + Participants: [2]sdk.AccAddress{sender, receiver}, + Coins: coins, + } + channelKeeper.setChannel(ctx, channel) + + // create closing update + payouts := Payouts{ + {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, + {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, + } + update := Update{ + ChannelID: channelID, + Payouts: payouts, + Sequence: 0, + Sigs: [1]crypto.Signature{}, + } + // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation + channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) + + // ACTION + _, err := channelKeeper.InitCloseChannelBySender(ctx, update) + + // CHECK RESULTS + // no error + assert.Nil(t, err) + // submittedupdate in queue and correct + suq, found := channelKeeper.getSubmittedUpdatesQueue(ctx) + assert.True(t, found) + assert.True(t, suq.Contains(channelID)) + + su, found := channelKeeper.getSubmittedUpdate(ctx, channelID) + assert.True(t, found) + expectedSubmittedUpdate := SubmittedUpdate{ + Update: update, + ExecutionTime: ChannelDisputeTime, + } + assert.Equal(t, expectedSubmittedUpdate, su) + // TODO check channel is still in db and coins haven't changed? + }) + +} + +/* func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { // create db diff --git a/internal/x/paychan/test_common.go b/internal/x/paychan/test_common.go new file mode 100644 index 00000000..bb881e7a --- /dev/null +++ b/internal/x/paychan/test_common.go @@ -0,0 +1,38 @@ +package paychan + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/mock" + //"github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/x/bank" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Setup an example app with an in memory DB and the required keepers +// Also create two accounts with 1000KVA +// Could do with refactoring +func createMockApp() (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, sdk.Coins) { + mApp := mock.NewApp() // creates a half complete app + coinKeeper := bank.NewKeeper(mApp.AccountMapper) + + // create channel keeper + keyChannel := sdk.NewKVStoreKey("channel") + channelKeeper := NewKeeper(mApp.Cdc, keyChannel, coinKeeper) + // add router? + //mapp.Router().AddRoute("channel", NewHandler(channelKeeper)) + + mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel}) // needs to be called I think to finish setup + + // create some accounts + numGenAccs := 2 // create two initial accounts + genAccFunding := sdk.Coins{sdk.NewCoin("KVA", 1000)} + genAccs, addrs, _, _ := mock.CreateGenAccounts(numGenAccs, genAccFunding) + + // initialize the app with these accounts + mock.SetGenesis(mApp, genAccs) + + mApp.BeginBlock(abci.RequestBeginBlock{}) // going off other module tests + ctx := mApp.BaseApp.NewContext(false, abci.Header{}) + + return ctx, coinKeeper, channelKeeper, addrs, genAccFunding +} diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 68531b9c..a93454e3 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -3,6 +3,7 @@ package paychan import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" + "reflect" ) /* CHANNEL TYPES */ @@ -20,13 +21,28 @@ type ChannelID int64 // TODO should this be positive only // The data that is passed between participants as payments, and submitted to the blockchain to close a channel. type Update struct { - ChannelID ChannelID - CoinsUpdate map[string]sdk.Coins // map of bech32 addresses to coins - Sequence int64 - Sigs [1]{crypto.Signature} // only sender needs to sign + ChannelID ChannelID + Payouts Payouts //map[string]sdk.Coins // map of bech32 addresses to coins + Sequence int64 + Sigs [1]crypto.Signature // only sender needs to sign +} +type Payout struct { + Address sdk.AccAddress + Coins sdk.Coins +} +type Payouts []Payout + +// Get the coins associated with payout address. TODO constrain payouts to only have one entry per address +func (payouts Payouts) Get(addr sdk.AccAddress) (sdk.Coins, bool) { + for _, p := range payouts { + if reflect.DeepEqual(p.Address, addr) { + return p.Coins, true + } + } + return nil, false } -var ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time +const ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time // An update that has been submitted to the blockchain, but not yet acted on. type SubmittedUpdate struct { @@ -49,15 +65,15 @@ func (suq SubmittedUpdatesQueue) Contains(channelID ChannelID) bool { } // Remove all values from queue that match argument -func (suq SubmittedUpdatesQueue) RemoveMatchingElements(channelID ChannelID) { +func (suq *SubmittedUpdatesQueue) RemoveMatchingElements(channelID ChannelID) { newSUQ := SubmittedUpdatesQueue{} - for _, id := range suq { + for _, id := range *suq { if id != channelID { newSUQ = append(newSUQ, id) } } - suq = newSUQ + *suq = newSUQ } /* MESSAGE TYPES */ diff --git a/internal/x/paychan/types_test.go b/internal/x/paychan/types_test.go new file mode 100644 index 00000000..14d45c39 --- /dev/null +++ b/internal/x/paychan/types_test.go @@ -0,0 +1,26 @@ +package paychan + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSubmittedUpdatesQueue(t *testing.T) { + t.Run("RemoveMatchingElements", func(t *testing.T) { + // SETUP + q := SubmittedUpdatesQueue{4, 8, 23, 0, 5645657} + // ACTION + q.RemoveMatchingElements(23) + // CHECK RESULTS + expectedQ := SubmittedUpdatesQueue{4, 8, 0, 5645657} + assert.Equal(t, expectedQ, q) + + // SETUP + q = SubmittedUpdatesQueue{0} + // ACTION + q.RemoveMatchingElements(0) + // CHECK RESULTS + expectedQ = SubmittedUpdatesQueue{} + assert.Equal(t, expectedQ, q) + }) +} From ec105e88ad410c40e0d6ef8657900e9a6fa502d5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 30 Aug 2018 13:43:15 -0400 Subject: [PATCH 20/30] remove unnecessary bidirectional features --- internal/x/paychan/README.md | 2 +- internal/x/paychan/endblocker_test.go | 1 - internal/x/paychan/keeper.go | 46 ++++++++++++++++----------- internal/x/paychan/keeper_test.go | 2 -- internal/x/paychan/types.go | 4 +-- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 7cb74683..80a3492e 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -12,8 +12,8 @@ Simplifications: - refactor queue into one object - Do all the small functions need to be methods on the keeper or can they just be floating around? - Tidy up - standardise var names, method descriptions, heading comments - - is having all the get functions return a bool if not found reasonable? - any problem in signing your own address? - Gas - Codespace - find nicer name for payouts + - tags - return channel id diff --git a/internal/x/paychan/endblocker_test.go b/internal/x/paychan/endblocker_test.go index 079cb7e7..d6e1d5dd 100644 --- a/internal/x/paychan/endblocker_test.go +++ b/internal/x/paychan/endblocker_test.go @@ -31,7 +31,6 @@ func TestEndBlocker(t *testing.T) { update := Update{ ChannelID: channelID, Payouts: payouts, - Sequence: 0, Sigs: [1]crypto.Signature{}, } sUpdate := SubmittedUpdate{ diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index d8520fb1..02c24e29 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -97,11 +97,18 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta } if q.Contains(update.ChannelID) { // Someone has previously tried to update channel - existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID) - if !found { - panic("can't find element in queue that should exist") - } - k.addToSubmittedUpdatesQueue(ctx, k.applyNewUpdate(existingSUpdate, update)) + // In bidirectional channels the new update is compared against existing and replaces it if it has a higher sequence number. + + // existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID) + // if !found { + // panic("can't find element in queue that should exist") + // } + // k.addToSubmittedUpdatesQueue(ctx, k.applyNewUpdate(existingSUpdate, update)) + + // However in unidirectional case, only the sender can close a channel this way. No clear need for them to be able to submit an update replacing a previous one they sent, so don't allow it. + // TODO tags + // TODO custom errors return sdk.EmptyTags(), sdk.NewError("Sender can't submit an update for channel if one has already been submitted.") + panic("Sender can't submit an update for channel if one has already been submitted.") } else { // No one has tried to update channel submittedUpdate := SubmittedUpdate{ @@ -136,21 +143,22 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags // Main function that compare updates against each other. // Pure function -func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate { - var returnUpdate SubmittedUpdate +// Not needed in unidirectional case. +// func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate { +// var returnUpdate SubmittedUpdate - if existingSUpdate.Sequence > proposedUpdate.Sequence { - // update accepted - returnUpdate = SubmittedUpdate{ - Update: proposedUpdate, - ExecutionTime: existingSUpdate.ExecutionTime, - } - } else { - // update rejected - returnUpdate = existingSUpdate - } - return returnUpdate -} +// if existingSUpdate.Sequence > proposedUpdate.Sequence { +// // update accepted +// returnUpdate = SubmittedUpdate{ +// Update: proposedUpdate, +// ExecutionTime: existingSUpdate.ExecutionTime, // FIXME any new update proposal should be subject to full dispute period from submission +// } +// } else { +// // update rejected +// returnUpdate = existingSUpdate +// } +// return returnUpdate +// } // unsafe close channel - doesn't check if update matches existing channel TODO make safer? func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 392f24a8..9fd59a87 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -70,7 +70,6 @@ func TestKeeper(t *testing.T) { update := Update{ ChannelID: channelID, Payouts: payouts, - Sequence: 0, Sigs: [1]crypto.Signature{}, } // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation @@ -118,7 +117,6 @@ func TestKeeper(t *testing.T) { update := Update{ ChannelID: channelID, Payouts: payouts, - Sequence: 0, Sigs: [1]crypto.Signature{}, } // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index a93454e3..3ed695b9 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -23,8 +23,8 @@ type ChannelID int64 // TODO should this be positive only type Update struct { ChannelID ChannelID Payouts Payouts //map[string]sdk.Coins // map of bech32 addresses to coins - Sequence int64 - Sigs [1]crypto.Signature // only sender needs to sign + //Sequence int64 Not needed for unidirectional channels + Sigs [1]crypto.Signature // only sender needs to sign in unidirectional } type Payout struct { Address sdk.AccAddress From bbe83c6ad7fcc98b1dd373dfb4e858426c80deac Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 13:16:56 -0400 Subject: [PATCH 21/30] add validation and signature checks also expands test cases --- internal/x/paychan/README.md | 7 +- internal/x/paychan/endblocker_test.go | 13 +- internal/x/paychan/keeper.go | 194 ++++++-------- internal/x/paychan/keeper_test.go | 360 +++++++++++++++++++------- internal/x/paychan/test_common.go | 32 ++- internal/x/paychan/types.go | 48 +++- internal/x/paychan/types_test.go | 19 ++ 7 files changed, 444 insertions(+), 229 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 80a3492e..30a8812c 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -14,6 +14,9 @@ Simplifications: - Tidy up - standardise var names, method descriptions, heading comments - any problem in signing your own address? - Gas - - Codespace - - find nicer name for payouts + - find nicer name for payout - tags - return channel id + - create custom errors instead of using sdk.ErrInternal + - maybe split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain + - consider removing pubKey from UpdateSignature - instead let channel module access accountMapper + - remove printout from tests when app initialised diff --git a/internal/x/paychan/endblocker_test.go b/internal/x/paychan/endblocker_test.go index d6e1d5dd..dd4cce82 100644 --- a/internal/x/paychan/endblocker_test.go +++ b/internal/x/paychan/endblocker_test.go @@ -3,13 +3,13 @@ package paychan import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" - "github.com/tendermint/tendermint/crypto" "testing" ) func TestEndBlocker(t *testing.T) { // SETUP - ctx, _, channelKeeper, addrs, _ := createMockApp() + accountSeeds := []string{"senderSeed", "receiverSeed"} + ctx, _, channelKeeper, addrs, _, _, _ := createMockApp(accountSeeds) sender := addrs[0] receiver := addrs[1] coins := sdk.Coins{sdk.NewCoin("KVA", 10)} @@ -24,14 +24,11 @@ func TestEndBlocker(t *testing.T) { channelKeeper.setChannel(ctx, channel) // create closing update and submittedUpdate - payouts := Payouts{ - {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, - {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, - } + payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}} update := Update{ ChannelID: channelID, - Payouts: payouts, - Sigs: [1]crypto.Signature{}, + Payout: payout, + //Sigs: [1]crypto.Signature{}, } sUpdate := SubmittedUpdate{ Update: update, diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 02c24e29..318a3435 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -1,6 +1,7 @@ package paychan import ( + "bytes" "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" @@ -15,7 +16,6 @@ type Keeper struct { cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper - // TODO investigate codespace //codespace sdk.CodespaceType } @@ -34,36 +34,21 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { // Create a new payment channel and lock up sender funds. func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver sdk.AccAddress, coins sdk.Coins) (sdk.Tags, sdk.Error) { - // TODO do validation and maybe move somewhere nicer - /* - // args present - if len(sender) == 0 { - return nil, sdk.ErrInvalidAddress(sender.String()) - } - if len(receiver) == 0 { - return nil, sdk.ErrInvalidAddress(receiver.String()) - } - if len(amount) == 0 { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - // Check if coins are sorted, non zero, positive - if !amount.IsValid() { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - if !amount.IsPositive() { - return nil, sdk.ErrInvalidCoins(amount.String()) - } - // sender should exist already as they had to sign. - // receiver address exists. am is the account mapper in the coin keeper. - // TODO automatically create account if not present? - // TODO remove as account mapper not available to this pkg - //if k.coinKeeper.am.GetAccount(ctx, receiver) == nil { - // return nil, sdk.ErrUnknownAddress(receiver.String()) - //} - // sender has enough coins - done in Subtract method - // TODO check if sender and receiver different? - */ + // Check addresses valid (Technicaly don't need to check sender address is valid as SubtractCoins does that) + if len(sender) == 0 { + return nil, sdk.ErrInvalidAddress(sender.String()) + } + if len(receiver) == 0 { + return nil, sdk.ErrInvalidAddress(receiver.String()) + } + // check coins are sorted and positive (disallow channels with zero balance) + if !coins.IsValid() { + return nil, sdk.ErrInvalidCoins(coins.String()) + } + if !coins.IsPositive() { + return nil, sdk.ErrInvalidCoins(coins.String()) + } // subtract coins from sender _, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins) @@ -89,7 +74,10 @@ func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver s func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // This is roughly the default path for non unidirectional channels - // TODO Validate update - e.g. check signed by sender + err := k.validateUpdate(ctx, update) + if err != nil { + return nil, err + } q, found := k.getSubmittedUpdatesQueue(ctx) if !found { @@ -108,7 +96,7 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta // However in unidirectional case, only the sender can close a channel this way. No clear need for them to be able to submit an update replacing a previous one they sent, so don't allow it. // TODO tags // TODO custom errors return sdk.EmptyTags(), sdk.NewError("Sender can't submit an update for channel if one has already been submitted.") - panic("Sender can't submit an update for channel if one has already been submitted.") + sdk.ErrInternal("Sender can't submit an update for channel if one has already been submitted.") } else { // No one has tried to update channel submittedUpdate := SubmittedUpdate{ @@ -124,7 +112,11 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta } func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { - // TODO Validate update + + err := k.validateUpdate(ctx, update) + if err != nil { + return nil, err + } // Check if there is an update in the queue already q, found := k.getSubmittedUpdatesQueue(ctx) @@ -160,16 +152,50 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags // return returnUpdate // } +func (k Keeper) validateUpdate(ctx sdk.Context, update Update) sdk.Error { + // Check that channel exists + channel, found := k.getChannel(ctx, update.ChannelID) + if !found { + return sdk.ErrInternal("Channel doesn't exist") + } + // Check the num of payout participants match channel participants + if len(update.Payout) != len(channel.Participants) { + return sdk.ErrInternal("Payout doesn't match number of channel participants") + } + // Check each coins are valid + for _, coins := range update.Payout { + if !coins.IsValid() { + return sdk.ErrInternal("Payout coins aren't formatted correctly") + } + } + // Check payout coins are each not negative (can be zero though) + if !update.Payout.IsNotNegative() { + return sdk.ErrInternal("Payout cannot be negative") + } + // Check payout sums to match channel.Coins + if !channel.Coins.IsEqual(update.Payout.Sum()) { + return sdk.ErrInternal("Payout amount doesn't match channel amount") + } + // Check sender signature is OK + if !k.verifySignatures(ctx, channel, update) { + return sdk.ErrInternal("Signature on update not valid") + } + return nil +} + // unsafe close channel - doesn't check if update matches existing channel TODO make safer? func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { var err sdk.Error var tags sdk.Tags + channel, _ := k.getChannel(ctx, update.ChannelID) + // TODO check channel exists and participants matches update payout length + // Add coins to sender and receiver // TODO check for possible errors first to avoid coins being half paid out? - for _, payout := range update.Payouts { + for i, coins := range update.Payout { // TODO check somewhere if coins are not negative? - _, tags, err = k.coinKeeper.AddCoins(ctx, payout.Address, payout.Coins) + _, tags, err = k.coinKeeper.AddCoins(ctx, channel.Participants[i], coins) if err != nil { panic(err) } @@ -180,6 +206,23 @@ func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Erro return tags, nil } +func (k Keeper) verifySignatures(ctx sdk.Context, channel Channel, update Update) bool { + // In non unidirectional channels there will be more than one signature to check + + signBytes := update.GetSignBytes() + + address := channel.Participants[0] + pubKey := update.Sigs[0].PubKey + cryptoSig := update.Sigs[0].CryptoSignature + + // Check public key submitted with update signature matches the account address + valid := bytes.Equal(pubKey.Address(), address) && + // Check the signature is correct + pubKey.VerifyBytes(signBytes, cryptoSig) + return valid + +} + // =========================================== QUEUE func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { @@ -239,7 +282,7 @@ func (k Keeper) getSubmittedUpdatesQueueKey() []byte { } // ============= SUBMITTED UPDATES -// These are keyed by the IDs of thei associated Channels +// These are keyed by the IDs of their associated Channels // This section deals with only setting and getting func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) { @@ -336,82 +379,3 @@ func (k Keeper) getChannelKey(channelID ChannelID) []byte { func (k Keeper) getLastChannelIDKey() []byte { return []byte("lastChannelID") } - -/* -// Close a payment channel and distribute funds to participants. -func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64, receiverAmount sdk.Coins) (sdk.Tags, sdk.Error) { - if len(sender) == 0 { - return nil, sdk.ErrInvalidAddress(sender.String()) - } - if len(receiver) == 0 { - return nil, sdk.ErrInvalidAddress(receiver.String()) - } - if len(receiverAmount) == 0 { - return nil, sdk.ErrInvalidCoins(receiverAmount.String()) - } - // check id ≥ 0 - if id < 0 { - return nil, sdk.ErrInvalidAddress(strconv.Itoa(int(id))) // TODO implement custom errors - } - - // Check if coins are sorted, non zero, non negative - if !receiverAmount.IsValid() { - return nil, sdk.ErrInvalidCoins(receiverAmount.String()) - } - if !receiverAmount.IsPositive() { - return nil, sdk.ErrInvalidCoins(receiverAmount.String()) - } - - store := ctx.KVStore(k.storeKey) - - pych, exists := k.GetPaychan(ctx, sender, receiver, id) - if !exists { - return nil, sdk.ErrUnknownAddress("paychan not found") // TODO implement custom errors - } - // compute coin distribution - senderAmount := pych.Balance.Minus(receiverAmount) // Minus sdk.Coins method - // check that receiverAmt not greater than paychan balance - if !senderAmount.IsNotNegative() { - return nil, sdk.ErrInsufficientFunds(pych.Balance.String()) - } - // add coins to sender - // creating account if it doesn't exist - k.coinKeeper.AddCoins(ctx, sender, senderAmount) - // add coins to receiver - k.coinKeeper.AddCoins(ctx, receiver, receiverAmount) - - // delete paychan from db - pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id) - store.Delete(pychKey) - - // TODO create tags - //sdk.NewTags( - // "action", []byte("channel closure"), - // "receiver", receiver.Bytes(), - // "sender", sender.Bytes(), - // "id", ??) - tags := sdk.NewTags() - return tags, nil -} - -// Creates a key to reference a paychan in the blockchain store. -func paychanKey(sender sdk.Address, receiver sdk.Address, id int64) []byte { - - //sdk.Address is just a slice of bytes under a different name - //convert id to string then to byte slice - idAsBytes := []byte(strconv.Itoa(int(id))) - // concat sender and receiver and integer ID - key := append(sender.Bytes(), receiver.Bytes()...) - key = append(key, idAsBytes...) - return key -} - -// Get all paychans between a given sender and receiver. -func (k Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan { - var paychans []Paychan - // TODO Implement this - return paychans -} - -// maybe getAllPaychans(sender sdk.address) []Paychan -*/ diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 9fd59a87..de6e0aa7 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -4,74 +4,152 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" //"github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto" + //"github.com/tendermint/tendermint/crypto" "testing" ) func TestKeeper(t *testing.T) { - t.Run("CreateChannel", func(t *testing.T) { - // - ////// SETUP - // create basic mock app - ctx, coinKeeper, channelKeeper, addrs, genAccFunding := createMockApp() + accountSeeds := []string{"senderSeed", "receiverSeed"} + const ( + senderAccountIndex int = 0 + receiverAccountIndex int = 1 + ) + _, addrs, _, _ := createTestGenAccounts(accountSeeds, sdk.Coins{}) // pure function - sender := addrs[0] - receiver := addrs[1] - coins := sdk.Coins{sdk.NewCoin("KVA", 10)} - - // - ////// ACTION - _, err := channelKeeper.CreateChannel(ctx, sender, receiver, coins) - - // - ////// CHECK RESULTS - assert.Nil(t, err) - // channel exists with correct attributes - channelID := ChannelID(0) // should be 0 as first channel - expectedChan := Channel{ - ID: channelID, - Participants: [2]sdk.AccAddress{sender, receiver}, - Coins: coins, + testCases := []struct { + name string + sender sdk.AccAddress + receiver sdk.AccAddress + coins sdk.Coins + shouldCreateChannel bool + shouldError bool + }{ + { + "HappyPath", + addrs[senderAccountIndex], + addrs[receiverAccountIndex], + sdk.Coins{sdk.NewCoin("KVA", 10)}, + true, + false, + }, + { + "NilAddress", + sdk.AccAddress{}, + sdk.AccAddress{}, + sdk.Coins{sdk.NewCoin("KVA", 10)}, + false, + true, + }, + { + "NilCoins", + addrs[senderAccountIndex], + addrs[receiverAccountIndex], + sdk.Coins{}, + false, + true, + }, + { + "NegativeCoins", + addrs[senderAccountIndex], + addrs[receiverAccountIndex], + sdk.Coins{sdk.NewCoin("KVA", -57)}, + false, + true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ////// SETUP + // create basic mock app + ctx, coinKeeper, channelKeeper, addrs, _, _, genAccFunding := createMockApp(accountSeeds) + // + ////// ACTION + _, err := channelKeeper.CreateChannel(ctx, testCase.sender, testCase.receiver, testCase.coins) + + // + ////// CHECK RESULTS + // Check error + if testCase.shouldError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + // Check if channel exists and is correct + channelID := ChannelID(0) // should be 0 as first channel + createdChan, found := channelKeeper.getChannel(ctx, channelID) + + if testCase.shouldCreateChannel { + expectedChan := Channel{ + ID: channelID, + Participants: [2]sdk.AccAddress{testCase.sender, testCase.receiver}, + Coins: testCase.coins, + } + + // channel exists and correct + assert.True(t, found) + assert.Equal(t, expectedChan, createdChan) + // check coins deducted from sender + assert.Equal(t, genAccFunding.Minus(testCase.coins), coinKeeper.GetCoins(ctx, testCase.sender)) + // check no coins deducted from receiver + assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, testCase.receiver)) + // check next global channelID incremented + assert.Equal(t, ChannelID(1), channelKeeper.getNewChannelID(ctx)) + } else { + // channel doesn't exist + assert.False(t, found) + assert.Equal(t, Channel{}, createdChan) + // check no coins deducted from sender + assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[senderAccountIndex])) + // check no coins deducted from receiver + assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex])) + // check next global channelID not incremented + assert.Equal(t, ChannelID(0), channelKeeper.getNewChannelID(ctx)) + } + }) } - createdChan, _ := channelKeeper.getChannel(ctx, channelID) - assert.Equal(t, expectedChan, createdChan) - // check coins deducted from sender - assert.Equal(t, genAccFunding.Minus(coins), coinKeeper.GetCoins(ctx, sender)) - // check no coins deducted from receiver - assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, receiver)) - // check next chan id - assert.Equal(t, ChannelID(1), channelKeeper.getNewChannelID(ctx)) }) - t.Run("ReceiverCloseChannel", func(t *testing.T) { - // SETUP - ctx, coinKeeper, channelKeeper, addrs, genAccFunding := createMockApp() + t.Run("CloseChannelByReceiver", func(t *testing.T) { + // TODO convert to table driven and add more test cases + // channel exists or not (assume channels correct) + // various Updates + // submittedUpdates existing or not (assume they are valid) + + // SETUP + accountSeeds := []string{"senderSeed", "receiverSeed"} + const ( + senderAccountIndex int = 0 + receiverAccountIndex int = 1 + ) + ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding := createMockApp(accountSeeds) - sender := addrs[0] - receiver := addrs[1] coins := sdk.Coins{sdk.NewCoin("KVA", 10)} // create new channel channelID := ChannelID(0) // should be 0 as first channel channel := Channel{ ID: channelID, - Participants: [2]sdk.AccAddress{sender, receiver}, + Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]}, Coins: coins, } channelKeeper.setChannel(ctx, channel) // create closing update - payouts := Payouts{ - {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, - {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, - } + payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}} update := Update{ ChannelID: channelID, - Payouts: payouts, - Sigs: [1]crypto.Signature{}, + Payout: payout, + // empty sig } + cryptoSig, _ := privKeys[senderAccountIndex].Sign(update.GetSignBytes()) + update.Sigs = [1]UpdateSignature{UpdateSignature{ + PubKey: pubKeys[senderAccountIndex], + CryptoSignature: cryptoSig, + }} + // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) @@ -80,71 +158,171 @@ func TestKeeper(t *testing.T) { // CHECK RESULTS // no error - assert.Nil(t, err) + assert.NoError(t, err) // coins paid out - senderPayout, _ := payouts.Get(sender) - assert.Equal(t, genAccFunding.Plus(senderPayout), coinKeeper.GetCoins(ctx, sender)) - receiverPayout, _ := payouts.Get(receiver) - assert.Equal(t, genAccFunding.Plus(receiverPayout), coinKeeper.GetCoins(ctx, receiver)) + senderPayout := payout[senderAccountIndex] + assert.Equal(t, genAccFunding.Plus(senderPayout), coinKeeper.GetCoins(ctx, addrs[senderAccountIndex])) + receiverPayout := payout[receiverAccountIndex] + assert.Equal(t, genAccFunding.Plus(receiverPayout), coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex])) // channel deleted _, found := channelKeeper.getChannel(ctx, channelID) assert.False(t, found) }) - t.Run("SenderInitCloseChannel", func(t *testing.T) { - // SETUP - ctx, _, channelKeeper, addrs, _ := createMockApp() + t.Run("InitCloseChannelBySender", func(t *testing.T) { - sender := addrs[0] - receiver := addrs[1] - coins := sdk.Coins{sdk.NewCoin("KVA", 10)} + // TODO do some documentation here + // Ideally this should mock calls to ctx.store.Get/Set - test the side effects without being dependent on implementatino details + // TODO test correct behaviour when a submittedUpdate already exists - // create new channel - channelID := ChannelID(0) // should be 0 as first channel - channel := Channel{ - ID: channelID, - Participants: [2]sdk.AccAddress{sender, receiver}, - Coins: coins, + accountSeeds := []string{"senderSeed", "receiverSeed", "notInChannelSeed"} + const ( + senderAccountIndex int = 0 + receiverAccountIndex int = 1 + otherAccountIndex int = 2 + ) + chanID := ChannelID(0) + + type testUpdate struct { // A parameterised version of an Update for use in specifying test cases. + channelID ChannelID // channelID of submitted update + payout Payout // payout of submitted update + pubKeyAccountIndex int // pubkey of signature of submitted update + sigAccountIndex int // crypto signature of signature of submitted update } - channelKeeper.setChannel(ctx, channel) - - // create closing update - payouts := Payouts{ - {sender, sdk.Coins{sdk.NewCoin("KVA", 3)}}, - {receiver, sdk.Coins{sdk.NewCoin("KVA", 7)}}, + testCases := []struct { + name string + setupChannel bool + updateToSubmit testUpdate + expectedSubmittedUpdate string // "empty" or "sameAsSubmitted" + shouldError bool + }{ + { + "HappyPath", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex}, + "sameAsSubmited", + false, + }, + { + "NoChannel", + false, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex}, + "empty", + true, + }, + { + "NoCoins", + true, + testUpdate{chanID, Payout{sdk.Coins{}}, senderAccountIndex, senderAccountIndex}, + "empty", + true, + }, + { + "NegativeCoins", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", -5)}, sdk.Coins{sdk.NewCoin("KVA", 15)}}, senderAccountIndex, senderAccountIndex}, + "empty", + true, + }, + { + "TooManyCoins", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 100)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex}, + "empty", + true, + }, + { + "WrongSignature", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, otherAccountIndex}, + "empty", + true, + }, + { + "WrongPubKey", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, otherAccountIndex, senderAccountIndex}, + "empty", + true, + }, + { + "ReceiverSigned", + true, + testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, receiverAccountIndex, receiverAccountIndex}, + "empty", + true, + }, } - update := Update{ - ChannelID: channelID, - Payouts: payouts, - Sigs: [1]crypto.Signature{}, + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + // SETUP + ctx, _, channelKeeper, addrs, pubKeys, privKeys, _ := createMockApp(accountSeeds) + // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation + channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) + // create new channel + if testCase.setupChannel { + channel := Channel{ + ID: chanID, // should be 0 as first channel + Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]}, + Coins: sdk.Coins{sdk.NewCoin("KVA", 10)}, + } + channelKeeper.setChannel(ctx, channel) + } + + // create update + // basic values + updateToSubmit := Update{ + ChannelID: testCase.updateToSubmit.channelID, + Payout: testCase.updateToSubmit.payout, + // empty sig + } + // create update's signature + cryptoSig, _ := privKeys[testCase.updateToSubmit.sigAccountIndex].Sign(updateToSubmit.GetSignBytes()) + updateToSubmit.Sigs = [1]UpdateSignature{UpdateSignature{ + PubKey: pubKeys[testCase.updateToSubmit.pubKeyAccountIndex], + CryptoSignature: cryptoSig, + }} + + // ACTION + _, err := channelKeeper.InitCloseChannelBySender(ctx, updateToSubmit) + + // CHECK RESULTS + // Check error + if testCase.shouldError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + // Check submittedUpdate + su, found := channelKeeper.getSubmittedUpdate(ctx, chanID) + switch testCase.expectedSubmittedUpdate { + case "empty": + assert.False(t, found) + assert.Zero(t, su) + case "sameAsSubmitted": + assert.True(t, found) + expectedSU := SubmittedUpdate{updateToSubmit, ChannelDisputeTime} + assert.Equal(t, expectedSU, su) + } + + }) } - // Set empty submittedUpdatesQueue TODO work out proper genesis initialisation - channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{}) - // ACTION - _, err := channelKeeper.InitCloseChannelBySender(ctx, update) - - // CHECK RESULTS - // no error - assert.Nil(t, err) - // submittedupdate in queue and correct - suq, found := channelKeeper.getSubmittedUpdatesQueue(ctx) - assert.True(t, found) - assert.True(t, suq.Contains(channelID)) - - su, found := channelKeeper.getSubmittedUpdate(ctx, channelID) - assert.True(t, found) - expectedSubmittedUpdate := SubmittedUpdate{ - Update: update, - ExecutionTime: ChannelDisputeTime, - } - assert.Equal(t, expectedSubmittedUpdate, su) - // TODO check channel is still in db and coins haven't changed? }) } +/* +func privAndAddr() (crypto.PrivKey, sdk.AccAddress) { + priv := ed25519.GenPrivKey() + addr := sdk.AccAddress(priv.PubKey().Address()) + return priv, addr + +sig, err := priv.Sign(signBytes) +*/ + /* func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { diff --git a/internal/x/paychan/test_common.go b/internal/x/paychan/test_common.go index bb881e7a..ca9940f4 100644 --- a/internal/x/paychan/test_common.go +++ b/internal/x/paychan/test_common.go @@ -4,14 +4,17 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/mock" //"github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" ) // Setup an example app with an in memory DB and the required keepers // Also create two accounts with 1000KVA // Could do with refactoring -func createMockApp() (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, sdk.Coins) { +func createMockApp(accountSeeds []string) (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, []crypto.PubKey, []crypto.PrivKey, sdk.Coins) { mApp := mock.NewApp() // creates a half complete app coinKeeper := bank.NewKeeper(mApp.AccountMapper) @@ -24,9 +27,8 @@ func createMockApp() (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, sdk.Co mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel}) // needs to be called I think to finish setup // create some accounts - numGenAccs := 2 // create two initial accounts genAccFunding := sdk.Coins{sdk.NewCoin("KVA", 1000)} - genAccs, addrs, _, _ := mock.CreateGenAccounts(numGenAccs, genAccFunding) + genAccs, addrs, pubKeys, privKeys := createTestGenAccounts(accountSeeds, genAccFunding) // initialize the app with these accounts mock.SetGenesis(mApp, genAccs) @@ -34,5 +36,27 @@ func createMockApp() (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, sdk.Co mApp.BeginBlock(abci.RequestBeginBlock{}) // going off other module tests ctx := mApp.BaseApp.NewContext(false, abci.Header{}) - return ctx, coinKeeper, channelKeeper, addrs, genAccFunding + return ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding +} + +// CreateTestGenAccounts deterministically generates genesis accounts loaded with coins, and returns +// their addresses, pubkeys, and privkeys. +func createTestGenAccounts(accountSeeds []string, genCoins sdk.Coins) (genAccs []auth.Account, addrs []sdk.AccAddress, pubKeys []crypto.PubKey, privKeys []crypto.PrivKey) { + for _, seed := range accountSeeds { + privKey := ed25519.GenPrivKeyFromSecret([]byte(seed)) + pubKey := privKey.PubKey() + addr := sdk.AccAddress(pubKey.Address()) + + genAcc := &auth.BaseAccount{ + Address: addr, + Coins: genCoins, + } + + genAccs = append(genAccs, genAcc) + privKeys = append(privKeys, privKey) + pubKeys = append(pubKeys, pubKey) + addrs = append(addrs, addr) + } + + return } diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 3ed695b9..7c4c0e38 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -3,7 +3,6 @@ package paychan import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" - "reflect" ) /* CHANNEL TYPES */ @@ -22,25 +21,56 @@ type ChannelID int64 // TODO should this be positive only // The data that is passed between participants as payments, and submitted to the blockchain to close a channel. type Update struct { ChannelID ChannelID - Payouts Payouts //map[string]sdk.Coins // map of bech32 addresses to coins + Payout Payout //map[string]sdk.Coins // map of bech32 addresses to coins //Sequence int64 Not needed for unidirectional channels - Sigs [1]crypto.Signature // only sender needs to sign in unidirectional + Sigs [1]UpdateSignature // only sender needs to sign in unidirectional } -type Payout struct { - Address sdk.AccAddress - Coins sdk.Coins + +func (u Update) GetSignBytes() []byte { + bz, err := msgCdc.MarshalJSON(struct { + ChannelID ChannelID + Payout Payout + }{ + ChannelID: u.ChannelID, + Payout: u.Payout}) + + if err != nil { + panic(err) + } + return sdk.MustSortJSON(bz) +} + +type UpdateSignature struct { + PubKey crypto.PubKey + CryptoSignature crypto.Signature +} + +type Payout []sdk.Coins // a list of coins to be paid to each of Channel.Participants +func (p Payout) IsNotNegative() bool { + result := true + for _, coins := range p { + result = result && coins.IsNotNegative() + } + return result +} +func (p Payout) Sum() sdk.Coins { + var total sdk.Coins + for _, coins := range p { + total = total.Plus(coins.Sort()) + total = total.Sort() + } + return total } -type Payouts []Payout // Get the coins associated with payout address. TODO constrain payouts to only have one entry per address -func (payouts Payouts) Get(addr sdk.AccAddress) (sdk.Coins, bool) { +/*func (payouts Payouts) Get(addr sdk.AccAddress) (sdk.Coins, bool) { for _, p := range payouts { if reflect.DeepEqual(p.Address, addr) { return p.Coins, true } } return nil, false -} +}*/ const ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time diff --git a/internal/x/paychan/types_test.go b/internal/x/paychan/types_test.go index 14d45c39..e4004317 100644 --- a/internal/x/paychan/types_test.go +++ b/internal/x/paychan/types_test.go @@ -1,6 +1,7 @@ package paychan import ( + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" "testing" ) @@ -24,3 +25,21 @@ func TestSubmittedUpdatesQueue(t *testing.T) { assert.Equal(t, expectedQ, q) }) } + +func TestPayout(t *testing.T) { + t.Run("IsNotNegative", func(t *testing.T) { + p := Payout{sdk.Coins{sdk.NewCoin("USD", 4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}} + assert.True(t, p.IsNotNegative()) + + p = Payout{sdk.Coins{sdk.NewCoin("USD", -4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}} + assert.False(t, p.IsNotNegative()) + }) + t.Run("Sum", func(t *testing.T) { + p := Payout{ + sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", -5)}, + sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", 100), sdk.NewCoin("GBP", 1)}, + } + expected := sdk.Coins{sdk.NewCoin("EUR", 2), sdk.NewCoin("GBP", 1), sdk.NewCoin("USD", 95)} + assert.Equal(t, expected, p.Sum()) + }) +} From 3ae217e92717667cecf2603d9d7df7a0d3d6a9db Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 14:37:10 -0400 Subject: [PATCH 22/30] prioritise todos --- internal/x/paychan/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 30a8812c..1d9a312c 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -7,16 +7,17 @@ Simplifications: TODO + - in code TODOs + - write basic cmds + - Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs - chnge module name to "channel"? - Find a better name for Queue - clarify distinction between int slice and abstract queue concept - - refactor queue into one object - - Do all the small functions need to be methods on the keeper or can they just be floating around? - - Tidy up - standardise var names, method descriptions, heading comments - - any problem in signing your own address? - - Gas + - write some sort of integration test - find nicer name for payout - - tags - return channel id - - create custom errors instead of using sdk.ErrInternal - - maybe split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain + - add Gas usage + - add tags (return channel id on creation) + - use custom errors instead of using sdk.ErrInternal + - split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain - consider removing pubKey from UpdateSignature - instead let channel module access accountMapper - remove printout from tests when app initialised + - refactor queue into one object From 3f7bcca4879104b5d5e31ceb41d9a84d5c2b9ed0 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 16:41:40 -0400 Subject: [PATCH 23/30] add commands --- internal/x/paychan/README.md | 2 +- internal/x/paychan/client/cmd/cmd.go | 212 +++++++++++++++++++-------- internal/x/paychan/handler.go | 4 +- internal/x/paychan/keeper.go | 34 +++-- internal/x/paychan/types.go | 6 +- 5 files changed, 180 insertions(+), 78 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 1d9a312c..6adb635c 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -8,7 +8,6 @@ Simplifications: TODO - in code TODOs - - write basic cmds - Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs - chnge module name to "channel"? - Find a better name for Queue - clarify distinction between int slice and abstract queue concept @@ -16,6 +15,7 @@ Simplifications: - find nicer name for payout - add Gas usage - add tags (return channel id on creation) + - refactor cmds to be able to test them, then test them - use custom errors instead of using sdk.ErrInternal - split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain - consider removing pubKey from UpdateSignature - instead let channel module access accountMapper diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index ccc1fb2c..bed4c1c4 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -3,6 +3,7 @@ package cli import ( "encoding/base64" "fmt" + "io/ioutil" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -21,62 +22,54 @@ import ( // list of functions that return pointers to cobra commands // No local storage needed for cli acting as a sender -// Current minimal set of cli commands: -// create paychan - create and fund (sender signs tx) -// generate new update - print a signed update (from sender) -// submit update - send update to chain (either can sign tx) -// Future cli commands: -// create paychan -// close paychan -// get paychan(s) -// send paychan payment -// get balance from receiver - -/* -func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { +func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" - flagAmount := "amount" + flagCoins := "amount" cmd := &cobra.Command{ Use: "create", Short: "Create a new payment channel", - Long: "Create a new payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the payment channel.", + Long: "Create a new unidirectional payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + // ctx.PrintResponse = true TODO is this needed for channelID // Get sender adress - senderAddress, err := ctx.GetFromAddress() + sender, err := ctx.GetFromAddress() if err != nil { return err } // Get receiver address toStr := viper.GetString(flagTo) - receiverAddress, err := sdk.GetAccAddressBech32(toStr) + receiver, err := sdk.GetAccAddressBech32(toStr) if err != nil { return err } // Get channel funding amount - amountString := viper.GetString(flagAmount) - amount, err := sdk.ParseCoins(amountString) + coinsString := viper.GetString(flagCoins) + coins, err := sdk.ParseCoins(coinsString) if err != nil { return err } // Create the create channel msg to send - // TODO write NewMsgCreate func? msg := paychan.MsgCreate{ - Sender: senderAddress, - Receiver: receiverAddress, - Amount: amount, + Participants: []sdk.AccAddress{sender, receiver}, + Coins: coins, } + err = msg.ValidateBasic() + if err != nil { + return err + } + // Build and sign the transaction, then broadcast to the blockchain - res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc) + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err } @@ -84,77 +77,180 @@ func CreatePaychanCmd(cdc *wire.Codec) *cobra.Command { return nil }, } - cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") - cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + cmd.Flags().String(flagTo, "", "Recipient address of the payment channel.") + cmd.Flags().String(flagAmount, "", "Amount of coins to fund the payment channel with.") return cmd } -func GenerateNewStateCmd(cdc *wire.Codec) *cobra.Command { - flagId := "id" - flagTo := "to" - flagAmount := "amount" +func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { + flagId := "id" // ChannelID + flagReceiverAmount := "r-amount" // amount the receiver should received on closing the channel + flagSenderAmount := "s-amount" // cmd := &cobra.Command{ - Use: "new-state", - Short: "Generate a new payment channel state.", - Long: "Generate a new state for an existing payment channel and print it out. The new state is represented as a half signed close transaction, signed by the sender.", + Use: "pay", + Short: "Generate a .", // TODO descriptions + Long: "Generate a new ", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + // ctx.PrintResponse = false TODO is this needed to stop any other output messing up json? // Get sender adress - senderAddress, err := ctx.GetFromAddress() - if err != nil { - return err - } + // senderAddress, err := ctx.GetFromAddress() + // if err != nil { + // return err + // } // Get the paychan id - id := viper.GetInt64(flagId) + id := viper.GetInt64(flagId) // TODO make this default to pulling id from chain - // Get receiver address - toStr := viper.GetString(flagTo) - receiverAddress, err := sdk.GetAccAddressBech32(toStr) + // Get channel receiver amount + senderCoins, err := sdk.ParseCoins(viper.GetString(flagSenderAmount)) if err != nil { return err } - // Get channel receiver amount - amountString := viper.GetString(flagAmount) - amount, err := sdk.ParseCoins(amountString) + receiverCoins, err := sdk.ParseCoins(viper.GetString(flagReceiverAmount)) if err != nil { return err } // create close paychan msg - msg := paychan.MsgClose{ - Sender: senderAddress, - Receiver: receiverAddress, - Id: id, - ReceiverAmount: amount, + update := paychan.Update{ + ChannelID: id, + Payout: paychan.Payout{senderCoins, receiverCoins}, + // empty sigs } - // Sign the msg as the sender - txBytes, err := EnsureSignBuild(ctx, ctx.FromAddressName, msg, cdc) + // Sign the update as the sender + keybase, err := keys.GetKeyBase() if err != nil { return err } + name := ctx.FromAddressName + passphrase, err := ctx.GetPassphraseFromStdin(name) + if err != nil { + return err + } + bz := update.GetSignBytes() + + sig, pubKey, err := keybase.Sign(name, passphrase, bz) + if err != nil { + return err + } + update.Sigs = [1]paychan.UpdateSignature{ + PubKey: pubKey, + CryptoSignature: sig, + } + + // Print out the update + jsonUpdate := cdc.MarshalJSONIndent(update) + fmt.Println(string(jsonUpdate)) - // Print out the signed msg - fmt.Println("txBytes:", txBytes) - //encodedTxBytes := make([]byte, base64.StdEncoding.EncodedLen(len(txBytes))) - encodedTxBytes := base64.StdEncoding.EncodeToString(txBytes) - fmt.Println("base64TxBytes:", encodedTxBytes) return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") - cmd.Flags().String(flagTo, "", "Recipient address of the payment channel") - cmd.Flags().String(flagAmount, "", "Amount of coins to fund the paymetn channel with") + cmd.Flags().String(flagSenderAmount, "", "") + cmd.Flags().String(flagReceiverAmount, "", "") return cmd } +func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command { + + cmd := &cobra.Command{ + Use: "verify", + Short: "", // TODO + Long: "", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + + // read in update + bz, err := ioutil.ReadAll(os.Stdin) + if err != nil { + // TODO add nice message about how to feed in stdin + return err + } + // decode json + var update paychan.Update + cdc.UnmarshalJSON(bz, &update) + +// get the channel from the node + res, err := ctx.QueryStore(paychan.GetChannelKey(update.ChannelID), paychanStoreName) + if len(res) == 0 || err != nil { + return errors.Errorf("channel with ID '%d' does not exist", update.ChannelID) + } + var channel paychan.Channel + cdc.MustUnmarshalBinary(res, &channel) + + //verify + updateIsOK := paychan.Keeper.VerifyUpdate(channel ,update) + + // print result + fmt.Println(updateIsOK) + + return nil + }, + } + + return cmd +} + +func SubmitPaymentChannelCmd(cdc *wire.Codec) *cobra.Command { + + cmd := &cobra.Command{ + Use: "submit", + Short: "", + Long: "", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + + // Create a "client context" stuct populated with info from common flags + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + // ctx.PrintResponse = true TODO is this needed for channelID + + // Get sender adress + submitter, err := ctx.GetFromAddress() + if err != nil { + return err + } + + // read in update + bz, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + // decode json + var update paychan.Update + cdc.UnmarshalJSON(bz, &update) + + // Create the create channel msg to send + msg := paychan.MsgSubmitUpdate{ + Update: update, + Submitter: submitter, + } + err = msg.ValidateBasic() + if err != nil { + return err + } + + // Build and sign the transaction, then broadcast to the blockchain + res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + if err != nil { + return err + } + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil + }, + } + + return cmd +} + +/* func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { flagState := "state" diff --git a/internal/x/paychan/handler.go b/internal/x/paychan/handler.go index c4d949a8..4bd93fd2 100644 --- a/internal/x/paychan/handler.go +++ b/internal/x/paychan/handler.go @@ -45,10 +45,10 @@ func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.R participants := channel.Participants // if only sender signed - if reflect.DeepEqual(msg.submitter, participants[0]) { + if reflect.DeepEqual(msg.Submitter, participants[0]) { tags, err = k.InitCloseChannelBySender(ctx, msg.Update) // else if receiver signed - } else if reflect.DeepEqual(msg.submitter, participants[len(participants)-1]) { + } else if reflect.DeepEqual(msg.Submitter, participants[len(participants)-1]) { tags, err = k.CloseChannelByReceiver(ctx, msg.Update) } diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 318a3435..4c1dc0ca 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -74,7 +74,12 @@ func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver s func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // This is roughly the default path for non unidirectional channels - err := k.validateUpdate(ctx, update) + // get the channel + channel, found := k.getChannel(ctx, update.ChannelID) + if !found { + return nil, sdk.ErrInternal("Channel doesn't exist") + } + err := k.VerifyUpdate(channel, update) if err != nil { return nil, err } @@ -113,7 +118,12 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { - err := k.validateUpdate(ctx, update) + // get the channel + channel, found := k.getChannel(ctx, update.ChannelID) + if !found { + return nil, sdk.ErrInternal("Channel doesn't exist") + } + err := k.VerifyUpdate(channel, update) if err != nil { return nil, err } @@ -152,12 +162,8 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags // return returnUpdate // } -func (k Keeper) validateUpdate(ctx sdk.Context, update Update) sdk.Error { - // Check that channel exists - channel, found := k.getChannel(ctx, update.ChannelID) - if !found { - return sdk.ErrInternal("Channel doesn't exist") - } +func (k Keeper) VerifyUpdate(channel Channel, update Update) sdk.Error { + // Check the num of payout participants match channel participants if len(update.Payout) != len(channel.Participants) { return sdk.ErrInternal("Payout doesn't match number of channel participants") @@ -177,7 +183,7 @@ func (k Keeper) validateUpdate(ctx sdk.Context, update Update) sdk.Error { return sdk.ErrInternal("Payout amount doesn't match channel amount") } // Check sender signature is OK - if !k.verifySignatures(ctx, channel, update) { + if !k.verifySignatures(channel, update) { return sdk.ErrInternal("Signature on update not valid") } return nil @@ -206,7 +212,7 @@ func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Erro return tags, nil } -func (k Keeper) verifySignatures(ctx sdk.Context, channel Channel, update Update) bool { +func (k Keeper) verifySignatures(channel Channel, update Update) bool { // In non unidirectional channels there will be more than one signature to check signBytes := update.GetSignBytes() @@ -326,7 +332,7 @@ func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte { func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) { // load from DB store := ctx.KVStore(k.storeKey) - bz := store.Get(k.getChannelKey(channelID)) + bz := store.Get(k.GetChannelKey(channelID)) var channel Channel if bz == nil { @@ -344,13 +350,13 @@ func (k Keeper) setChannel(ctx sdk.Context, channel Channel) { // marshal bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong // write to db - key := k.getChannelKey(channel.ID) + key := k.GetChannelKey(channel.ID) store.Set(key, bz) // panics if something goes wrong } func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) { store := ctx.KVStore(k.storeKey) - store.Delete(k.getChannelKey(channelID)) + store.Delete(k.GetChannelKey(channelID)) // TODO does this have return values? What happens when key doesn't exist? } @@ -373,7 +379,7 @@ func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID { return newID } -func (k Keeper) getChannelKey(channelID ChannelID) []byte { +func (k Keeper) GetChannelKey(channelID ChannelID) []byte { return []byte(fmt.Sprintf("channel:%d", channelID)) } func (k Keeper) getLastChannelIDKey() []byte { diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 7c4c0e38..3324a231 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -45,7 +45,7 @@ type UpdateSignature struct { CryptoSignature crypto.Signature } -type Payout []sdk.Coins // a list of coins to be paid to each of Channel.Participants +type Payout [2]sdk.Coins // a list of coins to be paid to each of Channel.Participants func (p Payout) IsNotNegative() bool { result := true for _, coins := range p { @@ -195,7 +195,7 @@ func (msg MsgCreate) GetSigners() []sdk.AccAddress { // A message to close a payment channel. type MsgSubmitUpdate struct { Update - submitter sdk.AccAddress + Submitter sdk.AccAddress } // func (msg MsgSubmitUpdate) NewMsgSubmitUpdate(update Update) MsgSubmitUpdate { @@ -247,5 +247,5 @@ func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error { func (msg MsgSubmitUpdate) GetSigners() []sdk.AccAddress { // Signing not strictly necessary as signatures contained within the channel update. // TODO add signature by submitting address - return []sdk.AccAddress{msg.submitter} + return []sdk.AccAddress{msg.Submitter} } From c168718332aea887638146cbb3a776579536bdd5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 19:29:51 -0400 Subject: [PATCH 24/30] fix genesis bug, get app running --- cmd/kvcli/main.go | 21 +++++---- internal/app/app.go | 1 + internal/app/app_test.go | 2 + internal/x/paychan/README.md | 1 - internal/x/paychan/client/cmd/cmd.go | 38 +++++++++-------- internal/x/paychan/client/rest/rest.go | 8 ++-- internal/x/paychan/endblocker.go | 7 ++- internal/x/paychan/endblocker_test.go | 2 +- internal/x/paychan/keeper.go | 59 ++++++++++---------------- internal/x/paychan/keeper_test.go | 2 + internal/x/paychan/types.go | 2 +- 11 files changed, 69 insertions(+), 74 deletions(-) diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index ff980fc3..06f2cc66 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -39,7 +39,7 @@ func main() { // add standard rpc commands rpc.AddCommands(rootCmd) - //Add state commands + // Add state commands tendermintCmd := &cobra.Command{ Use: "tendermint", Short: "Tendermint state querying subcommands", @@ -50,7 +50,7 @@ func main() { ) tx.AddCommands(tendermintCmd, cdc) - //Add IBC commands + // Add IBC commands // ibcCmd := &cobra.Command{ // Use: "ibc", // Short: "Inter-Blockchain Communication subcommands", @@ -76,7 +76,7 @@ func main() { client.LineBreak, ) - //Add stake commands + // Add stake commands stakeCmd := &cobra.Command{ Use: "stake", Short: "Stake and validation subcommands", @@ -102,7 +102,7 @@ func main() { stakeCmd, ) - //Add stake commands + // Add gov commands // govCmd := &cobra.Command{ // Use: "gov", // Short: "Governance and voting subcommands", @@ -123,7 +123,7 @@ func main() { // govCmd, // ) - //Add auth and bank commands + // Add auth and bank commands rootCmd.AddCommand( client.GetCommands( authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)), @@ -133,19 +133,22 @@ func main() { bankcmd.SendTxCmd(cdc), )...) + // Add paychan commands paychanCmd := &cobra.Command{ Use: "paychan", - Short: "Payment channel subcommands", + Short: "Payment channel subcommand", } paychanCmd.AddCommand( client.PostCommands( - paychancmd.CreatePaychanCmd(cdc), - paychancmd.GenerateNewStateCmd(cdc), - paychancmd.ClosePaychanCmd(cdc), + paychancmd.CreateChannelCmd(cdc), + paychancmd.GeneratePaymentCmd(cdc), + paychancmd.VerifyPaymentCmd(cdc, "paychan"), // pass in storeKey + paychancmd.SubmitPaymentCmd(cdc), )...) rootCmd.AddCommand( paychanCmd, ) + // add proxy, version and key info rootCmd.AddCommand( keys.Commands(), diff --git a/internal/app/app.go b/internal/app/app.go index faf3595a..1329ca4d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -151,6 +151,7 @@ func (app *KavaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ab // The function baseapp runs on receipt of a EndBlock ABCI message func (app *KavaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + paychan.EndBlocker(ctx, app.paychanKeeper) validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper) //tags, _ := gov.EndBlocker(ctx, app.govKeeper) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 39d82033..f4e25f89 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -1,5 +1,6 @@ package app +/* import ( "os" "testing" @@ -75,3 +76,4 @@ func TestGenesis(t *testing.T) { res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address) assert.Equal(t, acc, res1) } +*/ diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 6adb635c..858c9217 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -9,7 +9,6 @@ Simplifications: TODO - in code TODOs - Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs - - chnge module name to "channel"? - Find a better name for Queue - clarify distinction between int slice and abstract queue concept - write some sort of integration test - find nicer name for payout diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index bed4c1c4..d10f4883 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -1,9 +1,9 @@ package cli import ( - "encoding/base64" "fmt" "io/ioutil" + "os" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -13,7 +13,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/keys" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" - "github.com/cosmos/cosmos-sdk/x/auth" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/kava-labs/kava/internal/x/paychan" @@ -22,7 +21,6 @@ import ( // list of functions that return pointers to cobra commands // No local storage needed for cli acting as a sender - func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { flagTo := "to" flagCoins := "amount" @@ -46,7 +44,7 @@ func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { // Get receiver address toStr := viper.GetString(flagTo) - receiver, err := sdk.GetAccAddressBech32(toStr) + receiver, err := sdk.AccAddressFromBech32(toStr) if err != nil { return err } @@ -60,7 +58,7 @@ func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { // Create the create channel msg to send msg := paychan.MsgCreate{ - Participants: []sdk.AccAddress{sender, receiver}, + Participants: [2]sdk.AccAddress{sender, receiver}, Coins: coins, } err = msg.ValidateBasic() @@ -69,16 +67,15 @@ func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { } // Build and sign the transaction, then broadcast to the blockchain - res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err } - fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) return nil }, } cmd.Flags().String(flagTo, "", "Recipient address of the payment channel.") - cmd.Flags().String(flagAmount, "", "Amount of coins to fund the payment channel with.") + cmd.Flags().String(flagCoins, "", "Amount of coins to fund the payment channel with.") return cmd } @@ -105,7 +102,7 @@ func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { // } // Get the paychan id - id := viper.GetInt64(flagId) // TODO make this default to pulling id from chain + id := paychan.ChannelID(viper.GetInt64(flagId)) // TODO make this default to pulling id from chain // Get channel receiver amount senderCoins, err := sdk.ParseCoins(viper.GetString(flagSenderAmount)) @@ -141,13 +138,16 @@ func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { if err != nil { return err } - update.Sigs = [1]paychan.UpdateSignature{ + update.Sigs = [1]paychan.UpdateSignature{{ PubKey: pubKey, CryptoSignature: sig, - } + }} // Print out the update - jsonUpdate := cdc.MarshalJSONIndent(update) + jsonUpdate, err := wire.MarshalJSONIndent(cdc, update) + if err != nil { + return err + } fmt.Println(string(jsonUpdate)) return nil @@ -159,7 +159,7 @@ func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { return cmd } -func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command { +func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { cmd := &cobra.Command{ Use: "verify", @@ -168,6 +168,9 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + // Create a "client context" stuct populated with info from common flags + ctx := context.NewCoreContextFromViper() + // read in update bz, err := ioutil.ReadAll(os.Stdin) if err != nil { @@ -178,7 +181,7 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command var update paychan.Update cdc.UnmarshalJSON(bz, &update) -// get the channel from the node + // get the channel from the node res, err := ctx.QueryStore(paychan.GetChannelKey(update.ChannelID), paychanStoreName) if len(res) == 0 || err != nil { return errors.Errorf("channel with ID '%d' does not exist", update.ChannelID) @@ -187,7 +190,7 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command cdc.MustUnmarshalBinary(res, &channel) //verify - updateIsOK := paychan.Keeper.VerifyUpdate(channel ,update) + updateIsOK := paychan.VerifyUpdate(channel, update) // print result fmt.Println(updateIsOK) @@ -199,7 +202,7 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName, string) *cobra.Command return cmd } -func SubmitPaymentChannelCmd(cdc *wire.Codec) *cobra.Command { +func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "submit", @@ -238,11 +241,10 @@ func SubmitPaymentChannelCmd(cdc *wire.Codec) *cobra.Command { } // Build and sign the transaction, then broadcast to the blockchain - res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) if err != nil { return err } - fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) return nil }, } diff --git a/internal/x/paychan/client/rest/rest.go b/internal/x/paychan/client/rest/rest.go index d25e01cb..926a3474 100644 --- a/internal/x/paychan/client/rest/rest.go +++ b/internal/x/paychan/client/rest/rest.go @@ -1,10 +1,10 @@ package rest import ( - "github.com/gorilla/mux" - //"github.com/tendermint/go-crypto/keys" - //"github.com/cosmos/cosmos-sdk/client/context" - //"github.com/cosmos/cosmos-sdk/wire" +//"github.com/gorilla/mux" +//"github.com/tendermint/go-crypto/keys" +//"github.com/cosmos/cosmos-sdk/client/context" +//"github.com/cosmos/cosmos-sdk/wire" ) /* diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go index d4c9c777..d51a0c66 100644 --- a/internal/x/paychan/endblocker.go +++ b/internal/x/paychan/endblocker.go @@ -11,11 +11,10 @@ func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { // Iterate through submittedUpdatesQueue // TODO optimise so it doesn't pull every update from DB every block + q := k.getSubmittedUpdatesQueue(ctx) var sUpdate SubmittedUpdate - q, found := k.getSubmittedUpdatesQueue(ctx) - if !found { - panic("SubmittedUpdatesQueue not found.") - } + var found bool + for _, id := range q { // close the channel if the update has reached its execution time. // Using >= in case some are somehow missed. diff --git a/internal/x/paychan/endblocker_test.go b/internal/x/paychan/endblocker_test.go index dd4cce82..655a2fa1 100644 --- a/internal/x/paychan/endblocker_test.go +++ b/internal/x/paychan/endblocker_test.go @@ -49,7 +49,7 @@ func TestEndBlocker(t *testing.T) { _, found := channelKeeper.getChannel(ctx, channelID) assert.False(t, found) // check queue is empty, NOTE: due to encoding, an empty queue (underneath just an int slice) will be decoded as nil slice rather than an empty slice - suq, _ := channelKeeper.getSubmittedUpdatesQueue(ctx) + suq := channelKeeper.getSubmittedUpdatesQueue(ctx) assert.Equal(t, SubmittedUpdatesQueue(nil), suq) // check submittedUpdate is gone _, found = channelKeeper.getSubmittedUpdate(ctx, channelID) diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 4c1dc0ca..af648315 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -79,15 +79,12 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta if !found { return nil, sdk.ErrInternal("Channel doesn't exist") } - err := k.VerifyUpdate(channel, update) + err := VerifyUpdate(channel, update) if err != nil { return nil, err } - q, found := k.getSubmittedUpdatesQueue(ctx) - if !found { - panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors - } + q := k.getSubmittedUpdatesQueue(ctx) if q.Contains(update.ChannelID) { // Someone has previously tried to update channel // In bidirectional channels the new update is compared against existing and replaces it if it has a higher sequence number. @@ -123,16 +120,13 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags if !found { return nil, sdk.ErrInternal("Channel doesn't exist") } - err := k.VerifyUpdate(channel, update) + err := VerifyUpdate(channel, update) if err != nil { return nil, err } // Check if there is an update in the queue already - q, found := k.getSubmittedUpdatesQueue(ctx) - if !found { - panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors - } + q := k.getSubmittedUpdatesQueue(ctx) if q.Contains(update.ChannelID) { // Someone has previously tried to update channel but receiver has final say k.removeFromSubmittedUpdatesQueue(ctx, update.ChannelID) @@ -162,7 +156,7 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags // return returnUpdate // } -func (k Keeper) VerifyUpdate(channel Channel, update Update) sdk.Error { +func VerifyUpdate(channel Channel, update Update) sdk.Error { // Check the num of payout participants match channel participants if len(update.Payout) != len(channel.Participants) { @@ -183,7 +177,7 @@ func (k Keeper) VerifyUpdate(channel Channel, update Update) sdk.Error { return sdk.ErrInternal("Payout amount doesn't match channel amount") } // Check sender signature is OK - if !k.verifySignatures(channel, update) { + if !verifySignatures(channel, update) { return sdk.ErrInternal("Signature on update not valid") } return nil @@ -212,7 +206,7 @@ func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Erro return tags, nil } -func (k Keeper) verifySignatures(channel Channel, update Update) bool { +func verifySignatures(channel Channel, update Update) bool { // In non unidirectional channels there will be more than one signature to check signBytes := update.GetSignBytes() @@ -234,10 +228,7 @@ func (k Keeper) verifySignatures(channel Channel, update Update) bool { func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { // always overwrite prexisting values - leave paychan logic to higher levels // get current queue - q, found := k.getSubmittedUpdatesQueue(ctx) - if !found { - panic("SubmittedUpdatesQueue not found.") - } + q := k.getSubmittedUpdatesQueue(ctx) // append ID to queue if !q.Contains(sUpdate.ChannelID) { q = append(q, sUpdate.ChannelID) @@ -249,10 +240,7 @@ func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpd } func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID ChannelID) { // get current queue - q, found := k.getSubmittedUpdatesQueue(ctx) - if !found { - panic("SubmittedUpdatesQueue not found.") - } + q := k.getSubmittedUpdatesQueue(ctx) // remove id q.RemoveMatchingElements(channelID) // set queue @@ -261,19 +249,18 @@ func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID Chann k.deleteSubmittedUpdate(ctx, channelID) } -func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (SubmittedUpdatesQueue, bool) { +func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) SubmittedUpdatesQueue { // load from DB store := ctx.KVStore(k.storeKey) bz := store.Get(k.getSubmittedUpdatesQueueKey()) - var suq SubmittedUpdatesQueue - if bz == nil { - return suq, false // TODO maybe create custom error to pass up here + var suq SubmittedUpdatesQueue // if the submittedUpdatesQueue not found then return an empty one + if bz != nil { + // unmarshal + k.cdc.MustUnmarshalBinary(bz, &suq) } - // unmarshal - k.cdc.MustUnmarshalBinary(bz, &suq) - // return - return suq, true + return suq + } func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, suq SubmittedUpdatesQueue) { store := ctx.KVStore(k.storeKey) @@ -332,7 +319,7 @@ func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte { func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) { // load from DB store := ctx.KVStore(k.storeKey) - bz := store.Get(k.GetChannelKey(channelID)) + bz := store.Get(GetChannelKey(channelID)) var channel Channel if bz == nil { @@ -350,13 +337,13 @@ func (k Keeper) setChannel(ctx sdk.Context, channel Channel) { // marshal bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong // write to db - key := k.GetChannelKey(channel.ID) + key := GetChannelKey(channel.ID) store.Set(key, bz) // panics if something goes wrong } func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) { store := ctx.KVStore(k.storeKey) - store.Delete(k.GetChannelKey(channelID)) + store.Delete(GetChannelKey(channelID)) // TODO does this have return values? What happens when key doesn't exist? } @@ -364,7 +351,7 @@ func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID { // get last channel ID var lastID ChannelID store := ctx.KVStore(k.storeKey) - bz := store.Get(k.getLastChannelIDKey()) + bz := store.Get(getLastChannelIDKey()) if bz == nil { lastID = -1 // TODO is just setting to zero if uninitialized ok? } else { @@ -374,14 +361,14 @@ func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID { newID := lastID + 1 bz = k.cdc.MustMarshalBinary(newID) // set last channel id again - store.Set(k.getLastChannelIDKey(), bz) + store.Set(getLastChannelIDKey(), bz) // return return newID } -func (k Keeper) GetChannelKey(channelID ChannelID) []byte { +func GetChannelKey(channelID ChannelID) []byte { return []byte(fmt.Sprintf("channel:%d", channelID)) } -func (k Keeper) getLastChannelIDKey() []byte { +func getLastChannelIDKey() []byte { return []byte("lastChannelID") } diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index de6e0aa7..7435130b 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -11,6 +11,8 @@ import ( func TestKeeper(t *testing.T) { t.Run("CreateChannel", func(t *testing.T) { + // TODO test for receiver account not existing (OK) and sender not existing (not ok) + accountSeeds := []string{"senderSeed", "receiverSeed"} const ( senderAccountIndex int = 0 diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 3324a231..a1135282 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -80,7 +80,7 @@ type SubmittedUpdate struct { ExecutionTime int64 // BlockHeight } -type SubmittedUpdatesQueue []ChannelID +type SubmittedUpdatesQueue []ChannelID // not technically a queue // Check if value is in queue func (suq SubmittedUpdatesQueue) Contains(channelID ChannelID) bool { From f58262c8b0aea6c1184714b4fbde72189d2ab6ac Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 22:22:50 -0400 Subject: [PATCH 25/30] improve command UX --- cmd/kvcli/main.go | 1 + internal/x/paychan/README.md | 4 + internal/x/paychan/client/cmd/cmd.go | 215 +++++++-------------------- internal/x/paychan/wire.go | 6 +- 4 files changed, 64 insertions(+), 162 deletions(-) diff --git a/cmd/kvcli/main.go b/cmd/kvcli/main.go index 06f2cc66..acde7bc0 100644 --- a/cmd/kvcli/main.go +++ b/cmd/kvcli/main.go @@ -141,6 +141,7 @@ func main() { paychanCmd.AddCommand( client.PostCommands( paychancmd.CreateChannelCmd(cdc), + paychancmd.GetChannelCmd(cdc, "paychan"), // pass in storeKey paychancmd.GeneratePaymentCmd(cdc), paychancmd.VerifyPaymentCmd(cdc, "paychan"), // pass in storeKey paychancmd.SubmitPaymentCmd(cdc), diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 858c9217..c380ba21 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -11,10 +11,14 @@ Simplifications: - Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs - Find a better name for Queue - clarify distinction between int slice and abstract queue concept - write some sort of integration test + - possible bug in submitting same update repeatedly - find nicer name for payout - add Gas usage - add tags (return channel id on creation) - refactor cmds to be able to test them, then test them + - verify doesn’t throw json parsing error on invalid json + - can’t submit an update from an unitialised account + - pay without a --from returns confusing error - use custom errors instead of using sdk.ErrInternal - split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain - consider removing pubKey from UpdateSignature - instead let channel module access accountMapper diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index d10f4883..12dfd541 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -3,7 +3,6 @@ package cli import ( "fmt" "io/ioutil" - "os" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -34,7 +33,8 @@ func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - // ctx.PrintResponse = true TODO is this needed for channelID + // TODO is this needed for channelID + // ctx.PrintResponse = true // Get sender adress sender, err := ctx.GetFromAddress() @@ -80,26 +80,20 @@ func CreateChannelCmd(cdc *wire.Codec) *cobra.Command { } func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { - flagId := "id" // ChannelID - flagReceiverAmount := "r-amount" // amount the receiver should received on closing the channel - flagSenderAmount := "s-amount" // + flagId := "chan-id" + flagReceiverAmount := "rec-amt" // amount the receiver should received on closing the channel + flagSenderAmount := "sen-amt" + flagPaymentFile := "filename" cmd := &cobra.Command{ Use: "pay", - Short: "Generate a .", // TODO descriptions - Long: "Generate a new ", + Short: "Generate a new payment.", // TODO descriptions + Long: "Generate a payment file (json) to send to the receiver as a payment.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { // Create a "client context" stuct populated with info from common flags ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - // ctx.PrintResponse = false TODO is this needed to stop any other output messing up json? - - // Get sender adress - // senderAddress, err := ctx.GetFromAddress() - // if err != nil { - // return err - // } // Get the paychan id id := paychan.ChannelID(viper.GetInt64(flagId)) // TODO make this default to pulling id from chain @@ -143,28 +137,35 @@ func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command { CryptoSignature: sig, }} - // Print out the update + // Write out the update jsonUpdate, err := wire.MarshalJSONIndent(cdc, update) if err != nil { return err } - fmt.Println(string(jsonUpdate)) + paymentFile := viper.GetString(flagPaymentFile) + err = ioutil.WriteFile(paymentFile, jsonUpdate, 0644) + if err != nil { + return err + } + fmt.Printf("Written payment out to %v.\n", paymentFile) return nil }, } cmd.Flags().Int(flagId, 0, "ID of the payment channel.") - cmd.Flags().String(flagSenderAmount, "", "") - cmd.Flags().String(flagReceiverAmount, "", "") + cmd.Flags().String(flagSenderAmount, "", "Total coins to payout to sender on channel close.") + cmd.Flags().String(flagReceiverAmount, "", "Total coins to payout to sender on channel close.") + cmd.Flags().String(flagPaymentFile, "payment.json", "File name to write the payment into.") return cmd } func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { + flagPaymentFile := "payment" cmd := &cobra.Command{ Use: "verify", - Short: "", // TODO - Long: "", + Short: "Verify a payment file.", + Long: "Verify that a received payment can be used to close a channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -172,7 +173,7 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { ctx := context.NewCoreContextFromViper() // read in update - bz, err := ioutil.ReadAll(os.Stdin) + bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile)) if err != nil { // TODO add nice message about how to feed in stdin return err @@ -190,24 +191,30 @@ func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { cdc.MustUnmarshalBinary(res, &channel) //verify - updateIsOK := paychan.VerifyUpdate(channel, update) + verificationError := paychan.VerifyUpdate(channel, update) // print result - fmt.Println(updateIsOK) - + if verificationError == nil { + fmt.Printf("Payment is valid for channel '%d'.\n", update.ChannelID) + } else { + fmt.Printf("Payment is NOT valid for channel '%d'.\n", update.ChannelID) + fmt.Println(verificationError) + } return nil }, } + cmd.Flags().String(flagPaymentFile, "payment.json", "File name to read the payment from.") return cmd } func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command { + flagPaymentFile := "payment" cmd := &cobra.Command{ Use: "submit", - Short: "", - Long: "", + Short: "Submit a payment to the blockchain to close the channel.", + Long: fmt.Sprintf("Submit a payment to the blockchain to either close a channel immediately (if you are the receiver) or after a dispute period of %d blocks (if you are the sender).", paychan.ChannelDisputeTime), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -222,7 +229,7 @@ func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command { } // read in update - bz, err := ioutil.ReadAll(os.Stdin) + bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile)) if err != nil { return err } @@ -248,152 +255,44 @@ func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command { return nil }, } - + cmd.Flags().String(flagPaymentFile, "payment.json", "File to read the payment from.") return cmd } -/* -func ClosePaychanCmd(cdc *wire.Codec) *cobra.Command { - flagState := "state" - +func GetChannelCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { + flagId := "chan-id" cmd := &cobra.Command{ - Use: "close", - Short: "Close a payment channel, given a state", - Long: "Close an existing payment channel with a state received from a sender. This signs it as the receiver before submitting to the blockchain.", + Use: "get", + Short: "Get info on a channel.", + Long: "Get information on a non closed channel.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) - // Get the sender-signed close tx - state := viper.GetString(flagState) - txBytes, err := base64.StdEncoding.DecodeString(state) - if err != nil { - return err + // Create a "client context" stuct populated with info from common flags + ctx := context.NewCoreContextFromViper() + + // Get channel ID + id := paychan.ChannelID(viper.GetInt64(flagId)) + + // Get the channel from the node + res, err := ctx.QueryStore(paychan.GetChannelKey(id), paychanStoreName) + if len(res) == 0 || err != nil { + return errors.Errorf("channel with ID '%d' does not exist", id) } - stdTx := auth.StdTx{} - cdc.UnmarshalBinary(txBytes, &stdTx) + var channel paychan.Channel + cdc.MustUnmarshalBinary(res, &channel) - // Sign close tx - - // ensure contxt has up to date account and sequence numbers - ctx, err = Ensure(ctx) - if err != nil { - return err - } - // Sign message (asks user for password) - _, sig, err := UserSignMsg(ctx, ctx.FromAddressName, stdTx.Msg) + // Convert the channel to a json object for pretty printing + jsonChannel, err := wire.MarshalJSONIndent(cdc, channel) if err != nil { return err } - // Append signature to close tx - stdTx.Signatures = append(stdTx.Signatures, sig) - // encode close tx - txBytes, err = cdc.MarshalBinary(stdTx) - if err != nil { - return err - } - - // Broadcast close tx to the blockchain - - res, err := ctx.BroadcastTx(txBytes) - if err != nil { - return err - } - fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + // print out json channel + fmt.Println(string(jsonChannel)) return nil }, } - cmd.Flags().String(flagState, "", "State received from sender.") + cmd.Flags().Int(flagId, 0, "ID of the payment channel.") return cmd } - -// HELPER FUNCTIONS -// This is a partial refactor of cosmos-sdk/client/context. -// Existing API was awkard to use for paychans. - -func Ensure(ctx context.CoreContext) (context.CoreContext, error) { - - ctx, err := context.EnsureAccountNumber(ctx) - if err != nil { - return ctx, err - } - // default to next sequence number if none provided - ctx, err = context.EnsureSequence(ctx) - if err != nil { - return ctx, err - } - return ctx, nil -} - -func UserSignMsg(ctx context.CoreContext, name string, msg sdk.Msg) (signMsg auth.StdSignMsg, stdSig auth.StdSignature, err error) { - - // TODO check how to handle non error return values on error. Returning empty versions doesn't seem right. - - passphrase, err := ctx.GetPassphraseFromStdin(name) - if err != nil { - return signMsg, stdSig, err - } - - // build the Sign Messsage from the Standard Message - chainID := ctx.ChainID - if chainID == "" { - return signMsg, stdSig, errors.Errorf("Chain ID required but not specified") - } - accnum := ctx.AccountNumber - sequence := ctx.Sequence - - signMsg = auth.StdSignMsg{ - ChainID: chainID, - AccountNumbers: []int64{accnum}, - Sequences: []int64{sequence}, - Msg: msg, - Fee: auth.NewStdFee(ctx.Gas, sdk.Coin{}), // TODO run simulate to estimate gas? - } - - keybase, err := keys.GetKeyBase() - if err != nil { - return signMsg, stdSig, err - } - - // sign and build - bz := signMsg.Bytes() - - sig, pubkey, err := keybase.Sign(name, passphrase, bz) - if err != nil { - return signMsg, stdSig, err - } - stdSig = auth.StdSignature{ - PubKey: pubkey, - Signature: sig, - AccountNumber: accnum, - Sequence: sequence, - } - - return signMsg, stdSig, nil -} - -func Build(cdc *wire.Codec, signMsg auth.StdSignMsg, sig auth.StdSignature) ([]byte, error) { - tx := auth.NewStdTx(signMsg.Msg, signMsg.Fee, []auth.StdSignature{sig}) - return cdc.MarshalBinary(tx) -} - -func EnsureSignBuild(ctx context.CoreContext, name string, msg sdk.Msg, cdc *wire.Codec) ([]byte, error) { - //Ensure context has up to date account and sequence numbers - ctx, err := Ensure(ctx) - if err != nil { - return nil, err - } - // Sign message (asks user for password) - signMsg, sig, err := UserSignMsg(ctx, name, msg) - if err != nil { - return nil, err - } - // Create tx and marshal - txBytes, err := Build(cdc, signMsg, sig) - if err != nil { - return nil, err - } - return txBytes, nil -} -*/ diff --git a/internal/x/paychan/wire.go b/internal/x/paychan/wire.go index 98c7817e..ee3da559 100644 --- a/internal/x/paychan/wire.go +++ b/internal/x/paychan/wire.go @@ -9,12 +9,10 @@ func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(MsgSubmitUpdate{}, "paychan/MsgSubmitUpdate", nil) } +// TODO move this to near the msg definitions var msgCdc = wire.NewCodec() -/* func init() { + wire.RegisterCrypto(msgCdc) RegisterWire(msgCdc) - // TODO is this needed? - //wire.RegisterCrypto(msgCdc) } -*/ From 894509f1dab5fa6a3e1cdc29262192c04b59d297 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 1 Sep 2018 22:57:42 -0400 Subject: [PATCH 26/30] add usage instructions --- internal/x/paychan/README.md | 40 +++++- .../x/paychan/client/rest/channel-watcher.go | 4 +- internal/x/paychan/endblocker.go | 3 +- internal/x/paychan/keeper.go | 21 +-- internal/x/paychan/keeper_test.go | 128 ----------------- internal/x/paychan/test_common.go | 5 +- internal/x/paychan/types.go | 131 +++++++----------- internal/x/paychan/wire.go | 2 +- 8 files changed, 101 insertions(+), 233 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index c380ba21..720832e2 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -1,12 +1,40 @@ -Payment channel implementation sketch +# Unidrectional Payment Channels -Simplifications: +This module implements simple but feature complete unidirectional payment channels. Channels can be opened by a sender and closed immediately by the receiver, or by the sender subject to a dispute period. There are no top-ups or partial withdrawals (yet). Channels support multiple currencies. - - unidirectional paychans - - no top ups or partial withdrawals (only opening and closing) +>Note: This is a work in progress. More feature planned. More test cases needed. + +# Usage + +## Create a channel + + kvcli paychan create --from --to --amount 100KVA --chain-id + +## Send an off-chain payment +Send a payment for 10 KVA. + + kvcli paychan pay --from --sen-amt 90KVA --rec-amt 10KVA --chan-id --filename payment.json --chain-id + +Send the file payment.json to your receiver. Then they run the following to verify. + + kvcli paychan verify --filename payment.json + +## Close a channel +The receiver can close immediately at any time. + + kvcli paychan submit --from --payment payment.json --chain-id + +The sender can close subject to a dispute period during which the receiver can overrule them. + + kvcli paychan submit --from --payment payment.json --chain-id + +## Get info on a channel + + kvcli get --chan-id - TODO +# TODOs + - in code TODOs - Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs - Find a better name for Queue - clarify distinction between int slice and abstract queue concept @@ -22,5 +50,5 @@ Simplifications: - use custom errors instead of using sdk.ErrInternal - split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain - consider removing pubKey from UpdateSignature - instead let channel module access accountMapper - - remove printout from tests when app initialised - refactor queue into one object + - remove printout during tests caused by mock app initialisation diff --git a/internal/x/paychan/client/rest/channel-watcher.go b/internal/x/paychan/client/rest/channel-watcher.go index d00c7f51..3f4d1c58 100644 --- a/internal/x/paychan/client/rest/channel-watcher.go +++ b/internal/x/paychan/client/rest/channel-watcher.go @@ -2,6 +2,6 @@ package lcd import () -// implement thing that polls blockchain and handles paychan disputes +// implement a thing to poll blockchain and handles paychan disputes // needs plugged into LCD - add a "background processes" slot in the LCD run function? -// eventually LCD evolves into paychan (network) daemon +// eventually LCD could evolve into paychan (network) daemon diff --git a/internal/x/paychan/endblocker.go b/internal/x/paychan/endblocker.go index d51a0c66..f56f5083 100644 --- a/internal/x/paychan/endblocker.go +++ b/internal/x/paychan/endblocker.go @@ -10,7 +10,7 @@ func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { tags := sdk.EmptyTags() // Iterate through submittedUpdatesQueue - // TODO optimise so it doesn't pull every update from DB every block + // TODO optimise so it doesn't pull every channel update from DB every block q := k.getSubmittedUpdatesQueue(ctx) var sUpdate SubmittedUpdate var found bool @@ -31,6 +31,5 @@ func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags { tags.AppendTags(channelTags) } } - return tags } diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index af648315..47bb90d1 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -11,12 +11,13 @@ import ( // Keeper of the paychan store // Handles validation internally. Does not rely on calling code to do validation. // Aim to keep public methods safe, private ones not necessaily. +// Keepers contain main business logic of the module. type Keeper struct { storeKey sdk.StoreKey cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper - //codespace sdk.CodespaceType + //codespace sdk.CodespaceType TODO custom errors } // Called when creating new app. @@ -30,12 +31,10 @@ func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper { return keeper } -// ============================================== Main Business Logic - // Create a new payment channel and lock up sender funds. func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver sdk.AccAddress, coins sdk.Coins) (sdk.Tags, sdk.Error) { - // Check addresses valid (Technicaly don't need to check sender address is valid as SubtractCoins does that) + // Check addresses valid (Technicaly don't need to check sender address is valid as SubtractCoins checks) if len(sender) == 0 { return nil, sdk.ErrInvalidAddress(sender.String()) } @@ -71,6 +70,7 @@ func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver s return tags, err } +// Initiate the close of a payment channel, subject to dispute period. func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // This is roughly the default path for non unidirectional channels @@ -87,6 +87,7 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta q := k.getSubmittedUpdatesQueue(ctx) if q.Contains(update.ChannelID) { // Someone has previously tried to update channel + // In bidirectional channels the new update is compared against existing and replaces it if it has a higher sequence number. // existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID) @@ -97,7 +98,7 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta // However in unidirectional case, only the sender can close a channel this way. No clear need for them to be able to submit an update replacing a previous one they sent, so don't allow it. // TODO tags - // TODO custom errors return sdk.EmptyTags(), sdk.NewError("Sender can't submit an update for channel if one has already been submitted.") + // TODO custom errors sdk.ErrInternal("Sender can't submit an update for channel if one has already been submitted.") } else { // No one has tried to update channel @@ -113,6 +114,7 @@ func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Ta return tags, nil } +// Immediately close a channel. func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // get the channel @@ -138,8 +140,7 @@ func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags } // Main function that compare updates against each other. -// Pure function -// Not needed in unidirectional case. +// Pure function, Not needed in unidirectional case. // func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate { // var returnUpdate SubmittedUpdate @@ -183,7 +184,8 @@ func VerifyUpdate(channel Channel, update Update) sdk.Error { return nil } -// unsafe close channel - doesn't check if update matches existing channel TODO make safer? +// unsafe close channel - doesn't check if update matches existing channel +// TODO make safer? func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { var err sdk.Error var tags sdk.Tags @@ -194,7 +196,6 @@ func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Erro // Add coins to sender and receiver // TODO check for possible errors first to avoid coins being half paid out? for i, coins := range update.Payout { - // TODO check somewhere if coins are not negative? _, tags, err = k.coinKeeper.AddCoins(ctx, channel.Participants[i], coins) if err != nil { panic(err) @@ -223,7 +224,7 @@ func verifySignatures(channel Channel, update Update) bool { } -// =========================================== QUEUE +// =========================================== SUBMITTED UPDATES QUEUE func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { // always overwrite prexisting values - leave paychan logic to higher levels diff --git a/internal/x/paychan/keeper_test.go b/internal/x/paychan/keeper_test.go index 7435130b..145a4f28 100644 --- a/internal/x/paychan/keeper_test.go +++ b/internal/x/paychan/keeper_test.go @@ -3,8 +3,6 @@ package paychan import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" - //"github.com/stretchr/testify/require" - //"github.com/tendermint/tendermint/crypto" "testing" ) @@ -315,129 +313,3 @@ func TestKeeper(t *testing.T) { }) } - -/* -func privAndAddr() (crypto.PrivKey, sdk.AccAddress) { - priv := ed25519.GenPrivKey() - addr := sdk.AccAddress(priv.PubKey().Address()) - return priv, addr - -sig, err := priv.Sign(signBytes) -*/ - -/* - -func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { - // create db - db := dbm.NewMemDB() - // create keys - authKey := sdk.NewKVStoreKey("authkey") - paychanKey := sdk.NewKVStoreKey("paychankey") - // create new multistore around db - ms := store.NewCommitMultiStore(db) // DB handle plus store key maps - // register separate stores in the multistore - ms.MountStoreWithDB(authKey, sdk.StoreTypeIAVL, db) // sets store key map - ms.MountStoreWithDB(paychanKey, sdk.StoreTypeIAVL, db) - ms.LoadLatestVersion() - - return ms, authKey, paychanKey -} - -func setupCodec() *wire.Codec { - cdc := wire.NewCodec() - auth.RegisterBaseAccount(cdc) - // TODO might need to register paychan struct - return cdc -} - -func TestKeeper(t *testing.T) { - // Setup - - // create multistore and key - ms, authKey, paychanKey := setupMultiStore() - - // create and initialise codec(s) - cdc := setupCodec() - - // create context - ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) - - // create accountMapper - accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) - - // create coinkeeper - coinKeeper := bank.NewKeeper(accountMapper) - - // create keeper - paychanKeeper := NewKeeper(cdc, paychanKey, coinKeeper) - - // Test no paychans exist - _, exists := paychanKeeper.GetPaychan(ctx, sdk.Address{}, sdk.Address{}, 0) - if exists { - t.Error("payment channel found when none exist") - } - - // Test paychan can be set and get - p := Paychan{ - Sender: sdk.Address([]byte("senderAddress")), - Receiver: sdk.Address([]byte("receiverAddress")), - Id: 0, - Balance: sdk.Coins{{"KVA", 100}}, - } - paychanKeeper.setPaychan(ctx, p) - - _, exists = paychanKeeper.GetPaychan(ctx, p.Sender, p.Receiver, p.Id) - if !exists { - t.Error("payment channel not found") - } - - // Test create paychan under normal conditions - senderAddress := sdk.Address([]byte("senderAddress")) - senderFunds := sdk.Coins{{"KVA", 100}} - receiverAddress := sdk.Address([]byte("receiverAddress")) - balance := sdk.Coins{{"KVA", 10}} - - coinKeeper.SetCoins(ctx, senderAddress, senderFunds) - - _, err := paychanKeeper.CreatePaychan(ctx, senderAddress, receiverAddress, balance) - if err != nil { - t.Error("unexpected error created payment channel", err) - } - - p, exists = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) - if !exists { - t.Error("payment channel missing") - } - if !p.Balance.IsEqual(balance) { - t.Error("payment channel balance incorrect", p.Balance, balance) - } - expectedNewSenderFunds := senderFunds.Minus(balance) - if !coinKeeper.GetCoins(ctx, senderAddress).IsEqual(expectedNewSenderFunds) { - t.Error("sender has incorrect balance after paychan creation") - } - - // Test close paychan under normal conditions - senderFunds = coinKeeper.GetCoins(ctx, senderAddress) - receiverAmount := sdk.Coins{{"KVA", 9}} - _, err = paychanKeeper.ClosePaychan(ctx, senderAddress, receiverAddress, 0, receiverAmount) - if err != nil { - t.Error("unexpected error closing payment channel", err) - } - // paychan shouldn't exist - _, exists = paychanKeeper.GetPaychan(ctx, senderAddress, receiverAddress, 0) - if exists { - t.Error("payment channel should not exist") - } - // sender's funds should have increased - expectedNewSenderFunds = senderFunds.Plus(balance.Minus(receiverAmount)) - if !coinKeeper.GetCoins(ctx, senderAddress).IsEqual(expectedNewSenderFunds) { - t.Error("sender has incorrect balance after paychan creation", expectedNewSenderFunds) - } - // receiver's funds should have increased - expectedNewReceiverFunds := receiverAmount // started at zero - if !coinKeeper.GetCoins(ctx, receiverAddress).IsEqual(expectedNewReceiverFunds) { - t.Error("receiver has incorrect balance after paychan creation") - } - -} -*/ diff --git a/internal/x/paychan/test_common.go b/internal/x/paychan/test_common.go index ca9940f4..d102b27f 100644 --- a/internal/x/paychan/test_common.go +++ b/internal/x/paychan/test_common.go @@ -21,10 +21,10 @@ func createMockApp(accountSeeds []string) (sdk.Context, bank.Keeper, Keeper, []s // create channel keeper keyChannel := sdk.NewKVStoreKey("channel") channelKeeper := NewKeeper(mApp.Cdc, keyChannel, coinKeeper) - // add router? + // could add router for msg tests //mapp.Router().AddRoute("channel", NewHandler(channelKeeper)) - mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel}) // needs to be called I think to finish setup + mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel}) // create some accounts genAccFunding := sdk.Coins{sdk.NewCoin("KVA", 1000)} @@ -57,6 +57,5 @@ func createTestGenAccounts(accountSeeds []string, genCoins sdk.Coins) (genAccs [ pubKeys = append(pubKeys, pubKey) addrs = append(addrs, addr) } - return } diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index a1135282..76704ca7 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -16,12 +16,14 @@ type Channel struct { Coins sdk.Coins } -type ChannelID int64 // TODO should this be positive only +const ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time + +type ChannelID int64 // TODO should this be positive only? // The data that is passed between participants as payments, and submitted to the blockchain to close a channel. type Update struct { ChannelID ChannelID - Payout Payout //map[string]sdk.Coins // map of bech32 addresses to coins + Payout Payout //Sequence int64 Not needed for unidirectional channels Sigs [1]UpdateSignature // only sender needs to sign in unidirectional } @@ -40,11 +42,6 @@ func (u Update) GetSignBytes() []byte { return sdk.MustSortJSON(bz) } -type UpdateSignature struct { - PubKey crypto.PubKey - CryptoSignature crypto.Signature -} - type Payout [2]sdk.Coins // a list of coins to be paid to each of Channel.Participants func (p Payout) IsNotNegative() bool { result := true @@ -62,17 +59,10 @@ func (p Payout) Sum() sdk.Coins { return total } -// Get the coins associated with payout address. TODO constrain payouts to only have one entry per address -/*func (payouts Payouts) Get(addr sdk.AccAddress) (sdk.Coins, bool) { - for _, p := range payouts { - if reflect.DeepEqual(p.Address, addr) { - return p.Coins, true - } - } - return nil, false -}*/ - -const ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time +type UpdateSignature struct { + PubKey crypto.PubKey + CryptoSignature crypto.Signature +} // An update that has been submitted to the blockchain, but not yet acted on. type SubmittedUpdate struct { @@ -135,19 +125,6 @@ type MsgCreate struct { Coins sdk.Coins } -//Create a new message. -/* -Called in client code when constructing transaction from cli args to send to the network. -maybe just a placeholder for more advanced future functionality? -func (msg CreatMsg) NewMsgCreate(sender sdk.Address, receiver sdk.Address, amount sdk.Coins) MsgCreate { - return MsgCreate{ - sender - receiver - amount - } -} -*/ - func (msg MsgCreate) Type() string { return "paychan" } func (msg MsgCreate) GetSignBytes() []byte { @@ -164,25 +141,25 @@ func (msg MsgCreate) ValidateBasic() sdk.Error { //TODO implement - /* - // check if all fields present / not 0 valued - if len(msg.Sender) == 0 { - return sdk.ErrInvalidAddress(msg.Sender.String()) - } - if len(msg.Receiver) == 0 { - return sdk.ErrInvalidAddress(msg.Receiver.String()) - } - if len(msg.Amount) == 0 { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - // Check if coins are sorted, non zero, non negative - if !msg.Amount.IsValid() { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - if !msg.Amount.IsPositive() { - return sdk.ErrInvalidCoins(msg.Amount.String()) - } - // TODO check if Address valid? + /* old logic + // check if all fields present / not 0 valued + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) + } + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) + } + if len(msg.Amount) == 0 { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + // Check if coins are sorted, non zero, non negative + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + if !msg.Amount.IsPositive() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + // TODO check if Address valid? */ return nil } @@ -198,12 +175,6 @@ type MsgSubmitUpdate struct { Submitter sdk.AccAddress } -// func (msg MsgSubmitUpdate) NewMsgSubmitUpdate(update Update) MsgSubmitUpdate { -// return MsgSubmitUpdate{ -// update -// } -// } - func (msg MsgSubmitUpdate) Type() string { return "paychan" } func (msg MsgSubmitUpdate) GetSignBytes() []byte { @@ -217,35 +188,33 @@ func (msg MsgSubmitUpdate) GetSignBytes() []byte { func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error { // TODO implement - /* - // check if all fields present / not 0 valued - if len(msg.Sender) == 0 { - return sdk.ErrInvalidAddress(msg.Sender.String()) - } - if len(msg.Receiver) == 0 { - return sdk.ErrInvalidAddress(msg.Receiver.String()) - } - if len(msg.ReceiverAmount) == 0 { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - // check id ≥ 0 - if msg.Id < 0 { - return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors - } - // Check if coins are sorted, non zero, non negative - if !msg.ReceiverAmount.IsValid() { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - if !msg.ReceiverAmount.IsPositive() { - return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) - } - // TODO check if Address valid? + /* old logic + // check if all fields present / not 0 valued + if len(msg.Sender) == 0 { + return sdk.ErrInvalidAddress(msg.Sender.String()) + } + if len(msg.Receiver) == 0 { + return sdk.ErrInvalidAddress(msg.Receiver.String()) + } + if len(msg.ReceiverAmount) == 0 { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + // check id ≥ 0 + if msg.Id < 0 { + return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors + } + // Check if coins are sorted, non zero, non negative + if !msg.ReceiverAmount.IsValid() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + if !msg.ReceiverAmount.IsPositive() { + return sdk.ErrInvalidCoins(msg.ReceiverAmount.String()) + } + // TODO check if Address valid? */ return nil } func (msg MsgSubmitUpdate) GetSigners() []sdk.AccAddress { - // Signing not strictly necessary as signatures contained within the channel update. - // TODO add signature by submitting address return []sdk.AccAddress{msg.Submitter} } diff --git a/internal/x/paychan/wire.go b/internal/x/paychan/wire.go index ee3da559..d8a745aa 100644 --- a/internal/x/paychan/wire.go +++ b/internal/x/paychan/wire.go @@ -9,7 +9,7 @@ func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(MsgSubmitUpdate{}, "paychan/MsgSubmitUpdate", nil) } -// TODO move this to near the msg definitions +// TODO move this to near the msg definitions? var msgCdc = wire.NewCodec() func init() { From 60d5a03bb186e283c34d65481cd1c9ca0329d79b Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 3 Sep 2018 16:38:51 -0400 Subject: [PATCH 27/30] print out update from get pachan command --- internal/x/paychan/README.md | 9 ++++++--- internal/x/paychan/client/cmd/cmd.go | 22 ++++++++++++++++++++-- internal/x/paychan/endblocker_test.go | 2 ++ internal/x/paychan/keeper.go | 8 ++++---- internal/x/paychan/types.go | 2 +- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/internal/x/paychan/README.md b/internal/x/paychan/README.md index 720832e2..463f495c 100644 --- a/internal/x/paychan/README.md +++ b/internal/x/paychan/README.md @@ -2,7 +2,7 @@ This module implements simple but feature complete unidirectional payment channels. Channels can be opened by a sender and closed immediately by the receiver, or by the sender subject to a dispute period. There are no top-ups or partial withdrawals (yet). Channels support multiple currencies. ->Note: This is a work in progress. More feature planned. More test cases needed. +>Note: This module is still a bit rough around the edges. More feature planned. More test cases needed. # Usage @@ -15,7 +15,7 @@ Send a payment for 10 KVA. kvcli paychan pay --from --sen-amt 90KVA --rec-amt 10KVA --chan-id --filename payment.json --chain-id -Send the file payment.json to your receiver. Then they run the following to verify. +Send the file `payment.json` to your receiver. Then they run the following to verify. kvcli paychan verify --filename payment.json @@ -24,14 +24,17 @@ The receiver can close immediately at any time. kvcli paychan submit --from --payment payment.json --chain-id -The sender can close subject to a dispute period during which the receiver can overrule them. +The sender can submit a close request, causing the channel will close automatically after a dispute period. During this period a receiver can still close immediately. kvcli paychan submit --from --payment payment.json --chain-id +>Note: The dispute period on the testnet is 30 seconds for ease of testing. + ## Get info on a channel kvcli get --chan-id +This will print out a channel, if it exists, and any submitted close requests. # TODOs diff --git a/internal/x/paychan/client/cmd/cmd.go b/internal/x/paychan/client/cmd/cmd.go index 12dfd541..c7b8457f 100644 --- a/internal/x/paychan/client/cmd/cmd.go +++ b/internal/x/paychan/client/cmd/cmd.go @@ -264,7 +264,7 @@ func GetChannelCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { cmd := &cobra.Command{ Use: "get", Short: "Get info on a channel.", - Long: "Get information on a non closed channel.", + Long: "Get the details of a non closed channel plus any submitted update waiting to be executed.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -287,9 +287,27 @@ func GetChannelCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command { if err != nil { return err } - // print out json channel fmt.Println(string(jsonChannel)) + + // Get any submitted updates from the node + res, err = ctx.QueryStore(paychan.GetSubmittedUpdateKey(id), paychanStoreName) + if err != nil { + return err + } + // Print out the submited update if it exsits + if len(res) != 0 { + var submittedUpdate paychan.SubmittedUpdate + cdc.MustUnmarshalBinary(res, &submittedUpdate) + + // Convert the submitted update to a json object for pretty printing + jsonSU, err := wire.MarshalJSONIndent(cdc, submittedUpdate) + if err != nil { + return err + } + // print out json submitted update + fmt.Println(string(jsonSU)) + } return nil }, } diff --git a/internal/x/paychan/endblocker_test.go b/internal/x/paychan/endblocker_test.go index 655a2fa1..62fd276e 100644 --- a/internal/x/paychan/endblocker_test.go +++ b/internal/x/paychan/endblocker_test.go @@ -7,6 +7,8 @@ import ( ) func TestEndBlocker(t *testing.T) { + // TODO test that endBlocker doesn't close channels before the execution time + // SETUP accountSeeds := []string{"senderSeed", "receiverSeed"} ctx, _, channelKeeper, addrs, _, _, _ := createMockApp(accountSeeds) diff --git a/internal/x/paychan/keeper.go b/internal/x/paychan/keeper.go index 47bb90d1..14324795 100644 --- a/internal/x/paychan/keeper.go +++ b/internal/x/paychan/keeper.go @@ -283,7 +283,7 @@ func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (Submit // load from DB store := ctx.KVStore(k.storeKey) - bz := store.Get(k.getSubmittedUpdateKey(channelID)) + bz := store.Get(GetSubmittedUpdateKey(channelID)) var sUpdate SubmittedUpdate if bz == nil { @@ -301,16 +301,16 @@ func (k Keeper) setSubmittedUpdate(ctx sdk.Context, sUpdate SubmittedUpdate) { // marshal bz := k.cdc.MustMarshalBinary(sUpdate) // panics if something goes wrong // write to db - key := k.getSubmittedUpdateKey(sUpdate.ChannelID) + key := GetSubmittedUpdateKey(sUpdate.ChannelID) store.Set(key, bz) // panics if something goes wrong } func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ChannelID) { store := ctx.KVStore(k.storeKey) - store.Delete(k.getSubmittedUpdateKey(channelID)) + store.Delete(GetSubmittedUpdateKey(channelID)) // TODO does this have return values? What happens when key doesn't exist? } -func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte { +func GetSubmittedUpdateKey(channelID ChannelID) []byte { return []byte(fmt.Sprintf("submittedUpdate:%d", channelID)) } diff --git a/internal/x/paychan/types.go b/internal/x/paychan/types.go index 76704ca7..57d44b52 100644 --- a/internal/x/paychan/types.go +++ b/internal/x/paychan/types.go @@ -16,7 +16,7 @@ type Channel struct { Coins sdk.Coins } -const ChannelDisputeTime = int64(2000) // measured in blocks TODO pick reasonable time +const ChannelDisputeTime = int64(6) // measured in blocks TODO pick reasonable time, add to channel or genesis type ChannelID int64 // TODO should this be positive only? From b7898a1d3b21050f5df421b2a1b9243468e9342c Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 3 Sep 2018 21:51:41 -0400 Subject: [PATCH 28/30] add new testnet --- README.md | 10 ++-- testnets/kava-test-2/genesis.json | 99 +++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 testnets/kava-test-2/genesis.json diff --git a/README.md b/README.md index b876c974..89c8c260 100644 --- a/README.md +++ b/README.md @@ -79,15 +79,15 @@ TODO users need to set up keys first? ## Run a Full Node - kvd init --name --chain-id kava-test-1 + kvd init --name --chain-id kava-test-2 This will generate config and keys in `$HOME/.kvd` and `$HOME/.kvcli`. The default password is 'password'. > Note: Make sure `GOBIN` is set and added to your path if you want to be able to run installed go programs from any folder. -Copy the testnet genesis file (from https://raw.githubusercontent.com/Kava-Labs/kava/master/testnets/kava-test-1/genesis.json) into `$HOME/.kvd/config/`, replacing the existing one. +Copy the testnet genesis file (from https://raw.githubusercontent.com/Kava-Labs/kava/master/testnets/kava-test-2/genesis.json) into `$HOME/.kvd/config/`, replacing the existing one. -Add the kava node address, `0dfd43e440e34fc193ddee4ae99547184f3cb5d1@validator.connector.kava.io:26656`, to `seeds` in `$HOME/.kvd/config/config.toml` +Add the kava node address, `5c2bc5a95b014e4b2897791565398ee6bfd0a04a@validator.connector.kava.io:26656`, to `seeds` in `$HOME/.kvd/config/config.toml` Start your full node @@ -112,7 +112,7 @@ Then, your full running in the background or separate window, run: --pubkey \ --address-validator \ --moniker "" \ - --chain-id kava-test-1 \ + --chain-id kava-test-2 \ --from > Note You'll need to type in the default password "password" @@ -124,6 +124,6 @@ In order to stop validating, first remove yourself as validator, then you can st kvcli stake unbond begin \ --address-delegator \ --address-validator \ - --chain-id kava-test-1 \ + --chain-id kava-test-2 \ --shares-percent 1 \ --from diff --git a/testnets/kava-test-2/genesis.json b/testnets/kava-test-2/genesis.json new file mode 100644 index 00000000..17e0919f --- /dev/null +++ b/testnets/kava-test-2/genesis.json @@ -0,0 +1,99 @@ +{ + "genesis_time": "2018-09-04T01:25:04.497806902Z", + "chain_id": "kava-test-2", + "consensus_params": { + "block_size_params": { + "max_bytes": "22020096", + "max_txs": "10000", + "max_gas": "-1" + }, + "tx_size_params": { + "max_bytes": "10240", + "max_gas": "-1" + }, + "block_gossip_params": { + "block_part_size_bytes": "65536" + }, + "evidence_params": { + "max_age": "100000" + } + }, + "validators": [ + { + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "kcaCf2NcjO5I+jYTOnTsPMxXvO5m/c6HEUBKwYlLKFo=" + }, + "power": "1000", + "name": "" + } + ], + "app_hash": "", + "app_state": { + "accounts": [ + { + "address": "cosmosaccaddr1f6jrk3rgt9e647mjkeqqvq0yhl7wnntm5r6tyk", + "coins": [ + { + "denom": "KVA", + "amount": "99000" + } + ] + } + ], + "stake": { + "pool": { + "loose_tokens": "100000", + "bonded_tokens": "0", + "inflation_last_time": "0", + "inflation": "7/100", + "date_last_commission_reset": "0", + "prev_bonded_shares": "0" + }, + "params": { + "inflation_rate_change": "13/100", + "inflation_max": "1/5", + "inflation_min": "7/100", + "goal_bonded": "67/100", + "unbonding_time": "259200", + "max_validators": 100, + "bond_denom": "KVA" + }, + "validators": [ + { + "owner": "cosmosaccaddr1f6jrk3rgt9e647mjkeqqvq0yhl7wnntm5r6tyk", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "kcaCf2NcjO5I+jYTOnTsPMxXvO5m/c6HEUBKwYlLKFo=" + }, + "revoked": false, + "status": 0, + "tokens": "1000", + "delegator_shares": "1000", + "description": { + "moniker": "kava-validator", + "identity": "", + "website": "", + "details": "" + }, + "bond_height": "0", + "bond_intra_tx_counter": 0, + "proposer_reward_pool": [], + "commission": "0", + "commission_max": "0", + "commission_change_rate": "0", + "commission_change_today": "0", + "prev_bonded_tokens": "0" + } + ], + "bonds": [ + { + "delegator_addr": "cosmosaccaddr1f6jrk3rgt9e647mjkeqqvq0yhl7wnntm5r6tyk", + "validator_addr": "cosmosaccaddr1f6jrk3rgt9e647mjkeqqvq0yhl7wnntm5r6tyk", + "shares": "1000", + "height": "0" + } + ] + } + } +} \ No newline at end of file From 54592a23c697b8518bc3aa66e8956159bef362c6 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Mon, 3 Sep 2018 23:10:30 -0400 Subject: [PATCH 29/30] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 89c8c260..7d1b04ae 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ Building on the work of Tendermint and Interledger. Project status: We're currently in a very early public testnet. With future features being implemented. -Try it out - run a full node to sync to the testnet, or set up as a validator. - +Try it out - run a full node to sync to the testnet, [send some off chain payments](internal/x/paychan/README.md), or set up as a validator. ## Install From 9f2c9b033912deb59846290859883bab876363b5 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Tue, 4 Sep 2018 11:21:58 -0400 Subject: [PATCH 30/30] specify commit for testnet --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7d1b04ae..49052ac9 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Requirements: go installed and set up (version 1.10+). cd $GOPATH/src/github.com/kava-labs git clone https://github.com/kava-labs/kava cd kava + git checkout 8c9406c 2. Install the dependencies.