diff --git a/network/stream/stream.go b/network/stream/stream.go index 9a3e53eade..83e237ef1f 100644 --- a/network/stream/stream.go +++ b/network/stream/stream.go @@ -34,6 +34,7 @@ import ( "github.com/ethersphere/swarm/p2p/protocols" "github.com/ethersphere/swarm/state" "github.com/ethersphere/swarm/storage" + "github.com/ethersphere/swarm/swap" ) const ( @@ -658,13 +659,13 @@ func (sp *StreamerPrices) Price(msg interface{}) *protocols.Price { // Instead of hardcoding the price, get it // through a function - it could be quite complex in the future func (sp *StreamerPrices) getRetrieveRequestMsgPrice() uint64 { - return RetrieveRequestMsgPrice + return swap.RetrieveRequestMsgPrice } // Instead of hardcoding the price, get it // through a function - it could be quite complex in the future func (sp *StreamerPrices) getChunkDeliveryMsgRetrievalPrice() uint64 { - return ChunkDeliveryMsgRetrievalPrice + return swap.ChunkDeliveryMsgRetrievalPrice } // createPriceOracle sets up a matrix which can be queried to get diff --git a/swap/defaults.go b/swap/defaults.go index 63cfa3bb2f..494c955465 100644 --- a/swap/defaults.go +++ b/swap/defaults.go @@ -1,6 +1,23 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + package swap -// Uknown for now, placeholder values +// These are currently arbitrary values which have not been verified nor tested +// Need experimentation to arrive to values which make sense const ( DefaultPaymentThreshold = 1000000 DefaultDisconnectThreshold = 1500000 diff --git a/swap/peer.go b/swap/peer.go index e39f7c1725..532cbc822e 100644 --- a/swap/peer.go +++ b/swap/peer.go @@ -74,15 +74,10 @@ func (sp *Peer) handleMsg(ctx context.Context, msg interface{}) error { // a cheque from a creditor // TODO: validate the contract address in the cheque to match the address given at handshake // TODO: this should not be blocking -func (sp *Peer) handleEmitChequeMsg(ctx context.Context, msg interface{}) error { +func (sp *Peer) handleEmitChequeMsg(ctx context.Context, msg *EmitChequeMsg) error { log.Info("received emit cheque message") - chequeMsg, ok := msg.(*EmitChequeMsg) - if !ok { - return fmt.Errorf("Invalid message type, %v", msg) - } - - cheque := chequeMsg.Cheque + cheque := msg.Cheque if cheque.Contract != sp.contractAddress { return fmt.Errorf("wrong cheque parameters: expected contract: %s, was: %s", sp.contractAddress, cheque.Contract) } @@ -106,7 +101,7 @@ func (sp *Peer) handleEmitChequeMsg(ctx context.Context, msg interface{}) error // reset balance by amount // as this is done by the creditor, receiving the cheque, the amount should be negative, // so that updateBalance will calculate balance + amount which result in reducing the peer's balance - sp.swap.resetBalance(sp.ID(), 0-int64(cheque.Amount)) + sp.swap.resetBalance(sp.ID(), 0-int64(cheque.Honey)) // send confirmation if err := sp.Send(ctx, &ConfirmMsg{}); err != nil { log.Error("error while sending confirm msg", "peer", sp.ID().String(), "err", err.Error()) diff --git a/swap/protocol.go b/swap/protocol.go index d08b6a23cc..009b1d5e1b 100644 --- a/swap/protocol.go +++ b/swap/protocol.go @@ -29,18 +29,20 @@ import ( // ErrEmptyAddressInSignature is used when the empty address is used for the chequebook in the handshake var ErrEmptyAddressInSignature = errors.New("empty address in handshake") +// Spec is the swap protocol specification var Spec = &protocols.Spec{ Name: "swap", Version: 1, MaxMsgSize: 10 * 1024 * 1024, Messages: []interface{}{ - SwapHandshakeMsg{}, + HandshakeMsg{}, EmitChequeMsg{}, ErrorMsg{}, ConfirmMsg{}, }, } +// Protocols is a node.Service interface method func (s *Swap) Protocols() []p2p.Protocol { return []p2p.Protocol{ { @@ -52,6 +54,7 @@ func (s *Swap) Protocols() []p2p.Protocol { } } +// APIs is a node.Service interface method func (s *Swap) APIs() []rpc.API { return []rpc.API{ { @@ -63,18 +66,20 @@ func (s *Swap) APIs() []rpc.API { } } +// Start is a node.Service interface method func (s *Swap) Start(server *p2p.Server) error { log.Info("Swap service started") return nil } +// Stop is a node.Service interface method func (s *Swap) Stop() error { return nil } // verifyHandshake verifies the chequebook address transmitted in the swap handshake func (s *Swap) verifyHandshake(msg interface{}) error { - handshake, ok := msg.(*SwapHandshakeMsg) + handshake, ok := msg.(*HandshakeMsg) if !ok || (handshake.ContractAddress == common.Address{}) { return ErrEmptyAddressInSignature } @@ -82,22 +87,23 @@ func (s *Swap) verifyHandshake(msg interface{}) error { return s.verifyContract(context.TODO(), handshake.ContractAddress) } +// run is the actual swap protocol run method func (s *Swap) run(p *p2p.Peer, rw p2p.MsgReadWriter) error { protoPeer := protocols.NewPeer(p, rw, Spec) - answer, err := protoPeer.Handshake(context.TODO(), &SwapHandshakeMsg{ + answer, err := protoPeer.Handshake(context.TODO(), &HandshakeMsg{ ContractAddress: s.owner.Contract, }, s.verifyHandshake) if err != nil { return err } - beneficiary, err := s.getContractOwner(context.TODO(), answer.(*SwapHandshakeMsg).ContractAddress) + beneficiary, err := s.getContractOwner(context.TODO(), answer.(*HandshakeMsg).ContractAddress) if err != nil { return err } - swapPeer := NewPeer(protoPeer, s, s.backend, beneficiary, answer.(*SwapHandshakeMsg).ContractAddress) + swapPeer := NewPeer(protoPeer, s, s.backend, beneficiary, answer.(*HandshakeMsg).ContractAddress) s.lock.Lock() s.peers[p.ID()] = swapPeer @@ -108,5 +114,6 @@ func (s *Swap) run(p *p2p.Peer, rw p2p.MsgReadWriter) error { return swapPeer.Run(swapPeer.handleMsg) } +// PublicAPI would be the public API accessor for protocol methods type PublicAPI struct { } diff --git a/swap/protocol_test.go b/swap/protocol_test.go index a9dba10cf4..7746097640 100644 --- a/swap/protocol_test.go +++ b/swap/protocol_test.go @@ -16,105 +16,81 @@ package swap -// TODO: Update this test. -// We are no longer sending the cheque request messages, we send the cheques directly now. -/* -TestRequestCheque tests that a peer will respond with a -`EmitChequeMsg` containing a cheque for an expected amount -if it sends a `RequestChequeMsg` is sent to it -*/ -// func TestRequestCheque(t *testing.T) { -// var err error - -// // setup test swap object -// swap, dir := createTestSwap(t) -// defer os.RemoveAll(dir) +import ( + "os" + "testing" -// // dummy object so we can run the protocol -// ss := swap + "github.com/ethereum/go-ethereum/common" + p2ptest "github.com/ethereum/go-ethereum/p2p/testing" +) -// // setup the protocolTester, which will allow protocol testing by sending messages -// protocolTester := p2ptest.NewProtocolTester(swap.owner.privateKey, 2, ss.run) - -// // shortcut to creditor node -// creditor := protocolTester.Nodes[0] +/* +TestEmitCheque tests an end-to-end exchange between a debitor peer +and its creditor. The debitor issues the cheque, the creditor receives it +and responds with a confirmation +*/ +func TestEmitCheque(t *testing.T) { + var err error -// // set balance artifially -// swap.balances[creditor.ID()] = -42 + // setup test swap object + swap, dir := newTestSwap(t) + defer os.RemoveAll(dir) -// // create the expected cheque to be received -// // NOTE: this may be improved, as it is essentially running the same -// // code as in production -// expectedCheque := swap.cheques[creditor.ID()] -// expectedCheque = &Cheque{ -// ChequeParams: ChequeParams{ -// Serial: uint64(1), -// Amount: uint64(42), -// Timeout: defaultCashInDelay, -// Beneficiary: crypto.PubkeyToAddress(*creditor.Pubkey()), -// }, -// } + // setup the protocolTester, which will allow protocol testing by sending messages + protocolTester := p2ptest.NewProtocolTester(swap.owner.privateKey, 2, swap.run) -// // sign the cheque -// expectedCheque.Sig, err = swap.signContent(expectedCheque) -// if err != nil { -// t.Fatal(err) -// } + // shortcut to creditor node + debitor := protocolTester.Nodes[0] + creditor := protocolTester.Nodes[1] -// // run the exchange: -// // trigger a `ChequeRequestMsg` -// // expect a `EmitChequeMsg` with a valid cheque -// err = protocolTester.TestExchanges(p2ptest.Exchange{ -// Label: "TestRequestCheque", -// Triggers: []p2ptest.Trigger{ -// { -// Code: 0, -// Msg: &ChequeRequestMsg{ -// crypto.PubkeyToAddress(*creditor.Pubkey()), -// }, -// Peer: creditor.ID(), -// }, -// }, -// Expects: []p2ptest.Expect{ -// { -// Code: 1, -// Msg: &EmitChequeMsg{ -// Cheque: expectedCheque, -// }, -// Peer: creditor.ID(), -// }, -// }, -// }) + // set balance artifially + swap.balances[creditor.ID()] = -42 -// // there should be no error at this point -// if err != nil { -// t.Fatal(err) -// } + // create the expected cheque to be received + cheque := newTestCheque() -// // now we request a new cheque; -// // the peer though should have already reset the balance, -// // so no new cheque should be issued -// err = protocolTester.TestExchanges(p2ptest.Exchange{ -// Label: "TestRequestNoCheque", -// Triggers: []p2ptest.Trigger{ -// { -// Code: 0, -// Msg: &ChequeRequestMsg{ -// crypto.PubkeyToAddress(*creditor.Pubkey()), -// }, -// Peer: creditor.ID(), -// }, -// }, -// }) + // sign the cheque + cheque.Sig, err = swap.signContent(cheque) + if err != nil { + t.Fatal(err) + } -// // -// if err != nil { -// t.Fatal(err) -// } + // run the exchange: + // trigger a `ChequeRequestMsg` + // expect a `EmitChequeMsg` with a valid cheque + err = protocolTester.TestExchanges(p2ptest.Exchange{ + Label: "TestRequestCheque", + Triggers: []p2ptest.Trigger{ + { + Code: 1, + Msg: &EmitChequeMsg{ + Cheque: cheque, + }, + Peer: debitor.ID(), + }, + }, + Expects: []p2ptest.Expect{ + { + Code: 0, + Msg: &HandshakeMsg{ + ContractAddress: common.Address{}, + }, + Peer: creditor.ID(), + }, + { + Code: 0, + Msg: &HandshakeMsg{ + ContractAddress: common.Address{}, + }, + Peer: debitor.ID(), + }, + }, + }) -// // no new cheques should have been emitted -// if len(swap.cheques) != 1 { -// t.Fatalf("Expected unchanged number of cheques, but it changed: %d", len(swap.cheques)) -// } + // there should be no error at this point + if err != nil { + t.Fatal(err) + } -// } + // TODO: Test further exchanges +} diff --git a/swap/swap.go b/swap/swap.go index 7d16f6ffcd..4109c2d9f7 100644 --- a/swap/swap.go +++ b/swap/swap.go @@ -127,7 +127,7 @@ func (s *Swap) DeploySuccess() string { // Add is the (sole) accounting function // Swap implements the protocols.Balance interface -func (s *Swap) Add(honey int64, peer *protocols.Peer) (err error) { +func (s *Swap) Add(amount int64, peer *protocols.Peer) (err error) { s.lock.Lock() defer s.lock.Unlock() @@ -146,14 +146,6 @@ func (s *Swap) Add(honey int64, peer *protocols.Peer) (err error) { return errors.New(disconnectMessage) } - // convert honey to ETH - var amount int64 - amount, err = s.oracle.GetPrice(honey) - if err != nil { - log.Error("error getting price from oracle", "err", err) - return - } - // calculate new balance var newBalance int64 newBalance, err = s.updateBalance(peer.ID(), amount) @@ -242,8 +234,19 @@ func (s *Swap) createCheque(peer enode.ID) (*Cheque, error) { beneficiary := swapPeer.beneficiary peerBalance := s.balances[peer] - amount := -peerBalance + honey := -peerBalance + // convert honey to ETH + var amount int64 + amount, err = s.oracle.GetPrice(honey) + if err != nil { + log.Error("error getting price from oracle", "err", err) + return nil, err + } + + // we need to ignore the error check when loading from the StateStore, + // as an error might indicate that there is no existing cheque, which + // could mean it's the first interaction, which is absolutely valid _ = s.loadCheque(peer) lastCheque := s.cheques[peer] @@ -264,7 +267,9 @@ func (s *Swap) createCheque(peer enode.ID) (*Cheque, error) { } cheque.ChequeParams.Timeout = defaultCashInDelay cheque.ChequeParams.Contract = s.owner.Contract + cheque.ChequeParams.Honey = uint64(honey) cheque.Beneficiary = beneficiary + cheque.Sig, err = s.signContent(cheque) return cheque, err diff --git a/swap/swap_test.go b/swap/swap_test.go index f1afd8b56a..0d324b23b4 100644 --- a/swap/swap_test.go +++ b/swap/swap_test.go @@ -163,52 +163,71 @@ func TestRepeatedBookings(t *testing.T) { } // TestResetBalance tests that balances are correctly reset +// The test deploys creates swap instances for each node, +// deploys simulated contracts, sets the balance of each +// other node to some arbitrary number above thresholds, +// and then calls both `sendCheque` on one side and +// `handleEmitChequeMsg` in order to simulate a roundtrip +// and see that both have reset the balance correctly func TestResetBalance(t *testing.T) { - // create a test swap accounts - creditorSwap, testDir1 := newTestSwap2(t) - debitorSwap, testDir2 := newTestSwap2(t) + // create both test swap accounts + creditorSwap, testDir1 := newTestSwap(t) + debitorSwap, testDir2 := newTestSwap(t) defer os.RemoveAll(testDir1) defer os.RemoveAll(testDir2) ctx := context.Background() + // deploying would strictly speaking not be necessary, as the signing would also just work + // with empty contract addresses. Nevertheless to avoid later suprises and for + // coherence and clarity we deploy here so that we get a simulated contract address testDeploy(ctx, creditorSwap.backend, creditorSwap) testDeploy(ctx, debitorSwap.backend, debitorSwap) creditorSwap.backend.(*backends.SimulatedBackend).Commit() debitorSwap.backend.(*backends.SimulatedBackend).Commit() + // create Peer instances + // NOTE: remember that these are peer instances representing each **a model of the remote peer** for every local node + // so creditor is the model of the remote mode for the debitor! (and vice versa) cPeer := newDummyPeerWithSpec(Spec) dPeer := newDummyPeerWithSpec(Spec) - creditorPeerModelForDebitor := NewPeer(cPeer.Peer, debitorSwap, debitorSwap.backend, creditorSwap.owner.address, debitorSwap.owner.Contract) - debitorPeerModelForCreditor := NewPeer(dPeer.Peer, creditorSwap, creditorSwap.backend, debitorSwap.owner.address, debitorSwap.owner.Contract) + creditor := NewPeer(cPeer.Peer, debitorSwap, debitorSwap.backend, creditorSwap.owner.address, debitorSwap.owner.Contract) + debitor := NewPeer(dPeer.Peer, creditorSwap, creditorSwap.backend, debitorSwap.owner.address, debitorSwap.owner.Contract) + // set balances arbitrarily testAmount := int64(DefaultPaymentThreshold + 42) - creditorSwap.balances[debitorPeerModelForCreditor.ID()] = testAmount - debitorSwap.balances[creditorPeerModelForDebitor.ID()] = 0 - testAmount + creditorSwap.balances[debitor.ID()] = testAmount + debitorSwap.balances[creditor.ID()] = 0 - testAmount - creditorSwap.peers[debitorPeerModelForCreditor.ID()] = debitorPeerModelForCreditor - debitorSwap.peers[creditorPeerModelForDebitor.ID()] = creditorPeerModelForDebitor + // set the peers into each other's list + creditorSwap.peers[debitor.ID()] = debitor + debitorSwap.peers[creditor.ID()] = creditor - debitorSwap.sendCheque(creditorPeerModelForDebitor.ID()) - if debitorSwap.balances[creditorPeerModelForDebitor.ID()] != 0 { - t.Fatalf("unexpected balance to be 0, but it is %d", debitorSwap.balances[creditorPeerModelForDebitor.ID()]) + // now simulate sending the cheque to the creditor from the debitor + debitorSwap.sendCheque(creditor.ID()) + // the debitor should have already reset its balance + if debitorSwap.balances[creditor.ID()] != 0 { + t.Fatalf("unexpected balance to be 0, but it is %d", debitorSwap.balances[creditor.ID()]) } var err error - cheque := debitorSwap.cheques[creditorPeerModelForDebitor.ID()] + // now load the cheque that the debitor created... + cheque := debitorSwap.cheques[creditor.ID()] if cheque == nil { t.Fatal("expected to find a cheque, but it was empty") } + // ...create a message... msg := &EmitChequeMsg{ Cheque: cheque, } - - err = debitorPeerModelForCreditor.handleEmitChequeMsg(ctx, msg) + // ...and trigger message handling on the receiver side (creditor) + // remember that debitor is the model of the remote node for the creditor... + err = debitor.handleEmitChequeMsg(ctx, msg) if err != nil { t.Fatal(err) } - - if creditorSwap.balances[debitorPeerModelForCreditor.ID()] != 0 { - t.Fatalf("unexpected balance to be 0, but it is %d", creditorSwap.balances[debitorPeerModelForCreditor.ID()]) + // finally check that the creditor also successfully reset the balances + if creditorSwap.balances[debitor.ID()] != 0 { + t.Fatalf("unexpected balance to be 0, but it is %d", creditorSwap.balances[debitor.ID()]) } } @@ -310,7 +329,7 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { // create a test swap account with a backend // creates a stateStore for persistence and a Swap account -func newTestSwapWithBackend(t *testing.T, backend *backends.SimulatedBackend) (*Swap, string) { +func newTestSwap(t *testing.T) (*Swap, string) { dir, err := ioutil.TempDir("", "swap_test_store") if err != nil { t.Fatal(err) @@ -324,42 +343,21 @@ func newTestSwapWithBackend(t *testing.T, backend *backends.SimulatedBackend) (* t.Fatal(err) } - swap := New(stateStore, key, common.Address{}, backend) - return swap, dir -} + log.Debug("creating simulated backend") -func newTestSwap2(t *testing.T) (*Swap, string) { - dir, err := ioutil.TempDir("", "swap_test_store") - if err != nil { - t.Fatal(err) - } - stateStore, err2 := state.NewDBStore(dir) - if err2 != nil { - t.Fatal(err2) - } - key, err := crypto.GenerateKey() - if err != nil { - t.Fatal(err) - } + gasLimit := uint64(8000000) + owner := crypto.PubkeyToAddress(key.PublicKey) + defaultBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ + owner: {Balance: big.NewInt(1000000000)}, + ownerAddress: {Balance: big.NewInt(1000000000)}, + beneficiaryAddress: {Balance: big.NewInt(1000000000)}, + }, gasLimit) - address := crypto.PubkeyToAddress(key.PublicKey) - backend := backends.NewSimulatedBackend(core.GenesisAlloc{ - address: {Balance: big.NewInt(1000000000)}, - }, 8000000) - swap := New(stateStore, key, address, backend) + swap := New(stateStore, key, common.Address{}, defaultBackend) + defaultBackend.Commit() return swap, dir } -// create a test swap account -// create a default empty backend -// creates a stateStore for persistence and a Swap account -func newTestSwap(t *testing.T) (*Swap, string) { - defaultBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ - ownerAddress: {Balance: big.NewInt(1000000000)}, - }, 8000000) - return newTestSwapWithBackend(t, defaultBackend) -} - type dummyPeer struct { *protocols.Peer } @@ -553,26 +551,16 @@ func TestVerifyContractWrongContract(t *testing.T) { // and immediately try to cash-in the cheque func TestContractIntegration(t *testing.T) { - log.Debug("creating simulated backend") - - gasLimit := uint64(10000000) - balance := new(big.Int) - balance.SetString("1000000000", 10) - backend := backends.NewSimulatedBackend(core.GenesisAlloc{ - ownerAddress: {Balance: balance}, - beneficiaryAddress: {Balance: balance}, - }, gasLimit) - log.Debug("creating test swap") - issuerSwap, dir := newTestSwapWithBackend(t, backend) + issuerSwap, dir := newTestSwap(t) defer os.RemoveAll(dir) - backend.Commit() - issuerSwap.owner.address = ownerAddress issuerSwap.owner.privateKey = ownerKey + backend := issuerSwap.backend.(*backends.SimulatedBackend) + log.Debug("deploy issuer swap") ctx := context.TODO() diff --git a/swap/types.go b/swap/types.go index dc0ae167f1..f3941440d7 100644 --- a/swap/types.go +++ b/swap/types.go @@ -23,22 +23,25 @@ import ( // TODO: add handshake protocol where we exchange last cheque (this is useful if one node disconnects) // FIXME: Check the contract bytecode of the counterparty +// ChequeParams encapsulate all cheque parameters type ChequeParams struct { Contract common.Address // address of chequebook, needed to avoid cross-contract submission - Beneficiary common.Address - Serial uint64 // cumulative amount of all funds sent - Amount uint64 // cumulative amount of all funds sent - Timeout uint64 + Beneficiary common.Address // address of the beneficiary, the contract which will redeem the cheque + Serial uint64 // monotonically increasing serial number + Amount uint64 // cumulative amount of the cheque in currency + Honey uint64 // amount of honey which resulted in the cumulative currency difference + Timeout uint64 // timeout for cashing in } +// Cheque encapsulates the parameters and the signature // TODO: There should be a request cheque struct that only gives the Serial -// Cheque encapsulates the cheque information type Cheque struct { ChequeParams Sig []byte // signature Sign(Keccak256(contract, beneficiary, amount), prvKey) } -type SwapHandshakeMsg struct { +// HandshakeMsg is exchanged on peer handshake +type HandshakeMsg struct { ContractAddress common.Address } @@ -52,5 +55,5 @@ type ErrorMsg struct{} // ConfirmMsg is sent from the creditor to the debitor to confirm cheque reception type ConfirmMsg struct { - Cheque Cheque // TODO: probably not needed and if so likely should not include the full cheque + Cheque *Cheque // TODO: probably not needed and if so likely should not include the full cheque }